- Added a new Archify skill, enabling users to create polished architecture, workflow, sequence, data-flow, and lifecycle diagrams. - Implemented comprehensive functionality including rendering, validation, and delivery of diagrams in various formats. - Integrated a user-friendly command-line interface for generating and previewing diagrams. - Developed supporting files including package.json, LICENSE, and SKILL.md for documentation and licensing. - Added unit tests to ensure reliability and functionality of the new skill. These changes enhance the application by providing a structured approach to visualizing system architecture and workflows, improving user experience and data representation.
183 lines
6.6 KiB
JavaScript
183 lines
6.6 KiB
JavaScript
export const SKILL_ID = 'archify';
|
|
export const EXPECTED_REPOSITORY = 'https://github.com/tt-a1i/archify';
|
|
export const DEFAULT_MANIFEST_URL = 'https://tt-a1i.github.io/archify/skill-updates/archify/stable.json';
|
|
|
|
const CONTROL_OR_BIDI = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u;
|
|
const HEX_40 = /^[a-f0-9]{40}$/;
|
|
const HEX_64 = /^[a-f0-9]{64}$/;
|
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
const UTC_SECONDS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
|
|
export class UpdateContractError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = 'UpdateContractError';
|
|
}
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function hasExactKeys(value, expected) {
|
|
return isPlainObject(value)
|
|
&& Object.keys(value).sort().join('\0') === [...expected].sort().join('\0');
|
|
}
|
|
|
|
export function parseSemver(value) {
|
|
if (typeof value !== 'string' || value.length > 128) {
|
|
throw new UpdateContractError(`invalid SemVer: ${JSON.stringify(value)}`);
|
|
}
|
|
const match = SEMVER.exec(value);
|
|
if (!match) throw new UpdateContractError(`invalid SemVer: ${JSON.stringify(value)}`);
|
|
const prerelease = match[4]?.split('.') ?? null;
|
|
if (prerelease?.some((identifier) => /^\d+$/.test(identifier)
|
|
&& identifier.length > 1 && identifier.startsWith('0'))) {
|
|
throw new UpdateContractError(`invalid SemVer: ${JSON.stringify(value)}`);
|
|
}
|
|
return {
|
|
core: match.slice(1, 4),
|
|
prerelease,
|
|
build: match[5]?.split('.') ?? null,
|
|
};
|
|
}
|
|
|
|
function compareNumericIdentifiers(left, right) {
|
|
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
if (left === right) return 0;
|
|
return left < right ? -1 : 1;
|
|
}
|
|
|
|
function comparePrerelease(left, right) {
|
|
if (left === null && right === null) return 0;
|
|
if (left === null) return 1;
|
|
if (right === null) return -1;
|
|
const length = Math.max(left.length, right.length);
|
|
for (let index = 0; index < length; index += 1) {
|
|
if (left[index] === undefined) return -1;
|
|
if (right[index] === undefined) return 1;
|
|
if (left[index] === right[index]) continue;
|
|
const leftNumeric = /^\d+$/.test(left[index]);
|
|
const rightNumeric = /^\d+$/.test(right[index]);
|
|
if (leftNumeric && rightNumeric) return compareNumericIdentifiers(left[index], right[index]);
|
|
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
return left[index] < right[index] ? -1 : 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export function compareSemver(leftValue, rightValue) {
|
|
const left = parseSemver(leftValue);
|
|
const right = parseSemver(rightValue);
|
|
for (let index = 0; index < left.core.length; index += 1) {
|
|
const comparison = compareNumericIdentifiers(left.core[index], right.core[index]);
|
|
if (comparison !== 0) return comparison;
|
|
}
|
|
return comparePrerelease(left.prerelease, right.prerelease);
|
|
}
|
|
|
|
export function releaseChannelForVersion(value) {
|
|
return parseSemver(value).prerelease ? 'development' : 'stable';
|
|
}
|
|
|
|
export function isStableCoreVersion(value) {
|
|
try {
|
|
const parsed = parseSemver(value);
|
|
return parsed.prerelease === null && parsed.build === null;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function validateCanonicalUtcTimestamp(value) {
|
|
if (typeof value !== 'string' || !UTC_SECONDS.test(value)) {
|
|
throw new UpdateContractError('publication time must use YYYY-MM-DDTHH:mm:ssZ');
|
|
}
|
|
const timestamp = Date.parse(value);
|
|
if (!Number.isFinite(timestamp)
|
|
|| new Date(timestamp).toISOString().replace('.000Z', 'Z') !== value) {
|
|
throw new UpdateContractError('publication time is not a real UTC calendar instant');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function validateLocalRelease(value) {
|
|
if (!hasExactKeys(value, [
|
|
'schemaVersion', 'skillId', 'channel', 'version', 'source', 'updateManifestUrl',
|
|
])
|
|
|| value.schemaVersion !== 1
|
|
|| value.skillId !== SKILL_ID
|
|
|| !hasExactKeys(value.source, ['repository'])
|
|
|| value.source.repository !== EXPECTED_REPOSITORY
|
|
|| value.updateManifestUrl !== DEFAULT_MANIFEST_URL) {
|
|
throw new UpdateContractError('invalid local release identity');
|
|
}
|
|
const expectedChannel = releaseChannelForVersion(value.version);
|
|
if (value.channel !== expectedChannel) {
|
|
throw new UpdateContractError('local release channel does not match its version');
|
|
}
|
|
return {
|
|
schemaVersion: value.schemaVersion,
|
|
skillId: value.skillId,
|
|
channel: value.channel,
|
|
version: value.version,
|
|
source: { repository: value.source.repository },
|
|
updateManifestUrl: value.updateManifestUrl,
|
|
};
|
|
}
|
|
|
|
export function validateReleaseNotesUrl(value, version) {
|
|
if (!isStableCoreVersion(version)) {
|
|
throw new UpdateContractError('release notes require a stable core version');
|
|
}
|
|
const expected = `https://github.com/tt-a1i/archify/releases/tag/v${version}`;
|
|
if (value !== expected) {
|
|
throw new UpdateContractError('release notes URL is outside the exact trusted release path');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function validateStableUpdateManifest(value) {
|
|
if (!hasExactKeys(value, [
|
|
'schemaVersion', 'skillId', 'channel', 'version', 'publishedAt', 'source',
|
|
'artifact', 'summary', 'releaseNotes', 'severity',
|
|
])
|
|
|| value.schemaVersion !== 1
|
|
|| value.skillId !== SKILL_ID
|
|
|| value.channel !== 'stable'
|
|
|| !isStableCoreVersion(value.version)
|
|
|| !hasExactKeys(value.source, ['repository', 'ref', 'treeSha'])
|
|
|| value.source.repository !== EXPECTED_REPOSITORY
|
|
|| value.source.ref !== `v${value.version}`
|
|
|| !HEX_40.test(value.source.treeSha)
|
|
|| !hasExactKeys(value.artifact, ['sha256'])
|
|
|| !HEX_64.test(value.artifact.sha256)) {
|
|
throw new UpdateContractError('invalid immutable stable release identity');
|
|
}
|
|
validateCanonicalUtcTimestamp(value.publishedAt);
|
|
if (typeof value.summary !== 'string' || value.summary.length < 1 || value.summary.length > 160
|
|
|| CONTROL_OR_BIDI.test(value.summary)) {
|
|
throw new UpdateContractError('invalid release summary');
|
|
}
|
|
validateReleaseNotesUrl(value.releaseNotes, value.version);
|
|
if (!['normal', 'security'].includes(value.severity)) {
|
|
throw new UpdateContractError('invalid update severity');
|
|
}
|
|
return {
|
|
schemaVersion: value.schemaVersion,
|
|
skillId: value.skillId,
|
|
channel: value.channel,
|
|
version: value.version,
|
|
publishedAt: value.publishedAt,
|
|
source: {
|
|
repository: value.source.repository,
|
|
ref: value.source.ref,
|
|
treeSha: value.source.treeSha,
|
|
},
|
|
artifact: { sha256: value.artifact.sha256 },
|
|
summary: value.summary,
|
|
releaseNotes: value.releaseNotes,
|
|
severity: value.severity,
|
|
};
|
|
}
|