feat: introduce archify skill for generating architecture diagrams

- 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.
This commit is contained in:
shancheas
2026-08-31 11:31:29 +07:00
parent 050fafd731
commit 166e0d40ac
221 changed files with 191228 additions and 1 deletions
@@ -0,0 +1,71 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(__dirname, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-presentation-'));
const CASES = {
architecture: 'web-app.architecture.json',
workflow: 'agent-tool-call.workflow.json',
sequence: 'cache-miss-request.sequence.json',
dataflow: 'product-analytics.dataflow.json',
lifecycle: 'agent-run.lifecycle.json',
};
function render(mode, example) {
const output = path.join(tmp, `${mode}.html`);
execFileSync(process.execPath, [
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
path.join(skillRoot, 'examples', example),
output,
]);
return fs.readFileSync(output, 'utf8');
}
function svg(html) {
return html.match(/<svg\b[\s\S]*?<\/svg>/)?.[0] || '';
}
test('all typed renderers ship the same presentation stage contract', () => {
for (const [mode, example] of Object.entries(CASES)) {
const html = render(mode, example);
assert.match(html, /id="btn-present"[^>]+aria-label="Enter presentation stage"[^>]+aria-pressed="false"/, mode);
assert.match(html, /Archify\.presentation = \(function \(\)/, mode);
assert.match(html, /enter: function \(\) \{ return setActive\(true\); \}/, mode);
assert.match(html, /exit: function \(\) \{ return setActive\(false\); \}/, mode);
assert.match(html, /html\[data-present="true"\]:not\(\[data-embed="true"\]\) \.diagram-container/, mode);
assert.match(html, /height: 100dvh/, mode);
assert.match(html, /\.cards \{ display: none; \}/, mode);
assert.doesNotMatch(html.match(/<html[^>]*>/)?.[0] || '', /data-present=/, mode);
assert.doesNotMatch(svg(html), /data-present|btn-present|Presentation Stage/, mode);
}
});
test('presentation stage supports direct links and preserves view hashes', () => {
const html = render('architecture', CASES.architecture);
assert.match(html, /get\('present'\) === '1'/);
assert.match(html, /document\.documentElement\.setAttribute\('data-present', 'true'\)/);
assert.match(html, /url\.searchParams\.set\('present', '1'\)/);
assert.match(html, /url\.searchParams\.delete\('present'\)/);
assert.match(html, /url\.pathname \+ url\.search \+ url\.hash/);
assert.match(html, /Embed mode wins when both query parameters are present/);
});
test('presentation keyboard behavior exits in layers and remains accessible', () => {
const html = render('workflow', CASES.workflow);
assert.match(html, /e\.defaultPrevented\) return/);
assert.match(html, /e\.key === 'f' \|\| e\.key === 'F'/);
assert.match(html, /Archify\.presentation\.toggle\(\)/);
assert.match(html, /e\.key === 'Escape' && Archify\.focus\.active\(\)/);
assert.match(html, /e\.key === 'Escape' && Archify\.presentation\.active\(\)/);
assert.match(html, /btn\.setAttribute\('aria-pressed', next \? 'true' : 'false'\)/);
assert.match(html, /Exit presentation stage \(F or Escape\)/);
});
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));