@stacksjs/defaults 0.75.360 → 0.75.362
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-ai/SKILL.md +22 -0
- package/ai/skills/stacks-native/CATALOG.md +4 -4
- package/ai/skills/stacks-payments/SKILL.md +28 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
- package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
- package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
- package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
- package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
- package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
- package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
- package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
- package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
- package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
- package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
- package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
- package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
- package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
- package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
- package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
- package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
- package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
- package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
- package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
- package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
- package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
- package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
- package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
- package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
- package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
- package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
- package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
- package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
- package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
- package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
- package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
- package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
- package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
- package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
- package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
- package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
- package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
- package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
- package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
- package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
- package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
- package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
- package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
- package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
- package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
- package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
- package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
- package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
- package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -5,28 +5,78 @@ const OPENERS = {
|
|
|
5
5
|
darwin: {
|
|
6
6
|
command: 'open',
|
|
7
7
|
method: 'open',
|
|
8
|
+
timeoutMs: 5000,
|
|
8
9
|
args: (target) => [target],
|
|
9
10
|
},
|
|
10
11
|
linux: {
|
|
11
12
|
command: 'xdg-open',
|
|
12
13
|
method: 'xdg-open',
|
|
14
|
+
timeoutMs: 5000,
|
|
13
15
|
args: (target) => [target],
|
|
14
16
|
},
|
|
15
17
|
win32: {
|
|
16
18
|
command: 'powershell.exe',
|
|
17
19
|
method: 'powershell',
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
20
|
+
// PowerShell cold starts can approach five seconds on hosted Windows
|
|
21
|
+
// runners. Keep the launch bounded without treating normal startup as a
|
|
22
|
+
// timeout.
|
|
23
|
+
timeoutMs: 15000,
|
|
24
|
+
// Keep the command constant and pass the target through a child-only
|
|
25
|
+
// environment variable. Paths are never interpolated into executable source.
|
|
26
|
+
args: () => [
|
|
21
27
|
'-NoProfile',
|
|
22
28
|
'-NonInteractive',
|
|
23
29
|
'-Command',
|
|
24
|
-
'Start-Process -FilePath $
|
|
25
|
-
target,
|
|
30
|
+
'Start-Process -FilePath $env:ARCHIFY_OPEN_TARGET',
|
|
26
31
|
],
|
|
27
32
|
},
|
|
28
33
|
};
|
|
29
34
|
|
|
35
|
+
function failureDetails(result, opener, timeoutMs) {
|
|
36
|
+
const error = result?.error;
|
|
37
|
+
if (error?.code === 'ENOENT') {
|
|
38
|
+
return {
|
|
39
|
+
code: 'opener/unavailable',
|
|
40
|
+
reason: `Could not find ${opener.command}. Install or enable the platform opener, then open the target manually.`,
|
|
41
|
+
systemCode: 'ENOENT',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (error?.code === 'ETIMEDOUT') {
|
|
45
|
+
return {
|
|
46
|
+
code: 'opener/timeout',
|
|
47
|
+
reason: `${opener.command} did not finish within ${timeoutMs}ms. Open the target manually or retry when the system is less busy.`,
|
|
48
|
+
systemCode: 'ETIMEDOUT',
|
|
49
|
+
timeoutMs,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (error) {
|
|
53
|
+
return {
|
|
54
|
+
code: 'opener/spawn-failed',
|
|
55
|
+
reason: error.message || `${opener.command} could not be started. Open the target manually.`,
|
|
56
|
+
...(error.code ? { systemCode: String(error.code) } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (result?.signal) {
|
|
60
|
+
return {
|
|
61
|
+
code: 'opener/signaled',
|
|
62
|
+
reason: `${opener.command} was terminated by ${result.signal}. Open the target manually.`,
|
|
63
|
+
signal: result.signal,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (result?.status !== 0) {
|
|
67
|
+
return {
|
|
68
|
+
code: 'opener/nonzero-exit',
|
|
69
|
+
reason: `${opener.command} exited with status ${result?.status ?? 'unknown'}. Open the target manually.`,
|
|
70
|
+
exitCode: result?.status ?? null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function launchTimeout(value, fallback) {
|
|
77
|
+
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
30
80
|
function launchTarget(target, options = {}) {
|
|
31
81
|
const platform = options.platform || process.platform;
|
|
32
82
|
const opener = OPENERS[platform];
|
|
@@ -40,28 +90,38 @@ function launchTarget(target, options = {}) {
|
|
|
40
90
|
}
|
|
41
91
|
|
|
42
92
|
const spawn = options.spawn || spawnSync;
|
|
93
|
+
const timeoutMs = launchTimeout(options.timeoutMs, opener.timeoutMs);
|
|
43
94
|
let result;
|
|
44
95
|
try {
|
|
45
|
-
|
|
96
|
+
const spawnOptions = {
|
|
46
97
|
encoding: 'utf8',
|
|
47
98
|
shell: false,
|
|
48
99
|
stdio: 'ignore',
|
|
49
|
-
timeout:
|
|
100
|
+
timeout: timeoutMs,
|
|
50
101
|
windowsHide: true,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
102
|
+
};
|
|
103
|
+
if (platform === 'win32') {
|
|
104
|
+
spawnOptions.env = {
|
|
105
|
+
...process.env,
|
|
106
|
+
ARCHIFY_OPEN_TARGET: target,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
result = spawn(opener.command, opener.args(target), spawnOptions);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
result = { error };
|
|
54
112
|
}
|
|
55
113
|
|
|
114
|
+
const failure = failureDetails(result, opener, timeoutMs);
|
|
56
115
|
let status = 'opened';
|
|
57
|
-
if (
|
|
58
|
-
else if (
|
|
116
|
+
if (failure?.code === 'opener/unavailable') status = 'unsupported';
|
|
117
|
+
else if (failure) status = 'failed';
|
|
59
118
|
|
|
60
119
|
return {
|
|
61
120
|
requested: true,
|
|
62
121
|
status,
|
|
63
122
|
target,
|
|
64
123
|
method: opener.method,
|
|
124
|
+
...(failure ? { failure } : {}),
|
|
65
125
|
};
|
|
66
126
|
}
|
|
67
127
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
|
|
2
|
+
import { softwareNoticesHtml } from '../renderers/shared/software-notices.mjs';
|
|
1
3
|
import { spawn } from 'node:child_process';
|
|
2
4
|
import { createHash } from 'node:crypto';
|
|
3
5
|
import fs from 'node:fs';
|
|
@@ -7,7 +9,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
9
|
|
|
8
10
|
import { openLoopbackUrl } from './open-artifact.mjs';
|
|
9
11
|
import { resolveOutputPath } from '../renderers/shared/output-path.mjs';
|
|
10
|
-
import {
|
|
12
|
+
import { sameLocation } from '../renderers/shared/path-semantics.mjs';
|
|
13
|
+
import {
|
|
14
|
+
captureAtomicOutput,
|
|
15
|
+
captureRegularFileBinding,
|
|
16
|
+
publishRegularFileBinding,
|
|
17
|
+
releaseRegularFileBinding,
|
|
18
|
+
removeEmptyDirectoryWithRetry,
|
|
19
|
+
removeOwnedRegularFile,
|
|
20
|
+
verifyAtomicOutput,
|
|
21
|
+
} from '../renderers/shared/atomic-output.mjs';
|
|
11
22
|
|
|
12
23
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
24
|
const cliPath = path.join(here, 'archify.mjs');
|
|
@@ -16,7 +27,8 @@ const defaultDebounceMs = 400;
|
|
|
16
27
|
const defaultPollMs = 800;
|
|
17
28
|
const defaultStopGraceMs = 3000;
|
|
18
29
|
const defaultStopKillMs = 750;
|
|
19
|
-
const diagramTypes = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']);
|
|
30
|
+
const diagramTypes = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle', 'erd', 'tree', 'class', 'timeline', 'waterfall']);
|
|
31
|
+
let previewCommitSequence = 0;
|
|
20
32
|
|
|
21
33
|
function sha256(value) {
|
|
22
34
|
return createHash('sha256').update(value).digest('hex');
|
|
@@ -34,7 +46,7 @@ function sourceDigest(inputPath) {
|
|
|
34
46
|
function initialAuthoredOutput(inputPath) {
|
|
35
47
|
try {
|
|
36
48
|
const source = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
37
|
-
if (typeof source?.meta?.output === 'string'
|
|
49
|
+
if (typeof source?.meta?.output === 'string') {
|
|
38
50
|
return source.meta.output;
|
|
39
51
|
}
|
|
40
52
|
} catch {
|
|
@@ -48,9 +60,9 @@ function previewPage() {
|
|
|
48
60
|
return `<!doctype html>
|
|
49
61
|
<html lang="en">
|
|
50
62
|
<head>
|
|
51
|
-
<meta charset="utf-8"
|
|
63
|
+
<meta charset="utf-8">${softwareNoticesHtml}
|
|
52
64
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
53
|
-
<title>
|
|
65
|
+
<title>Archify Live Preview</title>
|
|
54
66
|
<style>
|
|
55
67
|
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
|
|
56
68
|
* { box-sizing: border-box; }
|
|
@@ -78,7 +90,7 @@ function previewPage() {
|
|
|
78
90
|
</head>
|
|
79
91
|
<body data-state="checking" data-has-artifact="false">
|
|
80
92
|
<header>
|
|
81
|
-
<span class="brand">
|
|
93
|
+
<span class="brand">Archify Preview</span>
|
|
82
94
|
<details id="failure" hidden>
|
|
83
95
|
<summary role="button" aria-controls="diagnostic-panel">View diagnostic</summary>
|
|
84
96
|
<div class="diagnostic" id="diagnostic-panel"><pre id="diagnostic"></pre><button id="copy" type="button">Copy diagnostic</button></div>
|
|
@@ -87,7 +99,7 @@ function previewPage() {
|
|
|
87
99
|
</header>
|
|
88
100
|
<main>
|
|
89
101
|
<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>
|
|
102
|
+
<iframe id="artifact" title="Verified Archify diagram"></iframe>
|
|
91
103
|
</main>
|
|
92
104
|
<script>
|
|
93
105
|
(function () {
|
|
@@ -179,6 +191,161 @@ function parseReceipt(stdout) {
|
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
|
|
194
|
+
function atomicOutputFailure(result) {
|
|
195
|
+
const code = result?.reason?.code || 'unclassified';
|
|
196
|
+
let diagnosticCode = 'output/target-indeterminate';
|
|
197
|
+
let message;
|
|
198
|
+
if (code === 'target-hardlinked' || code === 'candidate-hardlinked') {
|
|
199
|
+
diagnosticCode = 'output/target-hardlinked';
|
|
200
|
+
message = code === 'candidate-hardlinked'
|
|
201
|
+
? 'Preview commit candidate has multiple hard-link names and cannot be published safely.'
|
|
202
|
+
: 'Preview output has multiple hard-link names; atomic publication cannot update every name.';
|
|
203
|
+
} else if (code === 'target-not-regular-file' || code === 'candidate-not-regular-file') {
|
|
204
|
+
diagnosticCode = 'output/target-not-regular-file';
|
|
205
|
+
message = code === 'candidate-not-regular-file'
|
|
206
|
+
? 'Preview commit candidate is no longer a regular file.'
|
|
207
|
+
: 'Preview output already exists and is not a regular file.';
|
|
208
|
+
} else if (result?.status === 'different') {
|
|
209
|
+
diagnosticCode = 'output/target-changed';
|
|
210
|
+
message = `Preview output changed while the verified candidate was being prepared (${code}).`;
|
|
211
|
+
} else {
|
|
212
|
+
message = `Preview output stability could not be determined safely (${code}).`;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
code: diagnosticCode,
|
|
216
|
+
message,
|
|
217
|
+
evidence: { relation: result?.reason || { code } },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function atomicOutputError(result) {
|
|
222
|
+
const failure = atomicOutputFailure(result);
|
|
223
|
+
return Object.assign(new Error(failure.message), { previewFailure: failure });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function stagePreviewCommit(commitPath, artifact, mode) {
|
|
227
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
228
|
+
previewCommitSequence += 1;
|
|
229
|
+
const candidatePath = path.join(
|
|
230
|
+
path.dirname(commitPath),
|
|
231
|
+
`.archify-preview-commit-${process.pid}-${Date.now().toString(36)}-${previewCommitSequence}.tmp`,
|
|
232
|
+
);
|
|
233
|
+
let descriptor;
|
|
234
|
+
let identity;
|
|
235
|
+
try {
|
|
236
|
+
const noFollow = process.platform === 'win32' ? 0 : (fs.constants.O_NOFOLLOW || 0);
|
|
237
|
+
descriptor = fs.openSync(
|
|
238
|
+
candidatePath,
|
|
239
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
|
|
240
|
+
mode ?? 0o666,
|
|
241
|
+
);
|
|
242
|
+
let metadata;
|
|
243
|
+
try {
|
|
244
|
+
metadata = fs.fstatSync(descriptor, { bigint: true });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
try {
|
|
247
|
+
const retry = fs.fstatSync(descriptor, { bigint: true });
|
|
248
|
+
if (retry.isFile() && retry.ino !== 0n) {
|
|
249
|
+
identity = { device: retry.dev, inode: retry.ino };
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
if (!metadata.isFile() || metadata.ino === 0n) {
|
|
255
|
+
throw new Error('Preview commit candidate identity could not be verified safely.');
|
|
256
|
+
}
|
|
257
|
+
identity = { device: metadata.dev, inode: metadata.ino };
|
|
258
|
+
fs.writeFileSync(descriptor, artifact);
|
|
259
|
+
if (mode !== null) fs.fchmodSync(descriptor, mode);
|
|
260
|
+
fs.closeSync(descriptor);
|
|
261
|
+
descriptor = undefined;
|
|
262
|
+
return { candidatePath, identity };
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (descriptor !== undefined) {
|
|
265
|
+
try { fs.closeSync(descriptor); } catch {}
|
|
266
|
+
}
|
|
267
|
+
if (error.code === 'EEXIST') continue;
|
|
268
|
+
if (identity) removeOwnedRegularFile(candidatePath, identity);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw Object.assign(new Error('Could not reserve a preview commit candidate beside the output.'), {
|
|
273
|
+
code: 'EEXIST',
|
|
274
|
+
errno: -17,
|
|
275
|
+
syscall: 'open',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function captureOwnedDirectory(directoryPath) {
|
|
280
|
+
const metadata = fs.lstatSync(directoryPath, { bigint: true });
|
|
281
|
+
if (!metadata.isDirectory() || metadata.ino === 0n) {
|
|
282
|
+
throw new Error('Preview staging directory identity could not be verified safely.');
|
|
283
|
+
}
|
|
284
|
+
return { device: metadata.dev, inode: metadata.ino };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function cleanupOwnedDirectory(directoryPath, identity) {
|
|
288
|
+
let current;
|
|
289
|
+
try {
|
|
290
|
+
current = fs.lstatSync(directoryPath, { bigint: true });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return;
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
// Preserve a replacement rather than recursively deleting a directory that
|
|
296
|
+
// this preview instance did not create.
|
|
297
|
+
if (!current.isDirectory()
|
|
298
|
+
|| current.ino === 0n
|
|
299
|
+
|| current.dev !== identity.device
|
|
300
|
+
|| current.ino !== identity.inode) return;
|
|
301
|
+
try {
|
|
302
|
+
removeEmptyDirectoryWithRetry(directoryPath);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// An entry whose identity was never bound to this preview may be an
|
|
305
|
+
// external claimant. Preserve the private directory as recovery material
|
|
306
|
+
// instead of recursively deleting unknown contents.
|
|
307
|
+
if (error?.code !== 'ENOTEMPTY' && error?.code !== 'EEXIST') throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function captureOwnedDeliverySidecars(candidatePath, snapshotPath, receipt) {
|
|
312
|
+
const suffixes = ['.delivery.json', '.delivery-pending.json'];
|
|
313
|
+
const capturedSidecars = [];
|
|
314
|
+
for (const suffix of suffixes) {
|
|
315
|
+
const sidecarPath = candidatePath.replace(/\.html$/iu, suffix);
|
|
316
|
+
const captured = captureRegularFileBinding(sidecarPath, {
|
|
317
|
+
subject: 'preview-delivery-sidecar',
|
|
318
|
+
expectedLinks: 1,
|
|
319
|
+
includeContent: true,
|
|
320
|
+
});
|
|
321
|
+
if (captured.status !== 'captured') continue;
|
|
322
|
+
try {
|
|
323
|
+
const sidecar = JSON.parse(captured.content.buffer.toString('utf8'));
|
|
324
|
+
const commonMatches = sidecar?.schemaVersion === 1
|
|
325
|
+
&& sidecar.command === 'deliver'
|
|
326
|
+
&& sameLocation(sidecar.output, candidatePath).status === 'match'
|
|
327
|
+
&& (!receipt?.receiptId || sidecar.receiptId === receipt.receiptId);
|
|
328
|
+
const currentMatches = suffix === '.delivery.json'
|
|
329
|
+
&& commonMatches
|
|
330
|
+
&& sidecar.status === 'current'
|
|
331
|
+
&& receipt?.artifact?.sha256
|
|
332
|
+
&& sidecar.artifact?.sha256 === receipt.artifact.sha256;
|
|
333
|
+
const pendingMatches = suffix === '.delivery-pending.json'
|
|
334
|
+
&& commonMatches
|
|
335
|
+
&& sidecar.status === 'pending'
|
|
336
|
+
&& sameLocation(sidecar.input, snapshotPath).status === 'match';
|
|
337
|
+
if (currentMatches || pendingMatches) {
|
|
338
|
+
capturedSidecars.push({ path: sidecarPath, identity: captured.identity });
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
// Preserve malformed or claimant-controlled sidecars for recovery.
|
|
342
|
+
} finally {
|
|
343
|
+
releaseRegularFileBinding(captured.binding);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return capturedSidecars;
|
|
347
|
+
}
|
|
348
|
+
|
|
182
349
|
export async function startPreview(options) {
|
|
183
350
|
const type = options.type;
|
|
184
351
|
if (!diagramTypes.has(type)) throw new Error(`Unknown diagram type "${type}".`);
|
|
@@ -203,7 +370,20 @@ export async function startPreview(options) {
|
|
|
203
370
|
const shouldOpen = options.open !== false;
|
|
204
371
|
|
|
205
372
|
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
206
|
-
|
|
373
|
+
// Keep cleanup bound to the physical directory selected at startup. A
|
|
374
|
+
// symlink or junction in the requested output parent may be redirected while
|
|
375
|
+
// preview is running and must not redirect recursive cleanup to a claimant.
|
|
376
|
+
const physicalOutputDirectory = fs.realpathSync.native(outputDirectory);
|
|
377
|
+
const stagingDirectory = fs.mkdtempSync(path.toNamespacedPath(
|
|
378
|
+
path.join(physicalOutputDirectory, '.archify-preview-'),
|
|
379
|
+
));
|
|
380
|
+
let stagingIdentity;
|
|
381
|
+
try {
|
|
382
|
+
stagingIdentity = captureOwnedDirectory(stagingDirectory);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
try { fs.rmdirSync(stagingDirectory); } catch {}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
207
387
|
|
|
208
388
|
let port = 0;
|
|
209
389
|
let watcher;
|
|
@@ -213,6 +393,7 @@ export async function startPreview(options) {
|
|
|
213
393
|
let stopKillTimer;
|
|
214
394
|
let child;
|
|
215
395
|
let stopping = false;
|
|
396
|
+
let forceStopping = false;
|
|
216
397
|
let stopped = false;
|
|
217
398
|
let serverClosing = false;
|
|
218
399
|
let serverClosed = false;
|
|
@@ -224,6 +405,7 @@ export async function startPreview(options) {
|
|
|
224
405
|
let pendingBuild = false;
|
|
225
406
|
let artifactBuffer = null;
|
|
226
407
|
const clients = new Set();
|
|
408
|
+
const sockets = new Set();
|
|
227
409
|
const state = {
|
|
228
410
|
schemaVersion: 1,
|
|
229
411
|
status: 'checking',
|
|
@@ -315,6 +497,14 @@ export async function startPreview(options) {
|
|
|
315
497
|
res.end('Not found');
|
|
316
498
|
});
|
|
317
499
|
|
|
500
|
+
server.on('connection', (socket) => {
|
|
501
|
+
sockets.add(socket);
|
|
502
|
+
socket.once('close', () => sockets.delete(socket));
|
|
503
|
+
// A connection event already queued when force-stop begins must not keep
|
|
504
|
+
// server.close() waiting after the current sockets have been destroyed.
|
|
505
|
+
if (forceStopping) socket.destroy();
|
|
506
|
+
});
|
|
507
|
+
|
|
318
508
|
try {
|
|
319
509
|
await new Promise((resolve, reject) => {
|
|
320
510
|
server.once('error', reject);
|
|
@@ -326,7 +516,7 @@ export async function startPreview(options) {
|
|
|
326
516
|
});
|
|
327
517
|
} catch (error) {
|
|
328
518
|
try { server.close(); } catch {}
|
|
329
|
-
|
|
519
|
+
cleanupOwnedDirectory(stagingDirectory, stagingIdentity);
|
|
330
520
|
throw error;
|
|
331
521
|
}
|
|
332
522
|
|
|
@@ -340,7 +530,7 @@ export async function startPreview(options) {
|
|
|
340
530
|
clearTimeout(stopGraceTimer);
|
|
341
531
|
clearTimeout(stopKillTimer);
|
|
342
532
|
try {
|
|
343
|
-
|
|
533
|
+
cleanupOwnedDirectory(stagingDirectory, stagingIdentity);
|
|
344
534
|
} finally {
|
|
345
535
|
resolveClosed();
|
|
346
536
|
}
|
|
@@ -383,6 +573,7 @@ export async function startPreview(options) {
|
|
|
383
573
|
}
|
|
384
574
|
|
|
385
575
|
async function stop({ force = false } = {}) {
|
|
576
|
+
if (force) forceStopping = true;
|
|
386
577
|
if (!stopping) {
|
|
387
578
|
stopping = true;
|
|
388
579
|
clearTimeout(debounceTimer);
|
|
@@ -390,6 +581,9 @@ export async function startPreview(options) {
|
|
|
390
581
|
watcher?.close();
|
|
391
582
|
closeServer();
|
|
392
583
|
}
|
|
584
|
+
if (forceStopping) {
|
|
585
|
+
for (const socket of sockets) socket.destroy();
|
|
586
|
+
}
|
|
393
587
|
if (child && force) {
|
|
394
588
|
clearTimeout(stopGraceTimer);
|
|
395
589
|
clearTimeout(stopKillTimer);
|
|
@@ -423,6 +617,8 @@ export async function startPreview(options) {
|
|
|
423
617
|
state.status = 'needs-fix';
|
|
424
618
|
state.failure = {
|
|
425
619
|
stage: receipt?.stage || 'render',
|
|
620
|
+
...(receipt?.code ? { code: receipt.code } : {}),
|
|
621
|
+
...(receipt?.evidence ? { evidence: receipt.evidence } : {}),
|
|
426
622
|
message: redactDiagnostic(
|
|
427
623
|
diagnostic,
|
|
428
624
|
[
|
|
@@ -440,25 +636,69 @@ export async function startPreview(options) {
|
|
|
440
636
|
broadcast();
|
|
441
637
|
}
|
|
442
638
|
|
|
443
|
-
function commitCandidate(candidatePath, receipt, generationHash) {
|
|
639
|
+
function commitCandidate(candidatePath, receipt, generationHash, outputCapture) {
|
|
444
640
|
let candidate;
|
|
641
|
+
let sourceCandidateBinding;
|
|
642
|
+
let sourceCandidateIdentity;
|
|
643
|
+
let commitCandidatePath;
|
|
644
|
+
let commitCandidateIdentity;
|
|
645
|
+
let commitCandidateBinding;
|
|
445
646
|
try {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
647
|
+
const sourceCapture = captureRegularFileBinding(candidatePath, {
|
|
648
|
+
subject: 'candidate',
|
|
649
|
+
expectedSha256: receipt?.artifact?.sha256,
|
|
650
|
+
expectedBytes: receipt?.artifact?.bytes,
|
|
651
|
+
includeContent: true,
|
|
652
|
+
});
|
|
653
|
+
if (sourceCapture.status !== 'captured') throw atomicOutputError(sourceCapture);
|
|
654
|
+
sourceCandidateBinding = sourceCapture.binding;
|
|
655
|
+
sourceCandidateIdentity = sourceCapture.identity;
|
|
656
|
+
candidate = sourceCapture.content.buffer;
|
|
657
|
+
const digest = sourceCapture.content.sha256;
|
|
658
|
+
const releasedSource = releaseRegularFileBinding(sourceCandidateBinding);
|
|
659
|
+
sourceCandidateBinding = undefined;
|
|
660
|
+
if (releasedSource.status !== 'released') throw atomicOutputError(releasedSource);
|
|
451
661
|
resolveOutputPath(outputRequest);
|
|
452
662
|
const sameArtifact = state.lastVerified?.sha256 === digest;
|
|
453
|
-
let outputMatches = false;
|
|
454
|
-
if (sameArtifact) {
|
|
455
|
-
try { outputMatches = sha256(fs.readFileSync(outputPath)) === digest; } catch {}
|
|
456
|
-
}
|
|
457
663
|
const currentSource = sourceDigest(inputPath);
|
|
458
664
|
if (currentSource.hash !== generationHash) {
|
|
459
|
-
return {
|
|
665
|
+
return {
|
|
666
|
+
committed: false,
|
|
667
|
+
supersededBy: currentSource,
|
|
668
|
+
candidateIdentity: sourceCandidateIdentity,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const beforeStage = verifyAtomicOutput(outputCapture.snapshot);
|
|
672
|
+
if (beforeStage.status !== 'match') throw atomicOutputError(beforeStage);
|
|
673
|
+
({
|
|
674
|
+
candidatePath: commitCandidatePath,
|
|
675
|
+
identity: commitCandidateIdentity,
|
|
676
|
+
} = stagePreviewCommit(outputCapture.commitPath, candidate, outputCapture.mode));
|
|
677
|
+
const candidateCapture = captureRegularFileBinding(commitCandidatePath, {
|
|
678
|
+
subject: 'candidate',
|
|
679
|
+
expectedSha256: digest,
|
|
680
|
+
expectedBytes: candidate.byteLength,
|
|
681
|
+
expectedIdentity: commitCandidateIdentity,
|
|
682
|
+
...(outputCapture.mode === null ? {} : { expectedMode: outputCapture.mode }),
|
|
683
|
+
});
|
|
684
|
+
if (candidateCapture.status !== 'captured') throw atomicOutputError(candidateCapture);
|
|
685
|
+
commitCandidateBinding = candidateCapture.binding;
|
|
686
|
+
const beforeCommit = verifyAtomicOutput(outputCapture.snapshot);
|
|
687
|
+
if (beforeCommit.status !== 'match') throw atomicOutputError(beforeCommit);
|
|
688
|
+
const publication = publishRegularFileBinding(
|
|
689
|
+
commitCandidateBinding,
|
|
690
|
+
commitCandidatePath,
|
|
691
|
+
outputCapture.snapshot,
|
|
692
|
+
{ subject: 'candidate' },
|
|
693
|
+
);
|
|
694
|
+
if (!['committed', 'committed-with-warning'].includes(publication.status)) {
|
|
695
|
+
throw atomicOutputError(publication);
|
|
460
696
|
}
|
|
461
|
-
|
|
697
|
+
const releasedCandidate = releaseRegularFileBinding(commitCandidateBinding);
|
|
698
|
+
commitCandidateBinding = undefined;
|
|
699
|
+
if (releasedCandidate.status !== 'released') throw atomicOutputError(releasedCandidate);
|
|
700
|
+
commitCandidatePath = undefined;
|
|
701
|
+
commitCandidateIdentity = undefined;
|
|
462
702
|
artifactBuffer = candidate;
|
|
463
703
|
lastGoodSourceHash = generationHash;
|
|
464
704
|
state.status = 'verified';
|
|
@@ -475,10 +715,20 @@ export async function startPreview(options) {
|
|
|
475
715
|
}
|
|
476
716
|
state.failure = null;
|
|
477
717
|
broadcast();
|
|
478
|
-
return { committed: true, supersededBy: null };
|
|
718
|
+
return { committed: true, supersededBy: null, candidateIdentity: sourceCandidateIdentity };
|
|
479
719
|
} catch (error) {
|
|
480
|
-
publishFailure({
|
|
481
|
-
|
|
720
|
+
publishFailure({
|
|
721
|
+
stage: 'commit',
|
|
722
|
+
error: `Could not publish the verified preview: ${error.message}`,
|
|
723
|
+
...(error.previewFailure || {}),
|
|
724
|
+
}, '', '', candidatePath);
|
|
725
|
+
return { committed: false, supersededBy: null, candidateIdentity: sourceCandidateIdentity };
|
|
726
|
+
} finally {
|
|
727
|
+
if (sourceCandidateBinding) releaseRegularFileBinding(sourceCandidateBinding);
|
|
728
|
+
if (commitCandidateBinding) releaseRegularFileBinding(commitCandidateBinding);
|
|
729
|
+
if (commitCandidatePath && commitCandidateIdentity) {
|
|
730
|
+
removeOwnedRegularFile(commitCandidatePath, commitCandidateIdentity);
|
|
731
|
+
}
|
|
482
732
|
}
|
|
483
733
|
}
|
|
484
734
|
|
|
@@ -493,9 +743,38 @@ export async function startPreview(options) {
|
|
|
493
743
|
|
|
494
744
|
const candidatePath = path.join(stagingDirectory, `generation-${state.generation}.html`);
|
|
495
745
|
const snapshotPath = path.join(stagingDirectory, `generation-${state.generation}.json`);
|
|
746
|
+
let snapshotIdentity;
|
|
747
|
+
const outputCapture = captureAtomicOutput(outputPath);
|
|
748
|
+
if (outputCapture.status !== 'captured') {
|
|
749
|
+
const failure = atomicOutputFailure(outputCapture);
|
|
750
|
+
publishFailure(
|
|
751
|
+
{ stage: 'prepare', error: failure.message, ...failure },
|
|
752
|
+
'',
|
|
753
|
+
'',
|
|
754
|
+
candidatePath,
|
|
755
|
+
snapshotPath,
|
|
756
|
+
);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const beforeBuild = verifyAtomicOutput(outputCapture.snapshot);
|
|
760
|
+
if (beforeBuild.status !== 'match') {
|
|
761
|
+
const failure = atomicOutputFailure(beforeBuild);
|
|
762
|
+
publishFailure(
|
|
763
|
+
{ stage: 'prepare', error: failure.message, ...failure },
|
|
764
|
+
'',
|
|
765
|
+
'',
|
|
766
|
+
candidatePath,
|
|
767
|
+
snapshotPath,
|
|
768
|
+
);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
496
771
|
if (digest.bytes !== null) {
|
|
497
772
|
try {
|
|
498
773
|
fs.writeFileSync(snapshotPath, digest.bytes, { flag: 'wx', mode: 0o600 });
|
|
774
|
+
const metadata = fs.lstatSync(snapshotPath, { bigint: true });
|
|
775
|
+
if (metadata.isFile() && metadata.ino !== 0n) {
|
|
776
|
+
snapshotIdentity = { device: metadata.dev, inode: metadata.ino };
|
|
777
|
+
}
|
|
499
778
|
} catch (error) {
|
|
500
779
|
publishFailure(
|
|
501
780
|
{ stage: 'prepare', error: `Could not snapshot the observed input: ${error.message}` },
|
|
@@ -529,18 +808,44 @@ export async function startPreview(options) {
|
|
|
529
808
|
const generationHash = activeHash;
|
|
530
809
|
const stale = generationEpoch !== sourceEpoch;
|
|
531
810
|
let supersededBy = null;
|
|
811
|
+
let candidateIdentity;
|
|
812
|
+
const deliverySidecars = captureOwnedDeliverySidecars(
|
|
813
|
+
candidatePath,
|
|
814
|
+
snapshotPath,
|
|
815
|
+
receipt,
|
|
816
|
+
);
|
|
532
817
|
child = null;
|
|
533
818
|
clearTimeout(stopGraceTimer);
|
|
534
819
|
clearTimeout(stopKillTimer);
|
|
535
820
|
stopGraceTimer = undefined;
|
|
536
821
|
stopKillTimer = undefined;
|
|
537
822
|
if (!stopping && !stale && code === 0 && receipt?.ok) {
|
|
538
|
-
({ supersededBy } = commitCandidate(
|
|
823
|
+
({ supersededBy, candidateIdentity } = commitCandidate(
|
|
824
|
+
candidatePath,
|
|
825
|
+
receipt,
|
|
826
|
+
generationHash,
|
|
827
|
+
outputCapture,
|
|
828
|
+
));
|
|
829
|
+
} else if (code === 0 && receipt?.ok) {
|
|
830
|
+
const abandonedCandidate = captureRegularFileBinding(candidatePath, {
|
|
831
|
+
subject: 'abandoned-preview-candidate',
|
|
832
|
+
expectedSha256: receipt.artifact?.sha256,
|
|
833
|
+
expectedBytes: receipt.artifact?.bytes,
|
|
834
|
+
});
|
|
835
|
+
if (abandonedCandidate.status === 'captured') {
|
|
836
|
+
candidateIdentity = abandonedCandidate.identity;
|
|
837
|
+
releaseRegularFileBinding(abandonedCandidate.binding);
|
|
838
|
+
}
|
|
539
839
|
} else if (!stopping && !stale) {
|
|
540
840
|
publishFailure(receipt, stdout, stderr, candidatePath, snapshotPath);
|
|
541
841
|
}
|
|
542
|
-
|
|
543
|
-
|
|
842
|
+
if (candidateIdentity) removeOwnedRegularFile(candidatePath, candidateIdentity);
|
|
843
|
+
if (snapshotIdentity) removeOwnedRegularFile(snapshotPath, snapshotIdentity, { subject: 'snapshot' });
|
|
844
|
+
for (const deliverySidecar of deliverySidecars) {
|
|
845
|
+
removeOwnedRegularFile(deliverySidecar.path, deliverySidecar.identity, {
|
|
846
|
+
subject: 'preview-delivery-sidecar',
|
|
847
|
+
});
|
|
848
|
+
}
|
|
544
849
|
|
|
545
850
|
if (stopping) {
|
|
546
851
|
finishStop();
|
|
@@ -589,8 +894,20 @@ export async function startPreview(options) {
|
|
|
589
894
|
|
|
590
895
|
if (options.watch !== false) {
|
|
591
896
|
try {
|
|
592
|
-
|
|
593
|
-
|
|
897
|
+
// `path.resolve` keeps Windows 8.3 names intact. Canonicalize short names
|
|
898
|
+
// and junctions before libuv opens the directory so its callback path has
|
|
899
|
+
// the same prefix as the watched path.
|
|
900
|
+
const watchedDirectory = fs.realpathSync.native(path.dirname(inputPath));
|
|
901
|
+
watcher = fs.watch(watchedDirectory, (event, filename) => {
|
|
902
|
+
// On Windows the watcher hands us just the basename; on POSIX it can be
|
|
903
|
+
// null. Resolve named events against the directory libuv actually opened
|
|
904
|
+
// so file-level case, 8.3, and hard-link aliases still identify the input.
|
|
905
|
+
if (
|
|
906
|
+
!filename
|
|
907
|
+
|| sameLocation(path.join(watchedDirectory, filename.toString()), inputPath).status === 'match'
|
|
908
|
+
) {
|
|
909
|
+
observeSource();
|
|
910
|
+
}
|
|
594
911
|
});
|
|
595
912
|
watcher.on('error', () => {
|
|
596
913
|
const failedWatcher = watcher;
|
|
@@ -632,7 +949,7 @@ export async function runPreview(options) {
|
|
|
632
949
|
console.log(`watching ${preview.input}`);
|
|
633
950
|
console.log(`output ${preview.output}`);
|
|
634
951
|
if (preview.opener && preview.opener.status !== 'opened') {
|
|
635
|
-
console.error(`Could not open the preview (${preview.opener.status}). Open it manually: ${preview.url}`);
|
|
952
|
+
console.error(`Could not open the preview (${preview.opener.status}). ${preview.opener.failure?.reason || 'Open it manually.'} Target: ${preview.url}`);
|
|
636
953
|
}
|
|
637
954
|
|
|
638
955
|
let signalCount = 0;
|