@stacksjs/defaults 0.74.40 → 0.74.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/skills/stacks-cms/SKILL.md +4 -4
- package/ai/skills/stacks-composables/SKILL.md +1 -1
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-dashboard/SKILL.md +1 -1
- package/ai/skills/stacks-technical-diagrams/LICENSE +1 -1
- package/ai/skills/stacks-technical-diagrams/SKILL.md +257 -229
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +69 -0
- package/ai/skills/stacks-technical-diagrams/assets/JetBrainsMono-OFL.txt +93 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +14588 -929
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +10 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +86 -0
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +654 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +1898 -89
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +829 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +31 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/catalog.json +131 -0
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +1221 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +18 -22
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +58 -52
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +61 -0
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +47 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +30 -23
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +49 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +57 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +22 -16
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +71 -0
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +62 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +16 -11
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +279 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +391 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +243 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +65 -0
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +120 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +45 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +780 -73
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +25 -3
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +240 -52
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +31 -7
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +227 -50
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +36 -6
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +270 -63
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +563 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/bun-runtime.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +186 -10
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +127 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/engineering-profiles.mjs +157 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +3 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +1195 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +595 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +217 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +340 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +238 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +58 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +49 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +163 -19
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +51 -5
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -17
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +24 -470
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +154 -11
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +61 -4
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +72 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +40 -18
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +43 -18
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +41 -4
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +97 -1
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +551 -12
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +3 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/CookieConsent.stx +146 -0
- package/resources/components/CookieConsent.test.ts +79 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { openLoopbackUrl } from './open-artifact.mjs';
|
|
9
|
+
import { resolveOutputPath } from '../renderers/shared/output-path.mjs';
|
|
10
|
+
import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
|
|
11
|
+
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const cliPath = path.join(here, 'archify.mjs');
|
|
14
|
+
const loopbackHost = '127.0.0.1';
|
|
15
|
+
const defaultDebounceMs = 400;
|
|
16
|
+
const defaultPollMs = 800;
|
|
17
|
+
const defaultStopGraceMs = 3000;
|
|
18
|
+
const defaultStopKillMs = 750;
|
|
19
|
+
const diagramTypes = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']);
|
|
20
|
+
|
|
21
|
+
function sha256(value) {
|
|
22
|
+
return createHash('sha256').update(value).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sourceDigest(inputPath) {
|
|
26
|
+
try {
|
|
27
|
+
const bytes = fs.readFileSync(inputPath);
|
|
28
|
+
return { hash: sha256(bytes), bytes, missing: false };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return { hash: `unreadable:${error.code || 'unknown'}`, bytes: null, missing: true };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function initialAuthoredOutput(inputPath) {
|
|
35
|
+
try {
|
|
36
|
+
const source = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
37
|
+
if (typeof source?.meta?.output === 'string' && source.meta.output) {
|
|
38
|
+
return source.meta.output;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// An invalid initial source still gets a status shell. Its output target is
|
|
42
|
+
// fixed to the same fallback that `deliver` would use after repair.
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function previewPage() {
|
|
48
|
+
return `<!doctype html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="utf-8">
|
|
52
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
53
|
+
<title>Technical Diagrams Live Preview</title>
|
|
54
|
+
<style>
|
|
55
|
+
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
|
|
56
|
+
* { box-sizing: border-box; }
|
|
57
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #0b111b; }
|
|
58
|
+
body { display: grid; grid-template-rows: auto minmax(0, 1fr); color: #e8edf5; }
|
|
59
|
+
header { position: relative; z-index: 2; display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 7px 12px; border-bottom: 1px solid #253248; background: rgba(11, 17, 27, .96); box-shadow: 0 8px 22px rgba(0,0,0,.18); }
|
|
60
|
+
.brand { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #9cadc6; }
|
|
61
|
+
#status { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; min-height: 30px; padding: 5px 10px; border: 1px solid #33435d; border-radius: 999px; background: #111b2a; font-size: 12px; white-space: nowrap; }
|
|
62
|
+
#status::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #6f819d; }
|
|
63
|
+
body[data-state="checking"] #status::before { background: #f3b44b; box-shadow: 0 0 0 4px rgba(243,180,75,.12); }
|
|
64
|
+
body[data-state="verified"] #status::before { background: #45d6a8; box-shadow: 0 0 0 4px rgba(69,214,168,.12); }
|
|
65
|
+
body[data-state="needs-fix"] #status::before { background: #ff6f78; box-shadow: 0 0 0 4px rgba(255,111,120,.12); }
|
|
66
|
+
details { max-width: min(62vw, 760px); }
|
|
67
|
+
summary { cursor: pointer; color: #ffbdc2; font-size: 12px; }
|
|
68
|
+
.diagnostic { position: absolute; top: 38px; right: 12px; width: min(760px, calc(100vw - 24px)); max-height: min(44vh, 360px); overflow: auto; padding: 14px; border: 1px solid #6a3440; border-radius: 10px; background: #17131b; box-shadow: 0 14px 48px rgba(0,0,0,.42); }
|
|
69
|
+
pre { margin: 0 0 10px; white-space: pre-wrap; overflow-wrap: anywhere; font: 11px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; color: #f2dfe2; }
|
|
70
|
+
button { min-height: 32px; padding: 5px 10px; border: 1px solid #4a5d79; border-radius: 7px; background: #1a273a; color: #eef4ff; cursor: pointer; }
|
|
71
|
+
main { position: relative; min-height: 0; }
|
|
72
|
+
iframe { display: none; width: 100%; height: 100%; border: 0; background: #fff; }
|
|
73
|
+
body[data-has-artifact="true"] iframe { display: block; }
|
|
74
|
+
#empty { position: absolute; inset: 0; display: grid; place-items: center; padding: 32px; color: #91a2bc; text-align: center; background: radial-gradient(circle at 50% 38%, #15233a 0, #0b111b 55%); }
|
|
75
|
+
body[data-has-artifact="true"] #empty { display: none; }
|
|
76
|
+
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body data-state="checking" data-has-artifact="false">
|
|
80
|
+
<header>
|
|
81
|
+
<span class="brand">Live Preview</span>
|
|
82
|
+
<details id="failure" hidden>
|
|
83
|
+
<summary role="button" aria-controls="diagnostic-panel">View diagnostic</summary>
|
|
84
|
+
<div class="diagnostic" id="diagnostic-panel"><pre id="diagnostic"></pre><button id="copy" type="button">Copy diagnostic</button></div>
|
|
85
|
+
</details>
|
|
86
|
+
<span id="status" role="status" aria-live="polite">Checking · generation 1</span>
|
|
87
|
+
</header>
|
|
88
|
+
<main>
|
|
89
|
+
<div id="empty">Waiting for the first verified diagram. Invalid input will stay here with an exact diagnostic.</div>
|
|
90
|
+
<iframe id="artifact" title="Verified diagram"></iframe>
|
|
91
|
+
</main>
|
|
92
|
+
<script>
|
|
93
|
+
(function () {
|
|
94
|
+
'use strict';
|
|
95
|
+
var body = document.body;
|
|
96
|
+
var status = document.getElementById('status');
|
|
97
|
+
var failure = document.getElementById('failure');
|
|
98
|
+
var diagnostic = document.getElementById('diagnostic');
|
|
99
|
+
var artifact = document.getElementById('artifact');
|
|
100
|
+
var lastRevision = 0;
|
|
101
|
+
|
|
102
|
+
function render(state) {
|
|
103
|
+
body.dataset.state = state.status;
|
|
104
|
+
if (state.status === 'verified') {
|
|
105
|
+
status.textContent = 'Verified · rev ' + state.revision;
|
|
106
|
+
failure.hidden = true;
|
|
107
|
+
failure.open = false;
|
|
108
|
+
if (state.revision !== lastRevision) {
|
|
109
|
+
lastRevision = state.revision;
|
|
110
|
+
artifact.src = '/artifact.html?revision=' + encodeURIComponent(state.revision) + '&sha=' + encodeURIComponent(state.lastVerified.sha256.slice(0, 12));
|
|
111
|
+
body.dataset.hasArtifact = 'true';
|
|
112
|
+
}
|
|
113
|
+
} else if (state.status === 'needs-fix') {
|
|
114
|
+
status.textContent = 'Needs fix · ' + (state.revision ? 'showing rev ' + state.revision : 'no verified revision');
|
|
115
|
+
diagnostic.textContent = 'Generation ' + state.generation + ' · ' + state.failure.stage + '\\n\\n' + state.failure.message;
|
|
116
|
+
failure.hidden = false;
|
|
117
|
+
} else {
|
|
118
|
+
status.textContent = 'Checking · generation ' + state.generation;
|
|
119
|
+
failure.hidden = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
document.getElementById('copy').addEventListener('click', function () {
|
|
124
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
125
|
+
navigator.clipboard.writeText(diagnostic.textContent).catch(function () {});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
var events = new EventSource('/events');
|
|
130
|
+
events.addEventListener('state', function (event) {
|
|
131
|
+
try { render(JSON.parse(event.data)); } catch (_) {}
|
|
132
|
+
});
|
|
133
|
+
}());
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function compactMessage(value) {
|
|
140
|
+
let text = String(value || 'Preview build failed without a diagnostic.').trim();
|
|
141
|
+
const lines = text.split(/\r?\n/);
|
|
142
|
+
const errorLine = lines.findIndex((line) => /^Error:\s/.test(line));
|
|
143
|
+
if (errorLine > 0) text = lines.slice(errorLine).join('\n');
|
|
144
|
+
const relevant = text.split(/\r?\n/);
|
|
145
|
+
const stackLine = relevant.findIndex((line, index) => index > 0 && /^\s*at\s/.test(line));
|
|
146
|
+
if (stackLine > 0) text = relevant.slice(0, stackLine).join('\n');
|
|
147
|
+
return text.length > 6000 ? `${text.slice(0, 6000)}\n… diagnostic truncated` : text;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function redactDiagnostic(value, paths) {
|
|
151
|
+
let text = compactMessage(value);
|
|
152
|
+
for (const [absolutePath, replacement] of paths) {
|
|
153
|
+
if (!absolutePath) continue;
|
|
154
|
+
text = text.split(absolutePath).join(replacement);
|
|
155
|
+
}
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function safeJson(value) {
|
|
160
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function responseHeaders(contentType) {
|
|
164
|
+
return {
|
|
165
|
+
'Cache-Control': 'no-store',
|
|
166
|
+
'Content-Type': contentType,
|
|
167
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
168
|
+
'Referrer-Policy': 'no-referrer',
|
|
169
|
+
'X-Content-Type-Options': 'nosniff',
|
|
170
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseReceipt(stdout) {
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(stdout);
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function startPreview(options) {
|
|
183
|
+
const type = options.type;
|
|
184
|
+
if (!diagramTypes.has(type)) throw new Error(`Unknown diagram type "${type}".`);
|
|
185
|
+
if (options.quality && !['standard', 'showcase'].includes(options.quality)) {
|
|
186
|
+
throw new Error(`Unknown quality profile "${options.quality}".`);
|
|
187
|
+
}
|
|
188
|
+
const inputPath = path.resolve(options.input);
|
|
189
|
+
const outputRequest = {
|
|
190
|
+
requestedOutput: options.output,
|
|
191
|
+
authoredOutput: initialAuthoredOutput(inputPath),
|
|
192
|
+
defaultOutput: `${type}.html`,
|
|
193
|
+
inputPaths: [inputPath],
|
|
194
|
+
inputDescription: 'its JSON input',
|
|
195
|
+
cwd: options.cwd || process.cwd(),
|
|
196
|
+
};
|
|
197
|
+
const { outputPath } = resolveOutputPath(outputRequest);
|
|
198
|
+
const outputDirectory = path.dirname(outputPath);
|
|
199
|
+
const debounceMs = Number.isFinite(options.debounceMs) ? options.debounceMs : defaultDebounceMs;
|
|
200
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : defaultPollMs;
|
|
201
|
+
const stopGraceMs = Number.isFinite(options.stopGraceMs) ? Math.max(0, options.stopGraceMs) : defaultStopGraceMs;
|
|
202
|
+
const stopKillMs = Number.isFinite(options.stopKillMs) ? Math.max(0, options.stopKillMs) : defaultStopKillMs;
|
|
203
|
+
const shouldOpen = options.open !== false;
|
|
204
|
+
|
|
205
|
+
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
206
|
+
const stagingDirectory = fs.mkdtempSync(path.join(outputDirectory, '.archify-preview-'));
|
|
207
|
+
|
|
208
|
+
let port = 0;
|
|
209
|
+
let watcher;
|
|
210
|
+
let debounceTimer;
|
|
211
|
+
let pollTimer;
|
|
212
|
+
let stopGraceTimer;
|
|
213
|
+
let stopKillTimer;
|
|
214
|
+
let child;
|
|
215
|
+
let stopping = false;
|
|
216
|
+
let stopped = false;
|
|
217
|
+
let serverClosing = false;
|
|
218
|
+
let serverClosed = false;
|
|
219
|
+
let queuedHash = null;
|
|
220
|
+
let activeHash = null;
|
|
221
|
+
let lastGoodSourceHash = null;
|
|
222
|
+
let sourceEpoch = 0;
|
|
223
|
+
let activeEpoch = 0;
|
|
224
|
+
let pendingBuild = false;
|
|
225
|
+
let artifactBuffer = null;
|
|
226
|
+
const clients = new Set();
|
|
227
|
+
const state = {
|
|
228
|
+
schemaVersion: 1,
|
|
229
|
+
status: 'checking',
|
|
230
|
+
generation: 0,
|
|
231
|
+
revision: 0,
|
|
232
|
+
lastVerified: null,
|
|
233
|
+
failure: null,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
let resolveClosed;
|
|
237
|
+
const closed = new Promise((resolve) => { resolveClosed = resolve; });
|
|
238
|
+
|
|
239
|
+
function publicState() {
|
|
240
|
+
return JSON.parse(JSON.stringify(state));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sendState(res) {
|
|
244
|
+
res.write(`event: state\ndata: ${safeJson(publicState())}\n\n`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function broadcast() {
|
|
248
|
+
for (const res of clients) sendState(res);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const page = Buffer.from(previewPage());
|
|
252
|
+
const server = http.createServer((req, res) => {
|
|
253
|
+
const expectedHost = `${loopbackHost}:${port}`;
|
|
254
|
+
if (req.headers.host !== expectedHost) {
|
|
255
|
+
res.writeHead(403, responseHeaders('text/plain; charset=utf-8'));
|
|
256
|
+
res.end('Forbidden host');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
260
|
+
res.writeHead(405, { ...responseHeaders('text/plain; charset=utf-8'), Allow: 'GET, HEAD' });
|
|
261
|
+
res.end('Method not allowed');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let url;
|
|
266
|
+
try {
|
|
267
|
+
url = new URL(req.url, `http://${expectedHost}`);
|
|
268
|
+
} catch {
|
|
269
|
+
res.writeHead(400, responseHeaders('text/plain; charset=utf-8'));
|
|
270
|
+
res.end('Bad request');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (url.pathname === '/') {
|
|
275
|
+
res.writeHead(200, {
|
|
276
|
+
...responseHeaders('text/html; charset=utf-8'),
|
|
277
|
+
'Content-Security-Policy': "default-src 'none'; frame-src 'self'; connect-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
|
|
278
|
+
'Content-Length': page.byteLength,
|
|
279
|
+
});
|
|
280
|
+
if (req.method === 'HEAD') res.end();
|
|
281
|
+
else res.end(page);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (url.pathname === '/state') {
|
|
285
|
+
const body = Buffer.from(`${safeJson(publicState())}\n`);
|
|
286
|
+
res.writeHead(200, { ...responseHeaders('application/json; charset=utf-8'), 'Content-Length': body.byteLength });
|
|
287
|
+
if (req.method === 'HEAD') res.end();
|
|
288
|
+
else res.end(body);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (url.pathname === '/artifact.html') {
|
|
292
|
+
if (!artifactBuffer) {
|
|
293
|
+
res.writeHead(404, responseHeaders('text/plain; charset=utf-8'));
|
|
294
|
+
res.end('No verified artifact yet');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
res.writeHead(200, { ...responseHeaders('text/html; charset=utf-8'), 'Content-Length': artifactBuffer.byteLength });
|
|
298
|
+
if (req.method === 'HEAD') res.end();
|
|
299
|
+
else res.end(artifactBuffer);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (url.pathname === '/events' && req.method === 'GET') {
|
|
303
|
+
res.writeHead(200, {
|
|
304
|
+
...responseHeaders('text/event-stream; charset=utf-8'),
|
|
305
|
+
Connection: 'keep-alive',
|
|
306
|
+
});
|
|
307
|
+
res.write('retry: 1000\n\n');
|
|
308
|
+
clients.add(res);
|
|
309
|
+
sendState(res);
|
|
310
|
+
req.on('close', () => clients.delete(res));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
res.writeHead(404, responseHeaders('text/plain; charset=utf-8'));
|
|
315
|
+
res.end('Not found');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
await new Promise((resolve, reject) => {
|
|
320
|
+
server.once('error', reject);
|
|
321
|
+
server.listen(0, loopbackHost, () => {
|
|
322
|
+
server.off('error', reject);
|
|
323
|
+
port = server.address().port;
|
|
324
|
+
resolve();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
try { server.close(); } catch {}
|
|
329
|
+
fs.rmSync(stagingDirectory, { recursive: true, force: true });
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const url = `http://${loopbackHost}:${port}/`;
|
|
334
|
+
|
|
335
|
+
function finishStop() {
|
|
336
|
+
if (stopped || child || !serverClosed) return;
|
|
337
|
+
stopped = true;
|
|
338
|
+
clearTimeout(debounceTimer);
|
|
339
|
+
clearInterval(pollTimer);
|
|
340
|
+
clearTimeout(stopGraceTimer);
|
|
341
|
+
clearTimeout(stopKillTimer);
|
|
342
|
+
try {
|
|
343
|
+
fs.rmSync(stagingDirectory, { recursive: true, force: true });
|
|
344
|
+
} finally {
|
|
345
|
+
resolveClosed();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function signalActiveChild(signal) {
|
|
350
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
351
|
+
try {
|
|
352
|
+
if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
|
|
353
|
+
else child.kill(signal);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error.code === 'ESRCH') return;
|
|
356
|
+
try { child.kill(signal); } catch {}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function closeServer() {
|
|
361
|
+
if (serverClosing) return;
|
|
362
|
+
serverClosing = true;
|
|
363
|
+
for (const res of clients) res.end();
|
|
364
|
+
clients.clear();
|
|
365
|
+
server.close(() => {
|
|
366
|
+
serverClosed = true;
|
|
367
|
+
finishStop();
|
|
368
|
+
});
|
|
369
|
+
server.closeIdleConnections?.();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function startBoundedChildDrain() {
|
|
373
|
+
if (!child || stopGraceTimer || stopKillTimer) return;
|
|
374
|
+
stopGraceTimer = setTimeout(() => {
|
|
375
|
+
stopGraceTimer = undefined;
|
|
376
|
+
if (!child) return finishStop();
|
|
377
|
+
signalActiveChild('SIGTERM');
|
|
378
|
+
stopKillTimer = setTimeout(() => {
|
|
379
|
+
stopKillTimer = undefined;
|
|
380
|
+
signalActiveChild('SIGKILL');
|
|
381
|
+
}, stopKillMs);
|
|
382
|
+
}, stopGraceMs);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function stop({ force = false } = {}) {
|
|
386
|
+
if (!stopping) {
|
|
387
|
+
stopping = true;
|
|
388
|
+
clearTimeout(debounceTimer);
|
|
389
|
+
clearInterval(pollTimer);
|
|
390
|
+
watcher?.close();
|
|
391
|
+
closeServer();
|
|
392
|
+
}
|
|
393
|
+
if (child && force) {
|
|
394
|
+
clearTimeout(stopGraceTimer);
|
|
395
|
+
clearTimeout(stopKillTimer);
|
|
396
|
+
stopGraceTimer = undefined;
|
|
397
|
+
stopKillTimer = undefined;
|
|
398
|
+
signalActiveChild('SIGKILL');
|
|
399
|
+
} else if (child) {
|
|
400
|
+
startBoundedChildDrain();
|
|
401
|
+
} else {
|
|
402
|
+
finishStop();
|
|
403
|
+
}
|
|
404
|
+
return closed;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function publishFailure(receipt, stdout, stderr, candidatePath, snapshotPath) {
|
|
408
|
+
const repairDetails = receipt?.diagnostics
|
|
409
|
+
?.slice(0, 12)
|
|
410
|
+
.map((entry) => {
|
|
411
|
+
const fix = entry.supportedFixes?.length ? `\nFix: ${entry.supportedFixes.join('; ')}` : '';
|
|
412
|
+
return `[${entry.code}] ${entry.message}${fix}`;
|
|
413
|
+
}) || [];
|
|
414
|
+
const checkerDetails = receipt?.checker?.checks
|
|
415
|
+
?.filter((check) => !check.ok)
|
|
416
|
+
.flatMap((check) => check.details || [])
|
|
417
|
+
.filter(Boolean)
|
|
418
|
+
.slice(0, 12) || [];
|
|
419
|
+
const diagnostic = [
|
|
420
|
+
receipt?.error,
|
|
421
|
+
...(repairDetails.length ? repairDetails : checkerDetails),
|
|
422
|
+
].filter(Boolean).join('\n') || stderr || stdout;
|
|
423
|
+
state.status = 'needs-fix';
|
|
424
|
+
state.failure = {
|
|
425
|
+
stage: receipt?.stage || 'render',
|
|
426
|
+
message: redactDiagnostic(
|
|
427
|
+
diagnostic,
|
|
428
|
+
[
|
|
429
|
+
[inputPath, '<input.json>'],
|
|
430
|
+
[outputPath, '<output.html>'],
|
|
431
|
+
[snapshotPath, '<input.json>'],
|
|
432
|
+
[candidatePath, '<candidate.html>'],
|
|
433
|
+
[stagingDirectory, '<preview-staging>'],
|
|
434
|
+
[path.resolve(here, '..'), '<archify-skill>'],
|
|
435
|
+
[path.resolve(options.cwd || process.cwd()), '<working-directory>'],
|
|
436
|
+
...(options.repoRoot ? [[path.resolve(options.repoRoot), '<repo-root>']] : []),
|
|
437
|
+
],
|
|
438
|
+
),
|
|
439
|
+
};
|
|
440
|
+
broadcast();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function commitCandidate(candidatePath, receipt, generationHash) {
|
|
444
|
+
let candidate;
|
|
445
|
+
try {
|
|
446
|
+
candidate = fs.readFileSync(candidatePath);
|
|
447
|
+
const digest = sha256(candidate);
|
|
448
|
+
if (digest !== receipt?.artifact?.sha256) {
|
|
449
|
+
throw new Error('Verified candidate bytes do not match the delivery receipt.');
|
|
450
|
+
}
|
|
451
|
+
resolveOutputPath(outputRequest);
|
|
452
|
+
const sameArtifact = state.lastVerified?.sha256 === digest;
|
|
453
|
+
let outputMatches = false;
|
|
454
|
+
if (sameArtifact) {
|
|
455
|
+
try { outputMatches = sha256(fs.readFileSync(outputPath)) === digest; } catch {}
|
|
456
|
+
}
|
|
457
|
+
const currentSource = sourceDigest(inputPath);
|
|
458
|
+
if (currentSource.hash !== generationHash) {
|
|
459
|
+
return { committed: false, supersededBy: currentSource };
|
|
460
|
+
}
|
|
461
|
+
if (!sameArtifact || !outputMatches) fs.renameSync(candidatePath, outputPath);
|
|
462
|
+
artifactBuffer = candidate;
|
|
463
|
+
lastGoodSourceHash = generationHash;
|
|
464
|
+
state.status = 'verified';
|
|
465
|
+
if (!sameArtifact) {
|
|
466
|
+
state.revision += 1;
|
|
467
|
+
state.lastVerified = {
|
|
468
|
+
sha256: digest,
|
|
469
|
+
bytes: candidate.byteLength,
|
|
470
|
+
checksPassed: receipt.validation.checksPassed,
|
|
471
|
+
checkCount: receipt.validation.checkCount,
|
|
472
|
+
compositionProfile: receipt.validation.compositionProfile,
|
|
473
|
+
compositionStatus: receipt.validation.compositionStatus,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
state.failure = null;
|
|
477
|
+
broadcast();
|
|
478
|
+
return { committed: true, supersededBy: null };
|
|
479
|
+
} catch (error) {
|
|
480
|
+
publishFailure({ stage: 'commit', error: `Could not publish the verified preview: ${error.message}` }, '', '', candidatePath);
|
|
481
|
+
return { committed: false, supersededBy: null };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function beginBuild(digest, epoch) {
|
|
486
|
+
if (stopping || child) return;
|
|
487
|
+
activeHash = digest.hash;
|
|
488
|
+
activeEpoch = epoch;
|
|
489
|
+
state.generation += 1;
|
|
490
|
+
state.status = 'checking';
|
|
491
|
+
state.failure = null;
|
|
492
|
+
broadcast();
|
|
493
|
+
|
|
494
|
+
const candidatePath = path.join(stagingDirectory, `generation-${state.generation}.html`);
|
|
495
|
+
const snapshotPath = path.join(stagingDirectory, `generation-${state.generation}.json`);
|
|
496
|
+
if (digest.bytes !== null) {
|
|
497
|
+
try {
|
|
498
|
+
fs.writeFileSync(snapshotPath, digest.bytes, { flag: 'wx', mode: 0o600 });
|
|
499
|
+
} catch (error) {
|
|
500
|
+
publishFailure(
|
|
501
|
+
{ stage: 'prepare', error: `Could not snapshot the observed input: ${error.message}` },
|
|
502
|
+
'',
|
|
503
|
+
'',
|
|
504
|
+
candidatePath,
|
|
505
|
+
snapshotPath,
|
|
506
|
+
);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const args = [options.deliveryCli || cliPath, 'deliver', type, snapshotPath, candidatePath, '--json'];
|
|
511
|
+
if (options.quality) args.push('--quality', options.quality);
|
|
512
|
+
if (options.repoRoot) args.push('--repo-root', path.resolve(options.repoRoot));
|
|
513
|
+
let stdout = '';
|
|
514
|
+
let stderr = '';
|
|
515
|
+
child = spawn(process.execPath, runtimeArgs(args), {
|
|
516
|
+
cwd: options.cwd || process.cwd(),
|
|
517
|
+
env: process.env,
|
|
518
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
519
|
+
detached: process.platform !== 'win32',
|
|
520
|
+
});
|
|
521
|
+
child.stdout.setEncoding('utf8');
|
|
522
|
+
child.stderr.setEncoding('utf8');
|
|
523
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
524
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
525
|
+
child.on('error', (error) => { stderr += error.message; });
|
|
526
|
+
child.on('close', (code) => {
|
|
527
|
+
const receipt = parseReceipt(stdout);
|
|
528
|
+
const generationEpoch = activeEpoch;
|
|
529
|
+
const generationHash = activeHash;
|
|
530
|
+
const stale = generationEpoch !== sourceEpoch;
|
|
531
|
+
let supersededBy = null;
|
|
532
|
+
child = null;
|
|
533
|
+
clearTimeout(stopGraceTimer);
|
|
534
|
+
clearTimeout(stopKillTimer);
|
|
535
|
+
stopGraceTimer = undefined;
|
|
536
|
+
stopKillTimer = undefined;
|
|
537
|
+
if (!stopping && !stale && code === 0 && receipt?.ok) {
|
|
538
|
+
({ supersededBy } = commitCandidate(candidatePath, receipt, generationHash));
|
|
539
|
+
} else if (!stopping && !stale) {
|
|
540
|
+
publishFailure(receipt, stdout, stderr, candidatePath, snapshotPath);
|
|
541
|
+
}
|
|
542
|
+
try { fs.rmSync(candidatePath, { force: true }); } catch {}
|
|
543
|
+
try { fs.rmSync(snapshotPath, { force: true }); } catch {}
|
|
544
|
+
|
|
545
|
+
if (stopping) {
|
|
546
|
+
finishStop();
|
|
547
|
+
} else if (pendingBuild || stale || supersededBy) {
|
|
548
|
+
pendingBuild = false;
|
|
549
|
+
if (supersededBy && sourceEpoch === generationEpoch) sourceEpoch += 1;
|
|
550
|
+
const digest = supersededBy || sourceDigest(inputPath);
|
|
551
|
+
queueStableBuild(digest.hash, true);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function queueStableBuild(hash, immediate = false) {
|
|
557
|
+
queuedHash = hash;
|
|
558
|
+
clearTimeout(debounceTimer);
|
|
559
|
+
const launch = () => {
|
|
560
|
+
if (stopping) return;
|
|
561
|
+
const digest = sourceDigest(inputPath);
|
|
562
|
+
if (digest.hash !== queuedHash) {
|
|
563
|
+
queueStableBuild(digest.hash);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (digest.hash === lastGoodSourceHash) {
|
|
567
|
+
if (state.status !== 'verified' && state.lastVerified) {
|
|
568
|
+
state.status = 'verified';
|
|
569
|
+
state.failure = null;
|
|
570
|
+
broadcast();
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (child) {
|
|
575
|
+
pendingBuild = true;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
beginBuild(digest, sourceEpoch);
|
|
579
|
+
};
|
|
580
|
+
debounceTimer = setTimeout(launch, immediate ? 0 : debounceMs);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function observeSource({ immediate = false } = {}) {
|
|
584
|
+
const digest = sourceDigest(inputPath);
|
|
585
|
+
if (!immediate && digest.hash === queuedHash) return;
|
|
586
|
+
sourceEpoch += 1;
|
|
587
|
+
queueStableBuild(digest.hash, immediate);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (options.watch !== false) {
|
|
591
|
+
try {
|
|
592
|
+
watcher = fs.watch(path.dirname(inputPath), (event, filename) => {
|
|
593
|
+
if (!filename || filename.toString() === path.basename(inputPath)) observeSource();
|
|
594
|
+
});
|
|
595
|
+
watcher.on('error', () => {
|
|
596
|
+
const failedWatcher = watcher;
|
|
597
|
+
watcher = undefined;
|
|
598
|
+
failedWatcher?.close();
|
|
599
|
+
});
|
|
600
|
+
} catch (error) {
|
|
601
|
+
await stop();
|
|
602
|
+
throw new Error(`Could not watch the input directory: ${error.message}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
pollTimer = setInterval(() => observeSource(), pollMs);
|
|
606
|
+
|
|
607
|
+
let opener = null;
|
|
608
|
+
if (shouldOpen) {
|
|
609
|
+
try {
|
|
610
|
+
opener = openLoopbackUrl(url);
|
|
611
|
+
} catch {
|
|
612
|
+
opener = { requested: true, status: 'unsupported', target: url, method: null };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
observeSource({ immediate: true });
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
url,
|
|
620
|
+
input: inputPath,
|
|
621
|
+
output: outputPath,
|
|
622
|
+
opener,
|
|
623
|
+
state: publicState,
|
|
624
|
+
stop,
|
|
625
|
+
closed,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export async function runPreview(options) {
|
|
630
|
+
const preview = await startPreview(options);
|
|
631
|
+
console.log(`preview ${preview.url}`);
|
|
632
|
+
console.log(`watching ${preview.input}`);
|
|
633
|
+
console.log(`output ${preview.output}`);
|
|
634
|
+
if (preview.opener && preview.opener.status !== 'opened') {
|
|
635
|
+
console.error(`Could not open the preview (${preview.opener.status}). Open it manually: ${preview.url}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let signalCount = 0;
|
|
639
|
+
const stop = () => {
|
|
640
|
+
signalCount += 1;
|
|
641
|
+
if (signalCount === 1) {
|
|
642
|
+
console.log('\nstopping preview…');
|
|
643
|
+
preview.stop();
|
|
644
|
+
} else {
|
|
645
|
+
console.log('\nforcing preview shutdown…');
|
|
646
|
+
preview.stop({ force: true });
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
process.on('SIGINT', stop);
|
|
650
|
+
process.on('SIGTERM', stop);
|
|
651
|
+
await preview.closed;
|
|
652
|
+
process.off('SIGINT', stop);
|
|
653
|
+
process.off('SIGTERM', stop);
|
|
654
|
+
}
|