@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.
Files changed (209) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +4 -4
  3. package/ai/skills/stacks-payments/SKILL.md +28 -0
  4. package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
  5. package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
  6. package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
  7. package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
  8. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
  9. package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
  10. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
  11. package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
  12. package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
  14. package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
  15. package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
  16. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
  17. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
  18. package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
  19. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
  20. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
  21. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
  22. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
  23. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
  24. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
  25. package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
  27. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
  28. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
  29. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
  30. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
  31. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
  32. package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
  33. package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
  34. package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
  35. package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
  36. package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
  37. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
  38. package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
  39. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
  40. package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
  41. package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
  42. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
  43. package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
  44. package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
  45. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
  46. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
  47. package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
  48. package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
  49. package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
  50. package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
  51. package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
  52. package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
  53. package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
  54. package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
  55. package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
  56. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
  57. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
  58. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
  59. package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
  60. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
  61. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
  62. package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
  63. package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
  64. package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
  65. package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
  66. package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
  67. package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
  68. package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
  69. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
  70. package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
  71. package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
  72. package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
  73. package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
  74. package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
  75. package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
  76. package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
  77. package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
  78. package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
  79. package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
  80. package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
  81. package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
  82. package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
  83. package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
  84. package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
  85. package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
  86. package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
  87. package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
  88. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
  89. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
  90. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
  91. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
  92. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
  93. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
  94. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
  95. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
  96. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
  97. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
  98. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
  99. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
  100. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
  101. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
  102. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
  103. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
  104. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
  105. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
  106. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
  107. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
  108. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
  109. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
  110. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
  111. package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
  112. package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
  113. package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
  114. package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
  115. package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
  116. package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
  117. package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
  118. package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
  119. package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
  120. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
  121. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
  122. package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
  123. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
  124. package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
  125. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
  126. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
  127. package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
  128. package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
  129. package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
  130. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
  131. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
  132. package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
  133. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
  134. package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
  135. package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
  136. package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
  137. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
  138. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
  139. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
  140. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
  141. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
  142. package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
  143. package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
  144. package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
  145. package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
  146. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
  147. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
  148. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
  149. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
  150. package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
  151. package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
  152. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
  153. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
  154. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
  155. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
  156. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
  157. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
  158. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
  159. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
  160. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
  161. package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
  162. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
  163. package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
  164. package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
  165. package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
  166. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
  167. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
  168. package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
  169. package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
  170. package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
  171. package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
  172. package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
  173. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
  174. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
  175. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
  176. package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
  177. package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
  178. package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
  179. package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
  180. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
  181. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
  182. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
  183. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
  184. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
  185. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
  186. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
  187. package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
  188. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
  189. package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
  190. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
  191. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
  192. package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
  193. package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
  194. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
  195. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
  196. package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
  197. package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
  198. package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
  199. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
  200. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
  201. package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
  202. package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
  203. package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
  204. package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
  205. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
  206. package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
  207. package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
  208. package/ide/vscode/package.json +1 -1
  209. package/package.json +2 -2
@@ -0,0 +1,1717 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from 'node:crypto';
4
+ import { constants as fsConstants } from 'node:fs';
5
+ import fs from 'node:fs/promises';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { performance } from 'node:perf_hooks';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ import {
12
+ DEFAULT_MANIFEST_URL,
13
+ SKILL_ID,
14
+ UpdateContractError,
15
+ compareSemver,
16
+ isStableCoreVersion,
17
+ parseSemver,
18
+ validateLocalRelease,
19
+ validateReleaseNotesUrl,
20
+ validateReleaseSummary,
21
+ validateStableUpdateManifest,
22
+ } from './update-contract.mjs';
23
+ import { sameEntry } from '../renderers/shared/path-semantics.mjs';
24
+
25
+ const OPERATION_STATE_FILE = 'state.json';
26
+ const OPERATION_OWNER_FILE = 'owner.json';
27
+ const ACTIVE_CLAIM_DIRECTORY = 'active-claim';
28
+ const OPERATION_NAME = /^(reserved|pending|committed|fenced|cancelled)-(\d{1,20})$/;
29
+ const MAX_OPERATION_GENERATION = (10n ** 20n) - 1n;
30
+ const CHECK_TTL_MS = 24 * 60 * 60 * 1_000;
31
+ const MAX_CACHE_HORIZON_MS = Math.ceil(CHECK_TTL_MS * 1.2);
32
+ const FIRST_FAILURE_DELAY_MS = 6 * 60 * 60 * 1_000;
33
+ const LATER_FAILURE_DELAY_MS = 24 * 60 * 60 * 1_000;
34
+ const DEFAULT_TIMEOUT_MS = 1_000;
35
+ const MAX_RESPONSE_BYTES = 32 * 1_024;
36
+ const MAX_LOCAL_RELEASE_BYTES = 4 * 1_024;
37
+ const MAX_CACHE_STATE_BYTES = 64 * 1_024;
38
+ const MAX_CLAIM_OWNER_BYTES = 1_024;
39
+ const LONGEST_ISO_TIMESTAMP = '+275760-09-13T00:00:00.000Z';
40
+ const LOCK_STALE_MS = 30_000;
41
+ const PREFERENCE_LOCK_WAIT_MS = 1_200;
42
+ const LOCK_RETRY_DELAY_MS = 20;
43
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
44
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
45
+ const defaultReleasePath = path.resolve(scriptDirectory, '..', 'skill-release.json');
46
+
47
+ class FileIdentityChangedError extends Error {}
48
+ class ManifestWithdrawnError extends Error {}
49
+
50
+ function silent(reason) {
51
+ return { status: 'silent', reason };
52
+ }
53
+
54
+ function updatesDisabled() {
55
+ return process.env.ARCHIFY_UPDATE_CHECK_DISABLED === '1';
56
+ }
57
+
58
+ function isPlainObject(value) {
59
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
60
+ }
61
+
62
+ function hasOnlyKeys(value, allowed) {
63
+ return Object.keys(value).every((key) => allowed.has(key));
64
+ }
65
+
66
+ function validateManifest(value) {
67
+ const manifest = validateStableUpdateManifest(value);
68
+ return {
69
+ version: manifest.version,
70
+ targetDigest: `sha256:${manifest.artifact.sha256}`,
71
+ severity: manifest.severity,
72
+ summary: manifest.summary,
73
+ releaseNotes: manifest.releaseNotes,
74
+ };
75
+ }
76
+
77
+ function validateCachedCandidate(value) {
78
+ if (!isPlainObject(value)
79
+ || !hasOnlyKeys(value, new Set([
80
+ 'version', 'targetDigest', 'severity', 'releaseNotes', 'checkedAt', 'summary',
81
+ ]))) return null;
82
+ try {
83
+ if (!isStableCoreVersion(value.version)) return null;
84
+ if (!DIGEST_PATTERN.test(value.targetDigest)
85
+ || !['normal', 'security'].includes(value.severity)) return null;
86
+ validateReleaseNotesUrl(value.releaseNotes, value.version);
87
+ if (Object.hasOwn(value, 'summary')) validateReleaseSummary(value.summary);
88
+ if (typeof value.checkedAt !== 'string' || !Number.isFinite(Date.parse(value.checkedAt))) return null;
89
+ return { ...value };
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function emptyState(installedVersion = null) {
96
+ return {
97
+ schemaVersion: 2,
98
+ skillId: SKILL_ID,
99
+ installedVersion,
100
+ check: {
101
+ nextCheckAt: null,
102
+ consecutiveFailures: 0,
103
+ },
104
+ preferences: [],
105
+ };
106
+ }
107
+
108
+ function validPreference(value) {
109
+ return isPlainObject(value)
110
+ && hasOnlyKeys(value, new Set(['targetDigest', 'version', 'mode', 'until']))
111
+ && DIGEST_PATTERN.test(value.targetDigest)
112
+ && isStableCoreVersion(value.version)
113
+ && ['ignore', 'snooze'].includes(value.mode)
114
+ && (value.mode === 'ignore' ? value.until === null
115
+ : typeof value.until === 'string' && Number.isFinite(Date.parse(value.until)));
116
+ }
117
+
118
+ function encodeBoundedState(state) {
119
+ const encoded = Buffer.from(`${JSON.stringify(state)}\n`, 'utf8');
120
+ return encoded.length <= MAX_CACHE_STATE_BYTES ? encoded : null;
121
+ }
122
+
123
+ function stateWithFailureCheck(state, nextCheckAt, consecutiveFailures, withdrawCandidate = false) {
124
+ const failedState = {
125
+ ...state,
126
+ check: { nextCheckAt, consecutiveFailures },
127
+ };
128
+ if (withdrawCandidate) delete failedState.candidate;
129
+ return failedState;
130
+ }
131
+
132
+ // A committed state must leave room for its own failure backoff.
133
+ function encodeRecoverableState(state) {
134
+ const encoded = encodeBoundedState(state);
135
+ if (!encoded || !encodeBoundedState(stateWithFailureCheck(state, LONGEST_ISO_TIMESTAMP, 2))) return null;
136
+ return encoded;
137
+ }
138
+
139
+ function versionCacheDirectory(cacheDirectory, installedVersion) {
140
+ const partition = crypto.createHash('sha256').update(installedVersion).digest('hex').slice(0, 24);
141
+ return path.join(cacheDirectory, `version-${partition}`);
142
+ }
143
+
144
+ function normalizeState(value, installedVersion) {
145
+ const effectiveInstalledVersion = installedVersion ?? value?.installedVersion ?? null;
146
+ if (!isPlainObject(value) || value.schemaVersion !== 2 || value.skillId !== SKILL_ID
147
+ || (installedVersion !== null && value.installedVersion !== installedVersion)
148
+ || (value.installedVersion !== null && typeof value.installedVersion !== 'string')
149
+ || !isPlainObject(value.check)
150
+ || !(value.check.nextCheckAt === null || typeof value.check.nextCheckAt === 'string')
151
+ || !Number.isSafeInteger(value.check.consecutiveFailures)
152
+ || value.check.consecutiveFailures < 0
153
+ || !Array.isArray(value.preferences) || value.preferences.length > 32
154
+ || value.preferences.some((preference) => !validPreference(preference))) return null;
155
+ if (effectiveInstalledVersion !== null) {
156
+ try {
157
+ parseSemver(effectiveInstalledVersion);
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+
163
+ const state = emptyState(effectiveInstalledVersion);
164
+ state.check.nextCheckAt = value.check.nextCheckAt;
165
+ state.check.consecutiveFailures = value.check.consecutiveFailures;
166
+ state.preferences = value.preferences.map((preference) => ({ ...preference }));
167
+ const hasCandidate = Object.hasOwn(value, 'candidate');
168
+ const candidate = validateCachedCandidate(value.candidate);
169
+ if (hasCandidate && !candidate) return null;
170
+ if (candidate) state.candidate = candidate;
171
+ return encodeRecoverableState(state) ? state : null;
172
+ }
173
+
174
+ function isSameFile(left, right) {
175
+ if (left.dev !== right.dev || left.ino !== right.ino) return false;
176
+ if (left.ino !== 0n) return true;
177
+ if (left.birthtimeNs === 0n || right.birthtimeNs === 0n) return false;
178
+ return left.birthtimeNs === right.birthtimeNs
179
+ && left.mode === right.mode;
180
+ }
181
+
182
+ function assertBoundedRegularFile(metadata, maxBytes) {
183
+ if (!metadata.isFile()
184
+ || metadata.isSymbolicLink()
185
+ || metadata.size > BigInt(maxBytes)) {
186
+ throw new Error('unsafe JSON file');
187
+ }
188
+ }
189
+
190
+ async function readJsonFile(target, maxBytes, expectedMetadata = null) {
191
+ const pathMetadata = await fs.lstat(target, { bigint: true });
192
+ if (expectedMetadata && !isSameFile(expectedMetadata, pathMetadata)) {
193
+ throw new FileIdentityChangedError('JSON file identity changed');
194
+ }
195
+ assertBoundedRegularFile(pathMetadata, maxBytes);
196
+ const flags = fsConstants.O_RDONLY
197
+ | (fsConstants.O_NOFOLLOW ?? 0)
198
+ | (fsConstants.O_NONBLOCK ?? 0);
199
+ const handle = await fs.open(target, flags);
200
+ try {
201
+ const handleMetadata = await handle.stat({ bigint: true });
202
+ const currentPathMetadata = await fs.lstat(target, { bigint: true });
203
+ assertBoundedRegularFile(handleMetadata, maxBytes);
204
+ assertBoundedRegularFile(currentPathMetadata, maxBytes);
205
+ if (!isSameFile(pathMetadata, handleMetadata)
206
+ || !isSameFile(currentPathMetadata, handleMetadata)) {
207
+ throw new FileIdentityChangedError('JSON file changed while opening');
208
+ }
209
+
210
+ const chunks = [];
211
+ let size = 0;
212
+ while (size <= maxBytes) {
213
+ const chunk = Buffer.allocUnsafe(Math.min(8 * 1_024, (maxBytes + 1) - size));
214
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, size);
215
+ if (bytesRead === 0) break;
216
+ chunks.push(chunk.subarray(0, bytesRead));
217
+ size += bytesRead;
218
+ }
219
+ if (size > maxBytes) throw new Error('JSON file is too large');
220
+ return JSON.parse(Buffer.concat(chunks, size).toString('utf8'));
221
+ } finally {
222
+ await handle.close();
223
+ }
224
+ }
225
+
226
+ function isWithinDirectory(directory, target) {
227
+ const relative = path.relative(directory, target);
228
+ return relative === ''
229
+ // path-contract-allow: lexical-capability -- No-follow snapshots bind this mutation boundary.
230
+ || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
231
+ }
232
+
233
+ async function physicalDirectorySuffix(directory, target) {
234
+ const absoluteTarget = path.resolve(target);
235
+ const parsed = path.parse(absoluteTarget);
236
+ const segments = absoluteTarget.slice(parsed.root.length).split(path.sep).filter(Boolean);
237
+ let prefix = parsed.root;
238
+ for (let index = 0; index < segments.length; index += 1) {
239
+ prefix = path.join(prefix, segments[index]);
240
+ const relation = sameEntry(directory, prefix);
241
+ if (relation.status === 'match') {
242
+ const metadata = await fs.lstat(prefix, { bigint: true });
243
+ if (metadata.isSymbolicLink()) {
244
+ throw new Error(`trusted cache prefix is a symbolic link or junction: ${prefix}`);
245
+ }
246
+ return segments.slice(index + 1);
247
+ }
248
+ if (relation.status === 'unknown' && relation.reason.code === 'entry-missing') break;
249
+ }
250
+ return null;
251
+ }
252
+
253
+ async function canonicalizeTrustedDirectoryPrefix(target) {
254
+ const absoluteTarget = path.resolve(target);
255
+ const trustedDirectories = [...new Set([os.homedir(), os.tmpdir()].map((directory) => (
256
+ path.resolve(directory)
257
+ )))].sort((left, right) => right.length - left.length);
258
+ for (const trustedDirectory of trustedDirectories) {
259
+ const suffix = await physicalDirectorySuffix(trustedDirectory, absoluteTarget);
260
+ if (suffix === null) continue;
261
+ try {
262
+ const canonicalDirectory = await fs.realpath(trustedDirectory);
263
+ return path.resolve(canonicalDirectory, ...suffix);
264
+ } catch {
265
+ // Fall back to validating the absolute path from its filesystem root.
266
+ }
267
+ }
268
+ return target;
269
+ }
270
+
271
+ function assertSafeDirectory(metadata) {
272
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
273
+ throw new Error('unsafe cache directory');
274
+ }
275
+ }
276
+
277
+ class CachePathChangedError extends Error {}
278
+ class CacheOperationRaceError extends Error {}
279
+ class OperationFencedError extends Error {}
280
+
281
+ const preparedCacheDirectories = new Map();
282
+
283
+ function invalidateCacheToken(token, message, cause) {
284
+ token.trusted = false;
285
+ if (cause instanceof CachePathChangedError) return cause;
286
+ const error = new CachePathChangedError(message);
287
+ if (cause) error.cause = cause;
288
+ return error;
289
+ }
290
+
291
+ async function verifyDirectorySnapshots(token, snapshots, description) {
292
+ if (!token.trusted) throw new CachePathChangedError('cache directory is no longer trusted');
293
+ try {
294
+ for (const snapshot of snapshots) {
295
+ const metadata = await fs.lstat(snapshot.directory, { bigint: true });
296
+ assertSafeDirectory(metadata);
297
+ if (!isSameFile(snapshot.metadata, metadata)) {
298
+ throw new Error(`${description} changed`);
299
+ }
300
+ }
301
+ } catch (error) {
302
+ throw invalidateCacheToken(token, `${description} changed`, error);
303
+ }
304
+ }
305
+
306
+ async function verifyCacheToken(token) {
307
+ await verifyDirectorySnapshots(token, token.ancestorSnapshots, 'cache directory');
308
+ }
309
+
310
+ async function guardedCacheRead(token, read) {
311
+ await verifyCacheToken(token);
312
+ let result;
313
+ try {
314
+ result = await read();
315
+ } catch (error) {
316
+ await verifyCacheToken(token);
317
+ throw error;
318
+ }
319
+ await verifyCacheToken(token);
320
+ return result;
321
+ }
322
+
323
+ function cacheTokenFor(cacheDirectory) {
324
+ const token = preparedCacheDirectories.get(path.resolve(cacheDirectory));
325
+ if (!token || !token.trusted) {
326
+ throw new CachePathChangedError('cache directory has not been prepared safely');
327
+ }
328
+ return token;
329
+ }
330
+
331
+ function resolveCacheTarget(token, target) {
332
+ const resolved = path.resolve(target);
333
+ const relative = path.relative(token.directory, resolved);
334
+ // This is a lexical capability check, not a physical-containment query. The
335
+ // async lstat snapshots below bind the prepared directory and every mutation
336
+ // parent across awaits; following a link here would weaken that no-link rule.
337
+ if (relative === '' || !isWithinDirectory(token.directory, resolved)) {
338
+ throw invalidateCacheToken(token, 'cache mutation escaped its prepared directory');
339
+ }
340
+ return resolved;
341
+ }
342
+
343
+ async function captureMutationParentSnapshots(token, targets) {
344
+ await verifyCacheToken(token);
345
+ const snapshots = new Map();
346
+ try {
347
+ for (const target of targets) {
348
+ const parent = path.dirname(resolveCacheTarget(token, target));
349
+ const relative = path.relative(token.directory, parent);
350
+ let current = token.directory;
351
+ for (const component of relative.split(path.sep).filter(Boolean)) {
352
+ current = path.join(current, component);
353
+ if (snapshots.has(current)) continue;
354
+ const metadata = await fs.lstat(current, { bigint: true });
355
+ assertSafeDirectory(metadata);
356
+ snapshots.set(current, { directory: current, metadata });
357
+ }
358
+ }
359
+ } catch (error) {
360
+ throw invalidateCacheToken(token, 'cache mutation parent changed', error);
361
+ }
362
+ const captured = [...snapshots.values()];
363
+ await verifyMutationParentSnapshots(token, captured);
364
+ await verifyCacheToken(token);
365
+ return captured;
366
+ }
367
+
368
+ async function verifyMutationParentSnapshots(token, snapshots) {
369
+ try {
370
+ for (const snapshot of snapshots) {
371
+ const metadata = await fs.lstat(snapshot.directory, { bigint: true });
372
+ assertSafeDirectory(metadata);
373
+ if (!isSameFile(snapshot.metadata, metadata)) {
374
+ throw new Error('cache mutation parent changed');
375
+ }
376
+ }
377
+ } catch (error) {
378
+ const raced = new CacheOperationRaceError('cache mutation parent changed');
379
+ raced.cause = error;
380
+ throw raced;
381
+ }
382
+ }
383
+
384
+ async function verifyMutationContext(token, parentSnapshots) {
385
+ await verifyCacheToken(token);
386
+ await verifyMutationParentSnapshots(token, parentSnapshots);
387
+ await verifyCacheToken(token);
388
+ }
389
+
390
+ function assertSafeCacheEntry(metadata) {
391
+ if (metadata.isSymbolicLink() || (!metadata.isDirectory() && !metadata.isFile())) {
392
+ throw new Error('unsafe cache entry');
393
+ }
394
+ }
395
+
396
+ function assertSafeRegularFile(metadata) {
397
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
398
+ throw new Error('unsafe cache file');
399
+ }
400
+ }
401
+
402
+ function assertRenameableCacheEntry(metadata) {
403
+ if (!metadata.isDirectory() && !metadata.isFile() && !metadata.isSymbolicLink()) {
404
+ throw new Error('unsafe cache rename entry');
405
+ }
406
+ }
407
+
408
+ async function stableCacheEntry(
409
+ token,
410
+ target,
411
+ validate,
412
+ expectedMetadata = null,
413
+ missingIsOperationRace = false,
414
+ ) {
415
+ const resolved = resolveCacheTarget(token, target);
416
+ try {
417
+ const first = await fs.lstat(resolved, { bigint: true });
418
+ validate(first);
419
+ if (expectedMetadata && !isSameFile(expectedMetadata, first)) {
420
+ throw new Error('cache entry identity changed');
421
+ }
422
+ await verifyCacheToken(token);
423
+ const second = await fs.lstat(resolved, { bigint: true });
424
+ validate(second);
425
+ if (!isSameFile(first, second)
426
+ || (expectedMetadata && !isSameFile(expectedMetadata, second))) {
427
+ throw new Error('cache entry identity changed');
428
+ }
429
+ return second;
430
+ } catch (error) {
431
+ if (missingIsOperationRace && error?.code === 'ENOENT') {
432
+ await verifyCacheToken(token);
433
+ throw error;
434
+ }
435
+ throw invalidateCacheToken(token, 'cache mutation produced an untrusted object', error);
436
+ }
437
+ }
438
+
439
+ async function guardedCacheEntryRead(
440
+ token,
441
+ target,
442
+ validate,
443
+ read,
444
+ expectedMetadata = null,
445
+ ) {
446
+ const metadata = await stableCacheEntry(
447
+ token,
448
+ target,
449
+ validate,
450
+ expectedMetadata,
451
+ true,
452
+ );
453
+ let result;
454
+ try {
455
+ result = await read(metadata);
456
+ } catch (error) {
457
+ await stableCacheEntry(token, target, validate, metadata, true);
458
+ throw error;
459
+ }
460
+ await stableCacheEntry(token, target, validate, metadata, true);
461
+ return result;
462
+ }
463
+
464
+ async function stablePreparedDirectory(token, target) {
465
+ try {
466
+ const first = await fs.lstat(target, { bigint: true });
467
+ assertSafeDirectory(first);
468
+ await verifyCacheToken(token);
469
+ const second = await fs.lstat(target, { bigint: true });
470
+ assertSafeDirectory(second);
471
+ if (!isSameFile(first, second)) throw new Error('prepared directory identity changed');
472
+ return second;
473
+ } catch (error) {
474
+ throw invalidateCacheToken(token, 'prepared directory could not be verified', error);
475
+ }
476
+ }
477
+
478
+ async function assertCacheTargetAbsent(token, target) {
479
+ const resolved = resolveCacheTarget(token, target);
480
+ try {
481
+ await fs.lstat(resolved, { bigint: true });
482
+ } catch (error) {
483
+ if (error?.code === 'ENOENT') return;
484
+ throw invalidateCacheToken(token, 'cache mutation target could not be verified', error);
485
+ }
486
+ throw invalidateCacheToken(token, 'cache mutation left an unexpected object');
487
+ }
488
+
489
+ async function guardedCacheMutation(token, parentSnapshots, {
490
+ mutate,
491
+ verifyBefore = null,
492
+ verifyAfter,
493
+ }) {
494
+ await verifyMutationContext(token, parentSnapshots);
495
+ if (verifyBefore) await verifyBefore();
496
+ let result;
497
+ try {
498
+ result = await mutate();
499
+ } catch (error) {
500
+ await verifyMutationContext(token, parentSnapshots);
501
+ throw error;
502
+ }
503
+ try {
504
+ await verifyMutationContext(token, parentSnapshots);
505
+ const verified = await verifyAfter(result);
506
+ await verifyMutationContext(token, parentSnapshots);
507
+ return verified ?? result;
508
+ } catch (error) {
509
+ if (error instanceof CacheOperationRaceError) throw error;
510
+ throw invalidateCacheToken(token, 'cache mutation could not be verified', error);
511
+ }
512
+ }
513
+
514
+ async function cacheMkdirWithToken(token, target, options, parentSnapshots = null) {
515
+ const resolved = path.resolve(target);
516
+ const parents = parentSnapshots ?? await captureMutationParentSnapshots(token, [resolved]);
517
+ return guardedCacheMutation(token, parents, {
518
+ mutate: () => fs.mkdir(resolved, options),
519
+ verifyAfter: () => (parentSnapshots === null
520
+ ? stableCacheEntry(token, resolved, assertSafeDirectory)
521
+ : stablePreparedDirectory(token, resolved)),
522
+ });
523
+ }
524
+
525
+ async function cacheMkdir(cacheDirectory, target, options) {
526
+ return cacheMkdirWithToken(cacheTokenFor(cacheDirectory), target, options);
527
+ }
528
+
529
+ async function cacheWriteFile(cacheDirectory, target, data, options) {
530
+ if (options?.flag !== 'wx') {
531
+ throw new Error('cache files must be created exclusively');
532
+ }
533
+ const token = cacheTokenFor(cacheDirectory);
534
+ const resolved = resolveCacheTarget(token, target);
535
+ const parents = await captureMutationParentSnapshots(token, [resolved]);
536
+ return guardedCacheMutation(token, parents, {
537
+ mutate: async () => {
538
+ const flags = fsConstants.O_WRONLY
539
+ | fsConstants.O_CREAT
540
+ | fsConstants.O_EXCL
541
+ | (fsConstants.O_NOFOLLOW ?? 0);
542
+ const handle = await fs.open(resolved, flags, options.mode);
543
+ try {
544
+ const openedMetadata = await handle.stat({ bigint: true });
545
+ if (!openedMetadata.isFile() || openedMetadata.isSymbolicLink()) {
546
+ throw new Error('cache write did not open a regular file');
547
+ }
548
+ await handle.writeFile(data, options.encoding ? { encoding: options.encoding } : undefined);
549
+ const writtenMetadata = await handle.stat({ bigint: true });
550
+ const expectedBytes = Buffer.isBuffer(data)
551
+ ? data.length
552
+ : Buffer.byteLength(data, options.encoding || 'utf8');
553
+ if (!isSameFile(openedMetadata, writtenMetadata)
554
+ || writtenMetadata.size !== BigInt(expectedBytes)) {
555
+ throw new Error('cache write did not preserve the opened file identity and size');
556
+ }
557
+ return writtenMetadata;
558
+ } finally {
559
+ await handle.close();
560
+ }
561
+ },
562
+ verifyAfter: (writtenMetadata) => stableCacheEntry(token, resolved, (metadata) => {
563
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
564
+ throw new Error('cache write did not create a regular file');
565
+ }
566
+ }, writtenMetadata),
567
+ });
568
+ }
569
+
570
+ async function cacheRename(
571
+ cacheDirectory,
572
+ source,
573
+ destination,
574
+ validate,
575
+ expectedSourceMetadata = null,
576
+ ) {
577
+ if (typeof validate !== 'function') {
578
+ throw new TypeError('cache rename requires an explicit entry validator');
579
+ }
580
+ const token = cacheTokenFor(cacheDirectory);
581
+ const resolvedSource = resolveCacheTarget(token, source);
582
+ const resolvedDestination = resolveCacheTarget(token, destination);
583
+ const parents = await captureMutationParentSnapshots(
584
+ token,
585
+ [resolvedSource, resolvedDestination],
586
+ );
587
+ const sourceMetadata = await stableCacheEntry(
588
+ token,
589
+ resolvedSource,
590
+ validate,
591
+ expectedSourceMetadata,
592
+ true,
593
+ );
594
+ return guardedCacheMutation(token, parents, {
595
+ verifyBefore: () => stableCacheEntry(
596
+ token,
597
+ resolvedSource,
598
+ validate,
599
+ sourceMetadata,
600
+ true,
601
+ ),
602
+ mutate: () => fs.rename(resolvedSource, resolvedDestination),
603
+ verifyAfter: async () => {
604
+ await assertCacheTargetAbsent(token, resolvedSource);
605
+ return stableCacheEntry(
606
+ token,
607
+ resolvedDestination,
608
+ validate,
609
+ sourceMetadata,
610
+ );
611
+ },
612
+ });
613
+ }
614
+
615
+ async function cacheRm(cacheDirectory, target, options) {
616
+ if (options?.recursive) {
617
+ throw new Error('recursive cache removal is not allowed');
618
+ }
619
+ const token = cacheTokenFor(cacheDirectory);
620
+ const resolved = resolveCacheTarget(token, target);
621
+ const parents = await captureMutationParentSnapshots(token, [resolved]);
622
+ let initialMetadata;
623
+ try {
624
+ initialMetadata = await fs.lstat(resolved, { bigint: true });
625
+ } catch (error) {
626
+ if (error?.code === 'ENOENT' && options?.force) {
627
+ await verifyMutationContext(token, parents);
628
+ return;
629
+ }
630
+ throw error;
631
+ }
632
+ try {
633
+ assertSafeCacheEntry(initialMetadata);
634
+ } catch (error) {
635
+ throw invalidateCacheToken(token, 'cache removal target is unsafe', error);
636
+ }
637
+ const targetMetadata = await stableCacheEntry(
638
+ token,
639
+ resolved,
640
+ assertSafeCacheEntry,
641
+ initialMetadata,
642
+ );
643
+ return guardedCacheMutation(token, parents, {
644
+ verifyBefore: () => stableCacheEntry(
645
+ token,
646
+ resolved,
647
+ assertSafeCacheEntry,
648
+ targetMetadata,
649
+ ),
650
+ mutate: () => fs.rm(resolved, options),
651
+ verifyAfter: () => assertCacheTargetAbsent(token, resolved),
652
+ });
653
+ }
654
+
655
+ async function prepareCacheDirectory(cacheDirectory) {
656
+ const absoluteTarget = path.resolve(cacheDirectory);
657
+ const target = await canonicalizeTrustedDirectoryPrefix(absoluteTarget);
658
+ const parsed = path.parse(target);
659
+ const components = target.slice(parsed.root.length).split(path.sep).filter(Boolean);
660
+ let current = parsed.root;
661
+ const token = {
662
+ directory: target,
663
+ ancestorSnapshots: [],
664
+ trusted: true,
665
+ };
666
+
667
+ for (const component of components) {
668
+ current = path.join(current, component);
669
+ let metadata;
670
+ try {
671
+ metadata = await fs.lstat(current, { bigint: true });
672
+ } catch (error) {
673
+ if (error?.code !== 'ENOENT') throw error;
674
+ try {
675
+ await cacheMkdirWithToken(token, current, { mode: 0o700 }, []);
676
+ } catch (mkdirError) {
677
+ if (mkdirError?.code !== 'EEXIST') throw mkdirError;
678
+ }
679
+ metadata = await fs.lstat(current, { bigint: true });
680
+ }
681
+ assertSafeDirectory(metadata);
682
+ token.ancestorSnapshots.push({ directory: current, metadata });
683
+ }
684
+
685
+ await verifyCacheToken(token);
686
+ preparedCacheDirectories.set(target, token);
687
+ return target;
688
+ }
689
+
690
+ function operationName(kind, generation) {
691
+ return `${kind}-${generation.toString().padStart(20, '0')}`;
692
+ }
693
+
694
+ function parseOperationName(name) {
695
+ const match = OPERATION_NAME.exec(name);
696
+ if (!match) return null;
697
+ return { kind: match[1], generation: BigInt(match[2]) };
698
+ }
699
+
700
+ async function listOperations(cacheDirectory, token = cacheTokenFor(cacheDirectory)) {
701
+ return guardedCacheRead(token, async () => {
702
+ try {
703
+ const entries = await fs.readdir(cacheDirectory, { withFileTypes: true });
704
+ return entries.flatMap((entry) => {
705
+ const parsed = parseOperationName(entry.name);
706
+ if (!parsed) return [];
707
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
708
+ throw invalidateCacheToken(
709
+ token,
710
+ `cache operation ${entry.name} is not a safe directory`,
711
+ );
712
+ }
713
+ return [{ ...parsed, name: entry.name, directory: path.join(cacheDirectory, entry.name) }];
714
+ });
715
+ } catch (error) {
716
+ if (error?.code === 'ENOENT') return [];
717
+ throw error;
718
+ }
719
+ });
720
+ }
721
+
722
+ async function readState(cacheDirectory, installedVersion = null) {
723
+ const token = cacheTokenFor(cacheDirectory);
724
+ return guardedCacheRead(token, async () => {
725
+ const operations = (await listOperations(cacheDirectory, token))
726
+ .filter((operation) => operation.kind === 'committed')
727
+ .sort((left, right) => (left.generation > right.generation ? -1 : 1));
728
+ for (const operation of operations) {
729
+ try {
730
+ const state = await guardedCacheEntryRead(
731
+ token,
732
+ operation.directory,
733
+ assertSafeDirectory,
734
+ async () => normalizeState(
735
+ await readJsonFile(
736
+ path.join(operation.directory, OPERATION_STATE_FILE),
737
+ MAX_CACHE_STATE_BYTES,
738
+ ),
739
+ installedVersion,
740
+ ),
741
+ );
742
+ if (state) return state;
743
+ } catch (error) {
744
+ if (error instanceof FileIdentityChangedError
745
+ || error instanceof CachePathChangedError
746
+ || error instanceof CacheOperationRaceError) throw error;
747
+ // An incomplete or corrupt generation is ignored in favor of the previous commit.
748
+ }
749
+ }
750
+ return emptyState(installedVersion);
751
+ });
752
+ }
753
+
754
+ async function operationIsActive(
755
+ cacheDirectory,
756
+ operation,
757
+ token = cacheTokenFor(cacheDirectory),
758
+ ) {
759
+ try {
760
+ return await guardedCacheEntryRead(
761
+ token,
762
+ operation.directory,
763
+ assertSafeDirectory,
764
+ async (directoryMetadata) => {
765
+ try {
766
+ const ownerMetadata = await fs.lstat(
767
+ path.join(operation.directory, OPERATION_OWNER_FILE),
768
+ { bigint: true },
769
+ );
770
+ if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink()) return false;
771
+ try {
772
+ const owner = await readJsonFile(
773
+ path.join(operation.directory, OPERATION_OWNER_FILE),
774
+ MAX_CLAIM_OWNER_BYTES,
775
+ ownerMetadata,
776
+ );
777
+ if (Number.isSafeInteger(owner?.pid) && owner.pid > 0 && processIsGone(owner.pid)) {
778
+ return false;
779
+ }
780
+ } catch (error) {
781
+ if (error instanceof FileIdentityChangedError) throw error;
782
+ // A malformed owner still gets the hard lease before recovery.
783
+ }
784
+ return metadataIsWithinLease(ownerMetadata);
785
+ } catch (error) {
786
+ if (error?.code !== 'ENOENT') throw error;
787
+ return metadataIsWithinLease(directoryMetadata);
788
+ }
789
+ },
790
+ );
791
+ } catch (error) {
792
+ if (error?.code === 'ENOENT') return false;
793
+ throw error;
794
+ }
795
+ }
796
+
797
+ function processIsGone(pid) {
798
+ try {
799
+ process.kill(pid, 0);
800
+ return false;
801
+ } catch (error) {
802
+ return error?.code === 'ESRCH';
803
+ }
804
+ }
805
+
806
+ function validateClaimOwner(value) {
807
+ if (!isPlainObject(value)
808
+ || !hasOnlyKeys(value, new Set(['pid', 'token', 'generation']))
809
+ || !Number.isSafeInteger(value.pid)
810
+ || value.pid <= 0
811
+ || typeof value.token !== 'string'
812
+ || !/^[a-f0-9]{32}$/.test(value.token)
813
+ || typeof value.generation !== 'string'
814
+ || !/^[1-9]\d*$/.test(value.generation)) return null;
815
+ return { ...value, generation: BigInt(value.generation) };
816
+ }
817
+
818
+ function metadataIsWithinLease(metadata) {
819
+ if (typeof metadata.mtimeNs === 'bigint') {
820
+ const ageNs = (BigInt(Date.now()) * 1_000_000n) - metadata.mtimeNs;
821
+ const absoluteAgeNs = ageNs < 0n ? -ageNs : ageNs;
822
+ return absoluteAgeNs <= BigInt(LOCK_STALE_MS) * 1_000_000n;
823
+ }
824
+ return Math.abs(Date.now() - metadata.mtimeMs) <= LOCK_STALE_MS;
825
+ }
826
+
827
+ function corruptClaimStatus(directory, directoryMetadata, ownerMetadata = directoryMetadata) {
828
+ if (metadataIsWithinLease(ownerMetadata)) {
829
+ return { state: 'busy', directory, metadata: directoryMetadata, owner: null };
830
+ }
831
+ const fingerprint = crypto.createHash('sha256')
832
+ .update(JSON.stringify({
833
+ directory: {
834
+ dev: directoryMetadata.dev.toString(),
835
+ ino: directoryMetadata.ino.toString(),
836
+ mode: directoryMetadata.mode.toString(),
837
+ birthtimeNs: directoryMetadata.birthtimeNs.toString(),
838
+ },
839
+ }))
840
+ .digest('hex')
841
+ .slice(0, 24);
842
+ return {
843
+ state: 'releasable',
844
+ directory,
845
+ metadata: directoryMetadata,
846
+ key: `corrupt-${fingerprint}`,
847
+ owner: null,
848
+ };
849
+ }
850
+
851
+ async function inspectActiveClaim(cacheDirectory) {
852
+ const token = cacheTokenFor(cacheDirectory);
853
+ return guardedCacheRead(token, async () => {
854
+ const directory = path.join(cacheDirectory, ACTIVE_CLAIM_DIRECTORY);
855
+ let directoryMetadata;
856
+ try {
857
+ directoryMetadata = await fs.lstat(directory, { bigint: true });
858
+ } catch (error) {
859
+ if (error?.code === 'ENOENT') return { state: 'absent', directory };
860
+ throw error;
861
+ }
862
+ if (!directoryMetadata.isDirectory() || directoryMetadata.isSymbolicLink()) {
863
+ return corruptClaimStatus(directory, directoryMetadata);
864
+ }
865
+ return guardedCacheEntryRead(
866
+ token,
867
+ directory,
868
+ assertSafeDirectory,
869
+ async (stableDirectoryMetadata) => {
870
+ const ownerPath = path.join(directory, OPERATION_OWNER_FILE);
871
+ let ownerMetadata = stableDirectoryMetadata;
872
+ let owner = null;
873
+ try {
874
+ ownerMetadata = await fs.lstat(ownerPath, { bigint: true });
875
+ if (!ownerMetadata.isFile()
876
+ || ownerMetadata.isSymbolicLink()
877
+ || ownerMetadata.size > BigInt(MAX_CLAIM_OWNER_BYTES)) {
878
+ return corruptClaimStatus(directory, stableDirectoryMetadata, ownerMetadata);
879
+ }
880
+ const ownerSource = await readJsonFile(
881
+ ownerPath,
882
+ MAX_CLAIM_OWNER_BYTES,
883
+ ownerMetadata,
884
+ );
885
+ owner = validateClaimOwner(ownerSource);
886
+ } catch (error) {
887
+ if (error instanceof FileIdentityChangedError) throw error;
888
+ // A malformed claim is recoverable only after the same hard lease as a crashed owner.
889
+ }
890
+
891
+ if (owner) {
892
+ const operations = await listOperations(cacheDirectory, token);
893
+ if (operations.some((operation) => operation.generation === owner.generation
894
+ && ['committed', 'fenced', 'cancelled'].includes(operation.kind))) {
895
+ return {
896
+ state: 'releasable',
897
+ directory,
898
+ metadata: stableDirectoryMetadata,
899
+ key: owner.generation.toString(),
900
+ owner,
901
+ };
902
+ }
903
+ const pending = operations.find((operation) => operation.kind === 'pending'
904
+ && operation.generation === owner.generation);
905
+ if (pending) {
906
+ if (await operationIsActive(cacheDirectory, pending, token)) {
907
+ return {
908
+ state: 'busy',
909
+ directory,
910
+ metadata: stableDirectoryMetadata,
911
+ owner,
912
+ };
913
+ }
914
+ return {
915
+ state: 'releasable',
916
+ directory,
917
+ metadata: stableDirectoryMetadata,
918
+ key: owner.generation.toString(),
919
+ owner,
920
+ };
921
+ }
922
+ }
923
+
924
+ return corruptClaimStatus(directory, stableDirectoryMetadata, ownerMetadata);
925
+ },
926
+ directoryMetadata,
927
+ );
928
+ });
929
+ }
930
+
931
+ async function retireActiveClaim(cacheDirectory, claim) {
932
+ const retirement = path.join(cacheDirectory, `retired-claim-${claim.key}`);
933
+ await cacheMkdir(cacheDirectory, retirement, { mode: 0o700 }).catch((error) => {
934
+ if (error?.code !== 'EEXIST') throw error;
935
+ });
936
+ try {
937
+ const retirementMetadata = await fs.lstat(retirement);
938
+ if (!retirementMetadata.isDirectory() || retirementMetadata.isSymbolicLink()) return false;
939
+ } catch (error) {
940
+ if (error instanceof CacheOperationRaceError) return false;
941
+ if (error?.code === 'ENOENT') return false;
942
+ throw error;
943
+ }
944
+ try {
945
+ const metadata = await fs.lstat(claim.directory);
946
+ if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
947
+ await cacheWriteFile(
948
+ cacheDirectory,
949
+ path.join(claim.directory, '.retirement-guard'),
950
+ `${claim.key}\n`,
951
+ { encoding: 'utf8', flag: 'wx', mode: 0o600 },
952
+ ).catch((error) => {
953
+ if (error?.code !== 'EEXIST') throw error;
954
+ });
955
+ }
956
+ } catch (error) {
957
+ if (error instanceof CacheOperationRaceError) return false;
958
+ if (error?.code === 'ENOENT') return false;
959
+ throw error;
960
+ }
961
+ try {
962
+ await cacheRename(
963
+ cacheDirectory,
964
+ claim.directory,
965
+ path.join(retirement, 'active'),
966
+ assertRenameableCacheEntry,
967
+ claim.metadata,
968
+ );
969
+ return true;
970
+ } catch (error) {
971
+ if (error instanceof CacheOperationRaceError) return false;
972
+ if (['ENOENT', 'EEXIST', 'ENOTEMPTY', 'EPERM'].includes(error?.code)) return false;
973
+ throw error;
974
+ }
975
+ }
976
+
977
+ async function prepareOperationClaim(cacheDirectory, operation) {
978
+ const directory = path.join(cacheDirectory, operationName('claim', operation.generation));
979
+ await cacheMkdir(cacheDirectory, directory, { mode: 0o700 });
980
+ await cacheWriteFile(
981
+ cacheDirectory,
982
+ path.join(directory, OPERATION_OWNER_FILE),
983
+ `${JSON.stringify({
984
+ pid: process.pid,
985
+ token: operation.token,
986
+ generation: operation.generation.toString(),
987
+ })}\n`,
988
+ { encoding: 'utf8', flag: 'wx', mode: 0o600 },
989
+ );
990
+ operation.preparedClaimDirectory = directory;
991
+ }
992
+
993
+ async function discardPreparedClaim(cacheDirectory, operation) {
994
+ if (!operation.preparedClaimDirectory) return;
995
+ const discarded = path.join(
996
+ cacheDirectory,
997
+ `discarded-claim-${operation.generation.toString().padStart(20, '0')}-${operation.token}`,
998
+ );
999
+ try {
1000
+ await cacheRename(
1001
+ cacheDirectory,
1002
+ operation.preparedClaimDirectory,
1003
+ discarded,
1004
+ assertSafeDirectory,
1005
+ );
1006
+ } catch (error) {
1007
+ if (error?.code !== 'ENOENT') throw error;
1008
+ }
1009
+ delete operation.preparedClaimDirectory;
1010
+ }
1011
+
1012
+ async function promoteOperationClaim(cacheDirectory, operation) {
1013
+ await prepareOperationClaim(cacheDirectory, operation);
1014
+ const activeDirectory = path.join(cacheDirectory, ACTIVE_CLAIM_DIRECTORY);
1015
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1016
+ if (await operationIsSuperseded(cacheDirectory, operation)) {
1017
+ await discardPreparedClaim(cacheDirectory, operation);
1018
+ return false;
1019
+ }
1020
+ const active = await inspectActiveClaim(cacheDirectory);
1021
+ if (active.state === 'busy') {
1022
+ await discardPreparedClaim(cacheDirectory, operation);
1023
+ return false;
1024
+ }
1025
+ if (active.state === 'releasable') {
1026
+ if (active.owner?.generation > operation.generation) {
1027
+ await discardPreparedClaim(cacheDirectory, operation);
1028
+ return false;
1029
+ }
1030
+ await retireActiveClaim(cacheDirectory, active);
1031
+ continue;
1032
+ }
1033
+ try {
1034
+ await cacheRename(
1035
+ cacheDirectory,
1036
+ operation.preparedClaimDirectory,
1037
+ activeDirectory,
1038
+ assertSafeDirectory,
1039
+ );
1040
+ delete operation.preparedClaimDirectory;
1041
+ if (!await operationIsActive(cacheDirectory, operation)) return false;
1042
+ return true;
1043
+ } catch (promotionError) {
1044
+ const raced = await inspectActiveClaim(cacheDirectory);
1045
+ if (raced.state === 'absent') {
1046
+ if (['EEXIST', 'ENOTEMPTY', 'EPERM'].includes(promotionError?.code)) continue;
1047
+ await discardPreparedClaim(cacheDirectory, operation);
1048
+ throw promotionError;
1049
+ }
1050
+ continue;
1051
+ }
1052
+ }
1053
+ await discardPreparedClaim(cacheDirectory, operation);
1054
+ return false;
1055
+ }
1056
+
1057
+ async function operationOwnsActiveClaim(cacheDirectory, operation) {
1058
+ const active = await inspectActiveClaim(cacheDirectory);
1059
+ return active.state === 'busy'
1060
+ && active.owner?.generation === operation.generation
1061
+ && active.owner.token === operation.token;
1062
+ }
1063
+
1064
+ async function hasActivePendingOperation(cacheDirectory) {
1065
+ const token = cacheTokenFor(cacheDirectory);
1066
+ return guardedCacheRead(token, async () => {
1067
+ const pending = (await listOperations(cacheDirectory, token))
1068
+ .filter((operation) => operation.kind === 'pending');
1069
+ return (await Promise.all(pending.map(
1070
+ (operation) => operationIsActive(cacheDirectory, operation, token),
1071
+ ))).some(Boolean);
1072
+ });
1073
+ }
1074
+
1075
+ async function reserveOperation(cacheDirectory) {
1076
+ cacheDirectory = await prepareCacheDirectory(cacheDirectory);
1077
+ const operations = await listOperations(cacheDirectory);
1078
+ const occupied = new Set(operations.map((operation) => operation.generation.toString()));
1079
+ let generation = 1n;
1080
+ while (true) {
1081
+ while (occupied.has(generation.toString())) generation += 1n;
1082
+ if (generation > MAX_OPERATION_GENERATION) throw new Error('operation generation space exhausted');
1083
+ const reservation = path.join(cacheDirectory, operationName('reserved', generation));
1084
+ try {
1085
+ await cacheMkdir(cacheDirectory, reservation, { mode: 0o700 });
1086
+ } catch (error) {
1087
+ if (error?.code !== 'EEXIST') throw error;
1088
+ occupied.add(generation.toString());
1089
+ generation += 1n;
1090
+ continue;
1091
+ }
1092
+ const directory = path.join(cacheDirectory, operationName('pending', generation));
1093
+ try {
1094
+ await cacheMkdir(cacheDirectory, directory, { mode: 0o700 });
1095
+ const operation = {
1096
+ generation,
1097
+ token: crypto.randomBytes(16).toString('hex'),
1098
+ directory,
1099
+ };
1100
+ try {
1101
+ await cacheWriteFile(
1102
+ cacheDirectory,
1103
+ path.join(directory, OPERATION_OWNER_FILE),
1104
+ `${JSON.stringify({ pid: process.pid, token: operation.token })}\n`,
1105
+ { encoding: 'utf8', flag: 'wx', mode: 0o600 },
1106
+ );
1107
+ } catch (error) {
1108
+ if (await operationWasFenced(cacheDirectory, operation)) {
1109
+ const fencedError = new OperationFencedError('operation was fenced while reserving');
1110
+ fencedError.cause = error;
1111
+ throw fencedError;
1112
+ }
1113
+ await cacheRename(
1114
+ cacheDirectory,
1115
+ directory,
1116
+ path.join(cacheDirectory, operationName('cancelled', generation)),
1117
+ assertSafeDirectory,
1118
+ ).catch(() => {});
1119
+ throw error;
1120
+ }
1121
+ return operation;
1122
+ } catch (error) {
1123
+ if (error?.code !== 'EEXIST') throw error;
1124
+ occupied.add(generation.toString());
1125
+ generation += 1n;
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
+ async function operationIsSuperseded(cacheDirectory, operation) {
1131
+ return (await listOperations(cacheDirectory))
1132
+ .some((candidate) => candidate.generation > operation.generation);
1133
+ }
1134
+
1135
+ async function operationWasFenced(cacheDirectory, operation) {
1136
+ const token = cacheTokenFor(cacheDirectory);
1137
+ return guardedCacheRead(token, async () => {
1138
+ try {
1139
+ await fs.lstat(operation.directory, { bigint: true });
1140
+ return false;
1141
+ } catch (error) {
1142
+ if (error?.code !== 'ENOENT') throw error;
1143
+ }
1144
+ const fencedDirectory = path.join(
1145
+ cacheDirectory,
1146
+ operationName('fenced', operation.generation),
1147
+ );
1148
+ try {
1149
+ await stableCacheEntry(
1150
+ token,
1151
+ fencedDirectory,
1152
+ assertSafeDirectory,
1153
+ null,
1154
+ true,
1155
+ );
1156
+ return true;
1157
+ } catch (error) {
1158
+ if (error?.code === 'ENOENT') return false;
1159
+ throw error;
1160
+ }
1161
+ });
1162
+ }
1163
+
1164
+ async function transitionOperation(cacheDirectory, operation, kind) {
1165
+ const destination = path.join(cacheDirectory, operationName(kind, operation.generation));
1166
+ await cacheRename(cacheDirectory, operation.directory, destination, assertSafeDirectory);
1167
+ }
1168
+
1169
+ async function cancelOperation(cacheDirectory, operation) {
1170
+ try {
1171
+ await transitionOperation(cacheDirectory, operation, 'cancelled');
1172
+ } catch (error) {
1173
+ // A higher generation may already have fenced this unique pending directory.
1174
+ if (error?.code !== 'ENOENT') throw error;
1175
+ }
1176
+ }
1177
+
1178
+ async function acquireOperation(cacheDirectory) {
1179
+ cacheDirectory = await prepareCacheDirectory(cacheDirectory);
1180
+ if ((await inspectActiveClaim(cacheDirectory)).state === 'busy') return null;
1181
+ if (await hasActivePendingOperation(cacheDirectory)) return null;
1182
+ let operation;
1183
+ try {
1184
+ operation = await reserveOperation(cacheDirectory);
1185
+ } catch (error) {
1186
+ if (error instanceof OperationFencedError) return null;
1187
+ throw error;
1188
+ }
1189
+ if (!await operationIsSuperseded(cacheDirectory, operation)
1190
+ && await promoteOperationClaim(cacheDirectory, operation)) return operation;
1191
+ await cancelOperation(cacheDirectory, operation);
1192
+ return null;
1193
+ }
1194
+
1195
+ async function fenceLowerOperations(cacheDirectory, operation) {
1196
+ const lower = (await listOperations(cacheDirectory))
1197
+ .filter((candidate) => candidate.kind === 'pending'
1198
+ && candidate.generation < operation.generation)
1199
+ .sort((left, right) => (left.generation < right.generation ? -1 : 1));
1200
+ for (const candidate of lower) {
1201
+ try {
1202
+ await cacheRename(
1203
+ cacheDirectory,
1204
+ candidate.directory,
1205
+ path.join(cacheDirectory, operationName('fenced', candidate.generation)),
1206
+ assertSafeDirectory,
1207
+ );
1208
+ } catch (error) {
1209
+ if (error?.code !== 'ENOENT') throw error;
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ async function commitOperation(cacheDirectory, operation, installedVersion, mutate) {
1215
+ if (!await operationOwnsActiveClaim(cacheDirectory, operation)) {
1216
+ await cancelOperation(cacheDirectory, operation);
1217
+ return { fenced: true, result: silent('check-in-progress') };
1218
+ }
1219
+ await fenceLowerOperations(cacheDirectory, operation);
1220
+ if (!await operationOwnsActiveClaim(cacheDirectory, operation)) {
1221
+ await cancelOperation(cacheDirectory, operation);
1222
+ return { fenced: true, result: silent('check-in-progress') };
1223
+ }
1224
+ const state = await readState(cacheDirectory, installedVersion);
1225
+ const decision = mutate(state);
1226
+ if (!decision.state) {
1227
+ await cancelOperation(cacheDirectory, operation);
1228
+ return { fenced: false, result: decision.result };
1229
+ }
1230
+ let committedState = decision.state;
1231
+ let committedResult = decision.result;
1232
+ let encodedState = encodeRecoverableState(committedState);
1233
+ if (!encodedState && decision.capacityFallback) {
1234
+ committedState = decision.capacityFallback.state;
1235
+ committedResult = decision.capacityFallback.result;
1236
+ encodedState = encodeRecoverableState(committedState);
1237
+ }
1238
+ if (!encodedState) {
1239
+ await cancelOperation(cacheDirectory, operation);
1240
+ return { fenced: false, result: silent('cache-unavailable') };
1241
+ }
1242
+ const temporary = path.join(
1243
+ operation.directory,
1244
+ `.state.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`,
1245
+ );
1246
+ try {
1247
+ await cacheWriteFile(cacheDirectory, temporary, encodedState, {
1248
+ flag: 'wx',
1249
+ mode: 0o600,
1250
+ });
1251
+ await cacheRename(
1252
+ cacheDirectory,
1253
+ temporary,
1254
+ path.join(operation.directory, OPERATION_STATE_FILE),
1255
+ assertSafeRegularFile,
1256
+ );
1257
+ await transitionOperation(cacheDirectory, operation, 'committed');
1258
+ return { fenced: false, result: committedResult };
1259
+ } catch (error) {
1260
+ if (await operationWasFenced(cacheDirectory, operation)) {
1261
+ return { fenced: true, result: silent('check-in-progress') };
1262
+ }
1263
+ await cacheRm(cacheDirectory, temporary, { force: true }).catch(() => {});
1264
+ throw error;
1265
+ }
1266
+ }
1267
+
1268
+ function nextSuccessfulCheck(nowMs, random) {
1269
+ const boundedRandom = Math.min(1, Math.max(0, Number(random()) || 0));
1270
+ const multiplier = 0.8 + (boundedRandom * 0.4);
1271
+ return new Date(nowMs + Math.round(CHECK_TTL_MS * multiplier)).toISOString();
1272
+ }
1273
+
1274
+ function nextFailedCheck(nowMs, failures) {
1275
+ const delay = failures <= 1 ? FIRST_FAILURE_DELAY_MS : LATER_FAILURE_DELAY_MS;
1276
+ return new Date(nowMs + delay).toISOString();
1277
+ }
1278
+
1279
+ function stateAfterFailedCheck(state, nowMs, withdrawCandidate = false) {
1280
+ const failures = Math.min(state.check.consecutiveFailures + 1, 2);
1281
+ return stateWithFailureCheck(
1282
+ state,
1283
+ nextFailedCheck(nowMs, failures),
1284
+ failures,
1285
+ withdrawCandidate,
1286
+ );
1287
+ }
1288
+
1289
+ function eventKeyForDigest(targetDigest) {
1290
+ return `${SKILL_ID}@${targetDigest}`;
1291
+ }
1292
+
1293
+ function digestForEventKey(eventKey) {
1294
+ const prefix = `${SKILL_ID}@`;
1295
+ if (typeof eventKey !== 'string' || !eventKey.startsWith(prefix)) return null;
1296
+ const digest = eventKey.slice(prefix.length);
1297
+ return DIGEST_PATTERN.test(digest) ? digest : null;
1298
+ }
1299
+
1300
+ function notification(localRelease, candidate, source) {
1301
+ return {
1302
+ status: 'update_available',
1303
+ eventKey: eventKeyForDigest(candidate.targetDigest),
1304
+ installedVersion: localRelease.version,
1305
+ latestVersion: candidate.version,
1306
+ targetDigest: candidate.targetDigest,
1307
+ severity: candidate.severity,
1308
+ summary: candidate.summary ?? null,
1309
+ releaseNotes: candidate.releaseNotes,
1310
+ source,
1311
+ checkedAt: candidate.checkedAt,
1312
+ noticeRequired: true,
1313
+ };
1314
+ }
1315
+
1316
+ function resultForCandidate(localRelease, state, { source = 'cache', nowMs = Date.now() } = {}) {
1317
+ const { candidate } = state;
1318
+ if (!candidate) return silent('cache-valid');
1319
+ if (compareSemver(candidate.version, localRelease.version) <= 0) {
1320
+ return {
1321
+ status: 'current', installedVersion: localRelease.version,
1322
+ availableVersion: candidate.version, checkedAt: candidate.checkedAt,
1323
+ source, noticeRequired: false,
1324
+ };
1325
+ }
1326
+ const result = notification(localRelease, candidate, source);
1327
+ const preference = state.preferences?.find((entry) => entry.targetDigest === candidate.targetDigest
1328
+ && entry.version === candidate.version);
1329
+ if (preference?.mode === 'ignore') return { ...result, noticeRequired: false, reason: 'ignored' };
1330
+ if (preference?.mode === 'snooze' && Date.parse(preference.until) > nowMs) {
1331
+ return { ...result, noticeRequired: false, reason: 'snoozed', suppressedUntil: preference.until };
1332
+ }
1333
+ return result;
1334
+ }
1335
+
1336
+ async function readBoundedBody(response) {
1337
+ const contentLength = Number(response.headers.get('content-length'));
1338
+ if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
1339
+ await cancelResponseBody(response);
1340
+ throw new UpdateContractError('manifest response is too large');
1341
+ }
1342
+ if (!response.body || typeof response.body.getReader !== 'function') {
1343
+ throw new UpdateContractError('manifest response has no bounded stream');
1344
+ }
1345
+ const reader = response.body.getReader();
1346
+ const chunks = [];
1347
+ let size = 0;
1348
+ try {
1349
+ while (true) {
1350
+ const { done, value } = await reader.read();
1351
+ if (done) break;
1352
+ size += value.byteLength;
1353
+ if (size > MAX_RESPONSE_BYTES) {
1354
+ await reader.cancel().catch(() => {});
1355
+ throw new UpdateContractError('manifest response is too large');
1356
+ }
1357
+ chunks.push(value);
1358
+ }
1359
+ } finally {
1360
+ reader.releaseLock();
1361
+ }
1362
+ const joined = new Uint8Array(size);
1363
+ let offset = 0;
1364
+ for (const chunk of chunks) {
1365
+ joined.set(chunk, offset);
1366
+ offset += chunk.byteLength;
1367
+ }
1368
+ try {
1369
+ return new TextDecoder('utf-8', { fatal: true }).decode(joined);
1370
+ } catch {
1371
+ throw new UpdateContractError('manifest is not valid UTF-8');
1372
+ }
1373
+ }
1374
+
1375
+ async function cancelResponseBody(response) {
1376
+ try {
1377
+ if (response.body && typeof response.body.cancel === 'function') {
1378
+ await response.body.cancel();
1379
+ }
1380
+ } catch {
1381
+ // Network cleanup is best-effort; the original response classification wins.
1382
+ }
1383
+ }
1384
+
1385
+ async function fetchCandidate({ fetchImpl, manifestUrl, timeoutMs }) {
1386
+ if (manifestUrl !== DEFAULT_MANIFEST_URL) throw new UpdateContractError('unexpected manifest URL');
1387
+ const controller = new AbortController();
1388
+ let timer;
1389
+ const timeout = new Promise((_resolve, reject) => {
1390
+ timer = setTimeout(() => {
1391
+ controller.abort();
1392
+ reject(new Error('update check timed out'));
1393
+ }, timeoutMs);
1394
+ });
1395
+ const request = (async () => {
1396
+ const headers = { accept: 'application/json' };
1397
+ const response = await fetchImpl(manifestUrl, {
1398
+ method: 'GET',
1399
+ headers,
1400
+ redirect: 'error',
1401
+ signal: controller.signal,
1402
+ });
1403
+ if (response.status !== 200) {
1404
+ await cancelResponseBody(response);
1405
+ if (response.status === 404 || response.status === 410) {
1406
+ throw new ManifestWithdrawnError('manifest was withdrawn');
1407
+ }
1408
+ throw new Error(`manifest returned HTTP ${response.status}`);
1409
+ }
1410
+ const mediaType = response.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase();
1411
+ if (mediaType !== 'application/json') {
1412
+ await cancelResponseBody(response);
1413
+ throw new UpdateContractError('manifest is not JSON');
1414
+ }
1415
+ const source = await readBoundedBody(response);
1416
+ let decoded;
1417
+ try {
1418
+ decoded = JSON.parse(source);
1419
+ } catch {
1420
+ throw new UpdateContractError('manifest is not valid bounded JSON');
1421
+ }
1422
+ return { candidate: validateManifest(decoded) };
1423
+ })();
1424
+ try {
1425
+ return await Promise.race([request, timeout]);
1426
+ } finally {
1427
+ clearTimeout(timer);
1428
+ }
1429
+ }
1430
+
1431
+ function defaultCacheDirectory() {
1432
+ const homeDirectory = os.homedir();
1433
+ if (process.platform === 'win32') {
1434
+ const localData = process.env.LOCALAPPDATA;
1435
+ return path.join(localData && path.isAbsolute(localData) ? localData : path.join(homeDirectory, 'AppData', 'Local'), 'archify-skill');
1436
+ }
1437
+ if (process.platform === 'darwin') return path.join(homeDirectory, 'Library', 'Caches', 'archify-skill');
1438
+ const xdgCache = process.env.XDG_CACHE_HOME;
1439
+ return path.join(xdgCache && path.isAbsolute(xdgCache) ? xdgCache : path.join(homeDirectory, '.cache'), 'archify-skill');
1440
+ }
1441
+
1442
+ function freshStateResult(localRelease, state, nowMs) {
1443
+ const nextCheckAt = Date.parse(state.check.nextCheckAt || '');
1444
+ if (Number.isFinite(nextCheckAt) && nextCheckAt > nowMs
1445
+ && nextCheckAt <= nowMs + MAX_CACHE_HORIZON_MS) {
1446
+ return resultForCandidate(localRelease, state, { nowMs });
1447
+ }
1448
+ return null;
1449
+ }
1450
+
1451
+ async function resultAfterLosingOperation(cacheDirectory, operation, localRelease, nowMs) {
1452
+ await cancelOperation(cacheDirectory, operation);
1453
+ const cached = await readState(cacheDirectory, localRelease.version);
1454
+ return cached.candidate ? resultForCandidate(localRelease, cached, { nowMs }) : silent('check-in-progress');
1455
+ }
1456
+
1457
+ async function waitUntilRetry(deadline, monotonicNow) {
1458
+ let remaining;
1459
+ try {
1460
+ remaining = deadline - Number(monotonicNow());
1461
+ } catch {
1462
+ return false;
1463
+ }
1464
+ if (!Number.isFinite(remaining) || remaining <= 0) return false;
1465
+ await new Promise((resolve) => setTimeout(
1466
+ resolve,
1467
+ Math.min(LOCK_RETRY_DELAY_MS, remaining),
1468
+ ));
1469
+ return true;
1470
+ }
1471
+
1472
+ export async function checkForUpdate({
1473
+ releasePath = defaultReleasePath,
1474
+ cacheDirectory = defaultCacheDirectory(),
1475
+ fetchImpl = globalThis.fetch,
1476
+ now = Date.now,
1477
+ random = Math.random,
1478
+ timeoutMs = DEFAULT_TIMEOUT_MS,
1479
+ fetchDeadlineNs,
1480
+ } = {}) {
1481
+ if (updatesDisabled()) return silent('disabled');
1482
+ if (typeof fetchImpl !== 'function') return silent('runtime-unavailable');
1483
+ let localRelease;
1484
+ try {
1485
+ localRelease = validateLocalRelease(await readJsonFile(releasePath, MAX_LOCAL_RELEASE_BYTES));
1486
+ } catch {
1487
+ return silent('invalid-local-release');
1488
+ }
1489
+
1490
+ const nowMs = Number(now());
1491
+ if (!Number.isFinite(nowMs)) return silent('invalid-clock');
1492
+ let stateDirectory = versionCacheDirectory(cacheDirectory, localRelease.version);
1493
+ let state;
1494
+ try {
1495
+ stateDirectory = await prepareCacheDirectory(stateDirectory);
1496
+ state = await readState(stateDirectory, localRelease.version);
1497
+ } catch {
1498
+ return silent('cache-unavailable');
1499
+ }
1500
+ const cachedResult = freshStateResult(localRelease, state, nowMs);
1501
+ if (cachedResult) return cachedResult;
1502
+
1503
+ let operation;
1504
+ try {
1505
+ operation = await acquireOperation(stateDirectory);
1506
+ } catch {
1507
+ return silent('cache-unavailable');
1508
+ }
1509
+ if (!operation) {
1510
+ try {
1511
+ const cached = await readState(stateDirectory, localRelease.version);
1512
+ return cached.candidate ? resultForCandidate(localRelease, cached, { nowMs }) : silent('check-in-progress');
1513
+ } catch {
1514
+ return silent('cache-unavailable');
1515
+ }
1516
+ }
1517
+
1518
+ try {
1519
+ state = await readState(stateDirectory, localRelease.version);
1520
+ const racedResult = freshStateResult(localRelease, state, nowMs);
1521
+ if (racedResult) {
1522
+ await cancelOperation(stateDirectory, operation);
1523
+ return racedResult;
1524
+ }
1525
+
1526
+ if (!await operationOwnsActiveClaim(stateDirectory, operation)) {
1527
+ return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs);
1528
+ }
1529
+
1530
+ await verifyCacheToken(cacheTokenFor(stateDirectory));
1531
+ if (!await operationOwnsActiveClaim(stateDirectory, operation)) {
1532
+ return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs);
1533
+ }
1534
+ let fetched;
1535
+ try {
1536
+ const remainingFetchMs = fetchDeadlineNs === undefined ? timeoutMs
1537
+ : Math.min(timeoutMs, Number(fetchDeadlineNs - process.hrtime.bigint()) / 1_000_000);
1538
+ if (remainingFetchMs <= 0) throw new Error('update fetch deadline expired during cache preparation');
1539
+ fetched = await fetchCandidate({
1540
+ fetchImpl,
1541
+ manifestUrl: localRelease.updateManifestUrl,
1542
+ timeoutMs: remainingFetchMs,
1543
+ });
1544
+ } catch (error) {
1545
+ const withdrawn = error instanceof ManifestWithdrawnError;
1546
+ const reason = withdrawn ? 'withdrawn'
1547
+ : error instanceof UpdateContractError ? 'invalid-manifest' : 'check-failed';
1548
+ // Offline or invalid checks keep repeating the last validated candidate.
1549
+ const failedResult = (known) => (!withdrawn && known.candidate
1550
+ ? resultForCandidate(localRelease, known, { nowMs }) : silent(reason));
1551
+ try {
1552
+ const committed = await commitOperation(
1553
+ stateDirectory,
1554
+ operation,
1555
+ localRelease.version,
1556
+ (current) => ({
1557
+ state: stateAfterFailedCheck(current, nowMs, withdrawn),
1558
+ result: failedResult(current),
1559
+ }),
1560
+ );
1561
+ if (committed.fenced) {
1562
+ return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs);
1563
+ }
1564
+ return committed.result;
1565
+ } catch {
1566
+ await cancelOperation(stateDirectory, operation).catch(() => {});
1567
+ return failedResult(state);
1568
+ }
1569
+ }
1570
+
1571
+ const candidate = { ...fetched.candidate, checkedAt: new Date(nowMs).toISOString() };
1572
+ try {
1573
+ const committed = await commitOperation(
1574
+ stateDirectory,
1575
+ operation,
1576
+ localRelease.version,
1577
+ (current) => {
1578
+ const next = {
1579
+ ...current,
1580
+ candidate,
1581
+ check: {
1582
+ nextCheckAt: nextSuccessfulCheck(nowMs, random),
1583
+ consecutiveFailures: 0,
1584
+ },
1585
+ };
1586
+ const result = resultForCandidate(localRelease, next, { source: 'network', nowMs });
1587
+ return {
1588
+ state: next,
1589
+ result,
1590
+ capacityFallback: { state: stateAfterFailedCheck(current, nowMs, true), result },
1591
+ };
1592
+ },
1593
+ );
1594
+ if (committed.fenced) {
1595
+ return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs);
1596
+ }
1597
+ return committed.result;
1598
+ } catch {
1599
+ await cancelOperation(stateDirectory, operation).catch(() => {});
1600
+ return resultForCandidate(localRelease, { ...state, candidate }, { source: 'network', nowMs });
1601
+ }
1602
+ } catch {
1603
+ await cancelOperation(stateDirectory, operation).catch(() => {});
1604
+ return silent('cache-unavailable');
1605
+ }
1606
+ }
1607
+
1608
+ export async function setUpdatePreference({
1609
+ releasePath = defaultReleasePath,
1610
+ cacheDirectory = defaultCacheDirectory(),
1611
+ eventKey,
1612
+ mode,
1613
+ days = 7,
1614
+ now = Date.now,
1615
+ monotonicNow = () => performance.now(),
1616
+ } = {}) {
1617
+ const targetDigest = digestForEventKey(eventKey);
1618
+ if (!targetDigest || !['snooze', 'ignore'].includes(mode)
1619
+ || (mode === 'snooze' && (!Number.isInteger(days) || days < 1 || days > 365))) {
1620
+ return silent('invalid-preference');
1621
+ }
1622
+ let localRelease;
1623
+ try {
1624
+ localRelease = validateLocalRelease(await readJsonFile(releasePath, MAX_LOCAL_RELEASE_BYTES));
1625
+ } catch {
1626
+ return silent('invalid-local-release');
1627
+ }
1628
+ let stateDirectory = versionCacheDirectory(cacheDirectory, localRelease.version);
1629
+ try {
1630
+ stateDirectory = await prepareCacheDirectory(stateDirectory);
1631
+ } catch {
1632
+ return silent('cache-unavailable');
1633
+ }
1634
+ const nowMs = Number(now());
1635
+ if (!Number.isFinite(nowMs)) return silent('invalid-clock');
1636
+ const deadline = Number(monotonicNow()) + PREFERENCE_LOCK_WAIT_MS;
1637
+ while (Number(monotonicNow()) < deadline) {
1638
+ let operation;
1639
+ try {
1640
+ operation = await acquireOperation(stateDirectory);
1641
+ } catch {
1642
+ return silent('cache-unavailable');
1643
+ }
1644
+ if (!operation) {
1645
+ if (!await waitUntilRetry(deadline, monotonicNow)) break;
1646
+ continue;
1647
+ }
1648
+ try {
1649
+ const committed = await commitOperation(
1650
+ stateDirectory, operation, localRelease.version, (state) => {
1651
+ if (!state.candidate || state.candidate.targetDigest !== targetDigest
1652
+ || compareSemver(state.candidate.version, localRelease.version) <= 0) {
1653
+ return { state: null, result: silent('invalid-preference') };
1654
+ }
1655
+ const preference = {
1656
+ targetDigest,
1657
+ version: state.candidate.version,
1658
+ mode,
1659
+ until: mode === 'snooze' ? new Date(nowMs + days * 24 * 60 * 60 * 1_000).toISOString() : null,
1660
+ };
1661
+ const preferences = (state.preferences || [])
1662
+ .filter((entry) => entry.targetDigest !== targetDigest)
1663
+ .slice(-30);
1664
+ preferences.push(preference);
1665
+ return {
1666
+ state: { ...state, preferences },
1667
+ result: { status: mode === 'snooze' ? 'snoozed' : 'ignored', eventKey,
1668
+ ...(preference.until ? { suppressedUntil: preference.until } : {}) },
1669
+ };
1670
+ },
1671
+ );
1672
+ if (!committed.fenced) return committed.result;
1673
+ } catch {
1674
+ await cancelOperation(stateDirectory, operation).catch(() => {});
1675
+ return silent('cache-unavailable');
1676
+ }
1677
+ }
1678
+ return silent('check-in-progress');
1679
+ }
1680
+
1681
+ async function runCli() {
1682
+ if (updatesDisabled()) return silent('disabled');
1683
+ const argumentsList = process.argv.slice(2);
1684
+ if (argumentsList.length === 0) return checkForUpdate();
1685
+ if (argumentsList.length === 2 && argumentsList[0] === '--ack') {
1686
+ return silent('legacy-ack-no-op');
1687
+ }
1688
+ if (argumentsList.length === 2 && argumentsList[0] === '--snooze') {
1689
+ return setUpdatePreference({ eventKey: argumentsList[1], mode: 'snooze' });
1690
+ }
1691
+ if (argumentsList.length === 2 && argumentsList[0] === '--ignore') {
1692
+ return setUpdatePreference({ eventKey: argumentsList[1], mode: 'ignore' });
1693
+ }
1694
+ return silent('invalid-arguments');
1695
+ }
1696
+
1697
+ export function isMainModule({
1698
+ argvPath = process.argv[1],
1699
+ modulePath = fileURLToPath(import.meta.url),
1700
+ } = {}) {
1701
+ if (!argvPath) return false;
1702
+ try {
1703
+ return sameEntry(argvPath, modulePath).status === 'match';
1704
+ } catch {
1705
+ return false;
1706
+ }
1707
+ }
1708
+
1709
+ if (isMainModule()) {
1710
+ let result;
1711
+ try {
1712
+ result = await runCli();
1713
+ } catch {
1714
+ result = silent('check-failed');
1715
+ }
1716
+ process.stdout.write(`${JSON.stringify(result)}\n`);
1717
+ }