@stacksjs/defaults 0.75.361 → 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.
Files changed (208) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +2 -2
  3. package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
  4. package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
  5. package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
  6. package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
  7. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
  8. package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
  9. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
  10. package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
  11. package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
  12. package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
  14. package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
  15. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
  16. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
  17. package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
  18. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
  19. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
  20. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
  21. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
  22. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
  23. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
  24. package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
  25. package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
  27. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
  28. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
  29. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
  30. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
  31. package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
  32. package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
  33. package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
  34. package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
  35. package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
  36. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
  37. package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
  38. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
  39. package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
  40. package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
  41. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
  42. package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
  43. package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
  44. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
  45. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
  46. package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
  47. package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
  48. package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
  49. package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
  50. package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
  51. package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
  52. package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
  53. package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
  54. package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
  55. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
  56. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
  57. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
  58. package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
  59. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
  60. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
  61. package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
  62. package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
  63. package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
  64. package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
  65. package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
  66. package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
  67. package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
  68. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
  69. package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
  70. package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
  71. package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
  72. package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
  73. package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
  74. package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
  75. package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
  76. package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
  77. package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
  78. package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
  79. package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
  80. package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
  81. package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
  82. package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
  83. package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
  84. package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
  85. package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
  86. package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
  87. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
  88. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
  89. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
  90. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
  91. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
  92. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
  93. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
  94. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
  95. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
  96. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
  97. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
  98. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
  99. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
  100. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
  101. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
  102. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
  103. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
  104. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
  105. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
  106. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
  107. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
  108. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
  109. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
  110. package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
  111. package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
  112. package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
  113. package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
  114. package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
  115. package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
  116. package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
  117. package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
  118. package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
  119. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
  120. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
  121. package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
  122. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
  123. package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
  124. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
  125. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
  126. package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
  127. package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
  128. package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
  129. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
  130. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
  131. package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
  132. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
  133. package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
  134. package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
  135. package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
  136. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
  137. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
  138. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
  139. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
  140. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
  141. package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
  142. package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
  143. package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
  144. package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
  145. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
  146. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
  147. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
  148. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
  149. package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
  150. package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
  151. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
  152. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
  153. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
  154. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
  155. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
  156. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
  157. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
  158. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
  159. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
  160. package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
  161. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
  162. package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
  163. package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
  164. package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
  165. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
  166. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
  167. package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
  168. package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
  169. package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
  170. package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
  171. package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
  172. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
  173. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
  174. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
  175. package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
  176. package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
  177. package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
  178. package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
  179. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
  180. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
  181. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
  182. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
  183. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
  184. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
  185. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
  186. package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
  187. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
  188. package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
  189. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
  190. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
  191. package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
  192. package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
  193. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
  194. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
  195. package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
  196. package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
  197. package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
  198. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
  199. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
  200. package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
  201. package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
  202. package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
  203. package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
  204. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
  205. package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
  206. package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
  207. package/ide/vscode/package.json +1 -1
  208. 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
- // Keep the command constant and pass the target through PowerShell's
19
- // argument array. Paths are never interpolated into executable source.
20
- args: (target) => [
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 $args[0]',
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
- result = spawn(opener.command, opener.args(target), {
96
+ const spawnOptions = {
46
97
  encoding: 'utf8',
47
98
  shell: false,
48
99
  stdio: 'ignore',
49
- timeout: options.timeoutMs || 5000,
100
+ timeout: timeoutMs,
50
101
  windowsHide: true,
51
- });
52
- } catch {
53
- result = { error: new Error('opener threw') };
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 (result?.error?.code === 'ENOENT') status = 'unsupported';
58
- else if (result?.error || result?.signal || result?.status !== 0) status = 'failed';
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 { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
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' && source.meta.output) {
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>Technical Diagrams Live Preview</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">Live Preview</span>
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
- const stagingDirectory = fs.mkdtempSync(path.join(outputDirectory, '.archify-preview-'));
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
- fs.rmSync(stagingDirectory, { recursive: true, force: true });
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
- fs.rmSync(stagingDirectory, { recursive: true, force: true });
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
- 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
- }
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 { committed: false, supersededBy: currentSource };
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
- if (!sameArtifact || !outputMatches) fs.renameSync(candidatePath, outputPath);
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({ stage: 'commit', error: `Could not publish the verified preview: ${error.message}` }, '', '', candidatePath);
481
- return { committed: false, supersededBy: null };
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(candidatePath, receipt, generationHash));
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
- try { fs.rmSync(candidatePath, { force: true }); } catch {}
543
- try { fs.rmSync(snapshotPath, { force: true }); } catch {}
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
- watcher = fs.watch(path.dirname(inputPath), (event, filename) => {
593
- if (!filename || filename.toString() === path.basename(inputPath)) observeSource();
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;