- 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.
128 lines
4.5 KiB
JavaScript
128 lines
4.5 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const DIAGNOSTIC_MODE = process.env.ARCHIFY_DIAGNOSTIC_FORMAT === 'json';
|
|
const recorded = [];
|
|
const recordedMessages = new Set();
|
|
const boundaryKey = Symbol.for('archify.renderer-diagnostic-boundary');
|
|
let recordingSuppressionDepth = 0;
|
|
|
|
function plainObject(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
}
|
|
|
|
function normalizedDiagnostic(diagnostic) {
|
|
const message = String(diagnostic?.message || 'Archify could not classify this failure.').trim();
|
|
return {
|
|
code: String(diagnostic?.code || 'internal/unclassified'),
|
|
severity: diagnostic?.severity === 'warning' ? 'warning' : 'error',
|
|
message,
|
|
subject: plainObject(diagnostic?.subject),
|
|
evidence: plainObject(diagnostic?.evidence),
|
|
supportedFixes: Array.isArray(diagnostic?.supportedFixes)
|
|
? [...new Set(diagnostic.supportedFixes.map((fix) => String(fix).trim()).filter(Boolean))]
|
|
: [],
|
|
...(Array.isArray(diagnostic?.suppresses) ? {
|
|
suppresses: [...new Set(diagnostic.suppresses.map((code) => String(code).trim()).filter(Boolean))],
|
|
} : {}),
|
|
};
|
|
}
|
|
|
|
export function recordDiagnostic(diagnostic) {
|
|
if (!DIAGNOSTIC_MODE || recordingSuppressionDepth > 0) return;
|
|
const normalized = normalizedDiagnostic(diagnostic);
|
|
if (recordedMessages.has(normalized.message)) return;
|
|
recordedMessages.add(normalized.message);
|
|
recorded.push(normalized);
|
|
}
|
|
|
|
export function withDiagnosticRecordingSuppressed(callback) {
|
|
recordingSuppressionDepth += 1;
|
|
try {
|
|
return callback();
|
|
} finally {
|
|
recordingSuppressionDepth -= 1;
|
|
}
|
|
}
|
|
|
|
export function throwDiagnosticError(message, diagnostics) {
|
|
for (const diagnostic of diagnostics || []) recordDiagnostic(diagnostic);
|
|
const error = new Error(message);
|
|
error.archifyDiagnostics = (diagnostics || []).map(normalizedDiagnostic);
|
|
throw error;
|
|
}
|
|
|
|
export function throwDiagnosticProblems(prefix, problems, { code = 'layout/constraint', subject = {} } = {}) {
|
|
const messages = (problems || []).map((problem) => String(problem));
|
|
const diagnostics = messages.map((message) => normalizedDiagnostic({
|
|
code,
|
|
severity: 'error',
|
|
message,
|
|
subject,
|
|
evidence: {},
|
|
supportedFixes: [],
|
|
}));
|
|
throwDiagnosticError(`${prefix}:\n- ${messages.join('\n- ')}`, diagnostics);
|
|
}
|
|
|
|
function fallbackDiagnostic(error) {
|
|
const input = process.argv[2] ? path.resolve(process.argv[2]) : undefined;
|
|
if (error instanceof SyntaxError) {
|
|
return normalizedDiagnostic({
|
|
code: 'input/json-parse',
|
|
severity: 'error',
|
|
message: `Input JSON could not be parsed: ${error.message}`,
|
|
subject: { input },
|
|
evidence: { reason: error.message },
|
|
supportedFixes: ['repair the JSON syntax and run validation again'],
|
|
});
|
|
}
|
|
if (error?.code === 'ENOENT' || error?.code === 'EACCES' || error?.code === 'EISDIR') {
|
|
return normalizedDiagnostic({
|
|
code: 'input/read',
|
|
severity: 'error',
|
|
message: `Input could not be read: ${error.message}`,
|
|
subject: { input },
|
|
evidence: { systemCode: error.code, reason: error.message },
|
|
supportedFixes: ['provide one readable JSON input file'],
|
|
});
|
|
}
|
|
return normalizedDiagnostic({
|
|
code: 'internal/unclassified',
|
|
severity: 'error',
|
|
message: error?.message || 'Renderer failed without a diagnostic.',
|
|
subject: { input },
|
|
evidence: { errorName: error?.name || 'Error' },
|
|
supportedFixes: [],
|
|
});
|
|
}
|
|
function rendererFailure(error) {
|
|
const attached = Array.isArray(error?.archifyDiagnostics)
|
|
? error.archifyDiagnostics.map(normalizedDiagnostic)
|
|
: [];
|
|
const diagnostics = recorded.length ? recorded : (attached.length ? attached : [fallbackDiagnostic(error)]);
|
|
return {
|
|
schemaVersion: 1,
|
|
ok: false,
|
|
source: 'renderer',
|
|
error: error?.message || 'Renderer failed without a diagnostic.',
|
|
diagnostics,
|
|
};
|
|
}
|
|
|
|
export function installRendererDiagnosticBoundary() {
|
|
if (!DIAGNOSTIC_MODE || globalThis[boundaryKey]) return;
|
|
globalThis[boundaryKey] = true;
|
|
process.on('uncaughtException', (error) => {
|
|
const payload = `${JSON.stringify(rendererFailure(error))}\n`;
|
|
try {
|
|
fs.writeSync(process.stderr.fd, payload);
|
|
} catch {
|
|
// The renderer is already failing. Avoid replacing its real error with a
|
|
// secondary stream failure; the parent CLI still has the exit status.
|
|
}
|
|
process.exit(1);
|
|
});
|
|
}
|