Archify Diagram Viewer 0.1.0

VS Code extension that previews Archify diagrams from their JSON sources
(live, as you type) and opens rendered Archify HTML in a viewer tab.
Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime.
Adds validation diagnostics, JSON schema help, source-link navigation,
export saving, render-to-file and open-in-browser commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 12:40:55 +03:00
co-authored by Claude Opus 5.5
commit d56e784e97
119 changed files with 66708 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { DiagramSource, DiagramType, hasSourceEvidence, stripSourceEvidence } from './detect';
export interface RuntimeOptions {
/** Absolute path to vendor/archify/bin/archify.mjs. */
cliPath: string;
/** Node (or Electron) executable. */
nodePath: string;
/** Set ELECTRON_RUN_AS_NODE so VS Code's Electron binary behaves as Node. */
runAsNode: boolean;
/** Scratch directory for input/output files. */
workDir: string;
quality?: string;
repoRoot?: string;
timeoutMs?: number;
log?: (line: string) => void;
}
export interface ArchifyDiagnostic {
code?: string;
severity?: 'error' | 'warning' | 'info';
message: string;
subject?: { path?: string; identity?: string; [key: string]: unknown };
supportedFixes?: string[];
}
export interface ValidateResult {
ok: boolean;
diagnostics: ArchifyDiagnostic[];
/** Set when the CLI did not produce parseable JSON. */
crash?: string;
}
export interface RenderResult {
ok: boolean;
html?: string;
error?: string;
/** Set when the diagram rendered without its source evidence. */
evidenceNotice?: string;
}
interface RunResult {
code: number | null;
stdout: string;
stderr: string;
}
const EVIDENCE_ERROR_RE = /repository-evidence\/|source evidence|--repo-root|Evidence (root|repository|revision)/i;
function run(options: RuntimeOptions, args: string[], env: Record<string, string> = {}): Promise<RunResult> {
return new Promise((resolve, reject) => {
options.log?.(`archify ${args.join(' ')}`);
const child = spawn(options.nodePath, [options.cliPath, ...args], {
cwd: options.workDir,
env: {
...process.env,
...(options.runAsNode ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
...env,
},
windowsHide: true,
});
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8').on('data', (chunk: string) => (stdout += chunk));
child.stderr.setEncoding('utf8').on('data', (chunk: string) => (stderr += chunk));
const timer = setTimeout(() => child.kill(), options.timeoutMs ?? 60_000);
child.on('error', (error) => {
clearTimeout(timer);
reject(error);
});
child.on('close', (code) => {
clearTimeout(timer);
if (stderr.trim()) options.log?.(stderr.trim());
resolve({ code, stdout, stderr });
});
});
}
function commonArgs(options: RuntimeOptions, withRepoRoot: boolean): string[] {
const args: string[] = [];
if (options.quality) args.push('--quality', options.quality);
if (withRepoRoot && options.repoRoot) args.push('--repo-root', options.repoRoot);
return args;
}
async function withScratch<T>(options: RuntimeOptions, body: (dir: string) => Promise<T>): Promise<T> {
await fs.mkdir(options.workDir, { recursive: true });
const dir = await fs.mkdtemp(path.join(options.workDir, 'run-'));
try {
return await body(dir);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
}
export async function validate(options: RuntimeOptions, type: DiagramType, text: string): Promise<ValidateResult> {
return withScratch(options, async (dir) => {
const input = path.join(dir, `diagram.${type}.json`);
await fs.writeFile(input, text, 'utf8');
const result = await run(options, ['validate', type, input, '--json', ...commonArgs(options, true)]);
try {
const report = JSON.parse(result.stdout);
const diagnostics: ArchifyDiagnostic[] = Array.isArray(report.diagnostics) ? report.diagnostics : [];
if (!report.ok && diagnostics.length === 0 && report.error) {
diagnostics.push({ severity: 'error', message: String(report.error) });
}
return { ok: Boolean(report.ok), diagnostics };
} catch {
const crash = (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim();
return { ok: false, diagnostics: [], crash };
}
});
}
async function renderOnce(
options: RuntimeOptions,
type: DiagramType,
text: string,
withRepoRoot: boolean,
): Promise<RenderResult> {
return withScratch(options, async (dir) => {
const input = path.join(dir, `diagram.${type}.json`);
const output = path.join(dir, 'diagram.html');
await fs.writeFile(input, text, 'utf8');
const result = await run(options, ['render', type, input, output, ...commonArgs(options, withRepoRoot)]);
if (result.code === 0) {
try {
return { ok: true, html: await fs.readFile(output, 'utf8') };
} catch {
// fall through to the error below
}
}
const error = (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim();
return { ok: false, error };
});
}
/**
* Renders a diagram to HTML. With `evidence: 'fallback'`, a diagram whose
* source evidence cannot be verified is rendered again without it, and the
* reason is returned as `evidenceNotice`.
*/
export async function renderDiagram(
options: RuntimeOptions,
source: DiagramSource & { text: string },
evidence: 'fallback' | 'strict',
): Promise<RenderResult> {
const withEvidence = hasSourceEvidence(source.type, source.diagram);
if (withEvidence && evidence === 'fallback' && !options.repoRoot) {
const stripped = JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2);
const result = await renderOnce(options, source.type, stripped, false);
return {
...result,
evidenceNotice: 'Source links are hidden: the diagram is not inside a Git checkout, so its source evidence cannot be verified. Set "archify.repoRoot" to the matching checkout.',
};
}
const first = await renderOnce(options, source.type, source.text, withEvidence);
if (first.ok || !withEvidence || evidence !== 'fallback' || !isEvidenceError(first.error)) {
return first;
}
const stripped = JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2);
const second = await renderOnce(options, source.type, stripped, false);
if (!second.ok) return first;
const reason = (first.error ?? '').split('\n').find((line) => line.trim()) ?? 'verification failed';
return { ...second, evidenceNotice: `Source links are hidden: ${reason.replace(/^\[[^\]]+\]\s*/, '')}` };
}
/**
* Renders to a file the user chose. Source evidence is verified strictly
* unless `stripEvidence` is set, in which case it is removed first.
*/
export async function renderToFile(
options: RuntimeOptions,
source: DiagramSource & { text: string },
outputPath: string,
stripEvidence = false,
): Promise<RenderResult> {
const text = stripEvidence
? JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2)
: source.text;
const withEvidence = !stripEvidence && hasSourceEvidence(source.type, source.diagram);
return withScratch(options, async (dir) => {
const input = path.join(dir, `diagram.${source.type}.json`);
await fs.writeFile(input, text, 'utf8');
const result = await run(options, ['render', source.type, input, outputPath, ...commonArgs(options, withEvidence)]);
if (result.code === 0) return { ok: true };
return { ok: false, error: (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim() };
});
}
export function isEvidenceError(error: string | undefined): boolean {
return EVIDENCE_ERROR_RE.test(error ?? '');
}