@astryxdesign/cli 0.7.2-canary.fe2c655 → 0.7.2

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 (509) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +175 -154
  3. package/api/build/_adapter.d.mts +8 -0
  4. package/api/build/_adapter.mjs +12 -1
  5. package/api/build/build.doc.mjs +4 -0
  6. package/api/build/help/help.mjs +1 -1
  7. package/api/build/kit/kit.mjs +3 -11
  8. package/api/component/_adapter.d.mts +72 -1
  9. package/api/component/_adapter.mjs +212 -18
  10. package/api/component/component.doc.mjs +6 -1
  11. package/api/component/component.mjs +82 -15
  12. package/api/component/component.test.mjs +80 -0
  13. package/api/component/doc-links.test.mjs +386 -0
  14. package/api/component/integration-export-name.test.mjs +644 -0
  15. package/api/component/list/list.mjs +42 -4
  16. package/api/discover/_adapter.d.mts +132 -12
  17. package/api/discover/_adapter.mjs +488 -27
  18. package/api/discover/_adapter.test.mjs +599 -17
  19. package/api/discover/_catalog-view.d.mts +16 -5
  20. package/api/discover/_catalog-view.mjs +32 -8
  21. package/api/discover/detail/doc/doc.mjs +9 -1
  22. package/api/discover/discover.d.mts +11 -0
  23. package/api/discover/discover.doc.mjs +9 -7
  24. package/api/discover/discover.mjs +121 -19
  25. package/api/discover/discover.type.d.mts +20 -7
  26. package/api/discover/discover.type.mjs +17 -7
  27. package/api/discover/list/list.d.mts +4 -3
  28. package/api/discover/list/list.mjs +17 -10
  29. package/api/docs/_adapter.d.mts +74 -0
  30. package/api/docs/_adapter.mjs +553 -16
  31. package/api/docs/docs.doc.mjs +2 -1
  32. package/api/docs/docs.mjs +2 -1
  33. package/api/docs/docs.test.mjs +5 -2
  34. package/api/docs/integration-tree.test.mjs +22 -0
  35. package/api/docs/integrationDocs.test.mjs +124 -0
  36. package/api/docs/list/list.mjs +12 -2
  37. package/api/docs/reference-blocks.test.mjs +1 -3
  38. package/api/doctor/_adapter.d.mts +108 -0
  39. package/api/doctor/_adapter.mjs +223 -4
  40. package/api/doctor/doctor.d.mts +199 -0
  41. package/api/doctor/doctor.doc.mjs +3 -2
  42. package/api/doctor/doctor.mjs +530 -34
  43. package/api/doctor/doctor.test.mjs +180 -14
  44. package/api/doctor/release-checks.test.mjs +156 -0
  45. package/api/doctor/theme-checks.mjs +17 -4
  46. package/api/doctor/theme-checks.test.mjs +223 -7
  47. package/api/error.d.mts +10 -0
  48. package/api/error.mjs +25 -0
  49. package/api/gap-report/gap-report.d.mts +3 -0
  50. package/api/gap-report/gap-report.doc.mjs +11 -3
  51. package/api/gap-report/gap-report.mjs +219 -52
  52. package/api/gap-report/gap-report.test.mjs +233 -0
  53. package/api/gap-report/gap-report.type.d.mts +41 -0
  54. package/api/gap-report/gap-report.type.mjs +18 -0
  55. package/api/hook/_adapter.mjs +3 -11
  56. package/api/hook/hook.doc.mjs +4 -0
  57. package/api/hook/list/list.d.mts +1 -1
  58. package/api/index.d.mts +2 -0
  59. package/api/index.mjs +2 -0
  60. package/api/init/_adapter.d.mts +22 -0
  61. package/api/init/_adapter.mjs +59 -0
  62. package/api/init/init.d.mts +7 -5
  63. package/api/init/init.doc.mjs +28 -7
  64. package/api/init/init.mjs +28 -4
  65. package/api/init/init.type.d.mts +48 -3
  66. package/api/init/init.type.mjs +37 -3
  67. package/api/init/run/run.d.mts +1 -1
  68. package/api/init/run/run.mjs +17 -6
  69. package/api/init/user/user.d.mts +14 -0
  70. package/api/init/user/user.mjs +87 -0
  71. package/api/init/user/user.test.mjs +232 -0
  72. package/api/integration/_adapter.d.mts +45 -0
  73. package/api/integration/_adapter.mjs +112 -0
  74. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  75. package/api/integration/add-contribution.mjs +8 -4
  76. package/api/integration/add-contribution.test.mjs +4 -2
  77. package/api/integration/authoring-checks.mjs +110 -6
  78. package/api/integration/authoring-checks.test.mjs +808 -0
  79. package/api/integration/authoring-checks.type.d.mts +143 -0
  80. package/api/integration/authoring-checks.type.mjs +74 -2
  81. package/api/integration/check/check.d.mts +96 -0
  82. package/api/integration/check/check.mjs +357 -0
  83. package/api/integration/check/check.test.mjs +489 -0
  84. package/api/integration/integration-check.type.d.mts +118 -0
  85. package/api/integration/integration-check.type.mjs +84 -0
  86. package/api/integration/integrationCheck.doc.d.mts +10 -0
  87. package/api/integration/integrationCheck.doc.mjs +73 -0
  88. package/api/integration/integrationComponentConflicts.doc.mjs +45 -6
  89. package/api/integration/integrationDocConflicts.doc.mjs +22 -5
  90. package/api/integration/pack-check.mjs +364 -120
  91. package/api/integration/pack-check.test.mjs +730 -10
  92. package/api/integration/validate-integration-fixes.test.mjs +67 -0
  93. package/api/integration/validate-integration.d.mts +8 -0
  94. package/api/integration/validate-integration.mjs +63 -9
  95. package/api/integration/validate-integration.test.mjs +88 -8
  96. package/api/integration/validate-integration.type.d.mts +16 -0
  97. package/api/integration/validate-integration.type.mjs +14 -1
  98. package/api/integration/validate-unread-theme-folders.test.mjs +3 -2
  99. package/api/integration/validateIntegration.doc.mjs +19 -3
  100. package/api/search/_adapter.d.mts +11 -0
  101. package/api/search/_adapter.mjs +25 -0
  102. package/api/search/search.d.mts +82 -13
  103. package/api/search/search.doc.mjs +8 -3
  104. package/api/search/search.domain-priority.test.mjs +108 -18
  105. package/api/search/search.integration-check.test.mjs +24 -0
  106. package/api/search/search.mjs +645 -64
  107. package/api/search/search.test.mjs +179 -1
  108. package/api/search/search.type.d.mts +14 -1
  109. package/api/search/search.type.mjs +6 -1
  110. package/api/swizzle/_adapter.mjs +2 -2
  111. package/api/swizzle/copy/copy.mjs +29 -15
  112. package/api/template/template-integration.test.mjs +1 -1
  113. package/api/template/template.test.mjs +120 -3
  114. package/api/theme/_adapter.d.mts +16 -0
  115. package/api/theme/_adapter.mjs +32 -5
  116. package/api/theme/build/build.d.mts +2 -1
  117. package/api/theme/build/build.family.test.mjs +64 -0
  118. package/api/theme/build/build.mjs +250 -38
  119. package/api/theme/build/build.packed-old-core.test.mjs +20 -3
  120. package/api/theme/build/font-warning.mjs +5 -3
  121. package/api/theme/build/font-warning.test.mjs +2 -1
  122. package/api/theme/remove/remove.test.mjs +2 -2
  123. package/api/theme/targets/targets.mjs +3 -10
  124. package/api/theme/themeBuild.doc.mjs +4 -4
  125. package/api/theme/themeTargets.doc.mjs +4 -0
  126. package/api/theme/use/use.test.mjs +1 -1
  127. package/api/upgrade/_adapter.d.mts +40 -2
  128. package/api/upgrade/_adapter.mjs +118 -5
  129. package/api/upgrade/run/cli-behind-core.test.mjs +438 -0
  130. package/api/upgrade/run/run.mjs +35 -2
  131. package/api/upgrade/status/status.d.mts +21 -5
  132. package/api/upgrade/status/status.mjs +45 -8
  133. package/api/upgrade/upgrade.doc.mjs +6 -3
  134. package/api/upgrade/upgrade.mjs +5 -1
  135. package/api/upgrade/upgrade.type.d.mts +23 -0
  136. package/api/upgrade/upgrade.type.mjs +16 -1
  137. package/assets/codemods/__tests__/registry.test.mjs +1 -0
  138. package/assets/codemods/registry.mjs +1 -0
  139. package/assets/codemods/runner.mjs +7 -1
  140. package/assets/codemods/transforms/v0.7.2/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  141. package/assets/codemods/transforms/v0.7.2/index.mjs +20 -0
  142. package/assets/codemods/transforms/v0.7.2/migrate-list-edge-compensation-to-edge.mjs +198 -0
  143. package/assets/discover/official-packages.json +367 -0
  144. package/assets/docs/README.md +10 -0
  145. package/assets/docs/author-a-theme.doc.mjs +3 -3
  146. package/assets/docs/browser-support.doc.mjs +1 -1
  147. package/assets/docs/getting-started.doc.mjs +26 -6
  148. package/assets/docs/styling-overview.doc.mjs +1 -1
  149. package/assets/docs/theme.doc.dense.mjs +1 -1
  150. package/assets/docs/theme.doc.mjs +2 -2
  151. package/assets/docs/theme.doc.zh.mjs +1 -1
  152. package/assets/docs/tree/add-a-component.doc.mjs +22 -2
  153. package/assets/docs/tree/add-a-theme.doc.mjs +13 -3
  154. package/assets/docs/tree/add-a-topic.doc.mjs +54 -9
  155. package/assets/docs/tree/agent-guidance.doc.mjs +19 -5
  156. package/assets/docs/tree/block-template.doc.mjs +8 -20
  157. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  158. package/assets/docs/tree/building-blocks.doc.mjs +4 -0
  159. package/assets/docs/tree/check-the-props.doc.mjs +201 -0
  160. package/assets/docs/tree/check-your-docs.doc.mjs +69 -11
  161. package/assets/docs/tree/checks.doc.mjs +32 -24
  162. package/assets/docs/tree/ci-and-containers.doc.mjs +88 -0
  163. package/assets/docs/tree/cli-command.doc.mjs +142 -0
  164. package/assets/docs/tree/cli.doc.mjs +6 -0
  165. package/assets/docs/tree/codemods.doc.mjs +17 -7
  166. package/assets/docs/tree/commands.doc.mjs +7 -1
  167. package/assets/docs/tree/component-family.doc.mjs +8 -6
  168. package/assets/docs/tree/component-imports.doc.mjs +21 -3
  169. package/assets/docs/tree/components.doc.mjs +7 -1
  170. package/assets/docs/tree/computer-agent-guide.doc.mjs +160 -0
  171. package/assets/docs/tree/computer-integrations.doc.mjs +93 -0
  172. package/assets/docs/tree/configuration.doc.mjs +7 -3
  173. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +41 -22
  174. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  175. package/assets/docs/tree/describe-the-component.doc.mjs +3 -3
  176. package/assets/docs/tree/docs.doc.mjs +9 -3
  177. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  178. package/assets/docs/tree/document-the-theme.doc.mjs +3 -3
  179. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  180. package/assets/docs/tree/extend-or-replace.doc.mjs +19 -9
  181. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  182. package/assets/docs/tree/generate-a-palette.doc.mjs +7 -5
  183. package/assets/docs/tree/global-integrations.doc.mjs +124 -0
  184. package/assets/docs/tree/global.doc.mjs +131 -0
  185. package/assets/docs/tree/grade-template-with-agent.doc.mjs +5 -5
  186. package/assets/docs/tree/install-once.doc.mjs +105 -0
  187. package/assets/docs/tree/integrations.doc.mjs +13 -1
  188. package/assets/docs/tree/layout-spacing.doc.mjs +2 -3
  189. package/assets/docs/tree/layout.doc.mjs +6 -6
  190. package/assets/docs/tree/links.doc.mjs +7 -6
  191. package/assets/docs/tree/migration-setup.doc.mjs +3 -2
  192. package/assets/docs/tree/migration-verification.doc.mjs +0 -1
  193. package/assets/docs/tree/migration.doc.mjs +0 -1
  194. package/assets/docs/tree/monorepos.doc.mjs +84 -0
  195. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  196. package/assets/docs/tree/page-template.doc.mjs +4 -8
  197. package/assets/docs/tree/problems.doc.mjs +205 -0
  198. package/assets/docs/tree/project-global-or-both.doc.mjs +131 -0
  199. package/assets/docs/tree/publishing.doc.mjs +6 -6
  200. package/assets/docs/tree/quick-start.doc.mjs +53 -27
  201. package/assets/docs/tree/reference.doc.mjs +192 -0
  202. package/assets/docs/tree/replace-a-core-component.doc.mjs +15 -7
  203. package/assets/docs/tree/replace-a-core-template.doc.mjs +10 -12
  204. package/assets/docs/tree/scaffold.doc.mjs +1 -1
  205. package/assets/docs/tree/sections-and-placement.doc.mjs +9 -11
  206. package/assets/docs/tree/see-it-in-an-app.doc.mjs +17 -5
  207. package/assets/docs/tree/short-and-findable.doc.mjs +9 -8
  208. package/assets/docs/tree/single-component.doc.mjs +15 -6
  209. package/assets/docs/tree/start-a-template.doc.mjs +18 -8
  210. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  211. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  212. package/assets/docs/tree/template-doc-overview.doc.mjs +6 -17
  213. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  214. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  215. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  216. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  217. package/assets/docs/tree/templates.doc.mjs +7 -1
  218. package/assets/docs/tree/test-in-an-app.doc.mjs +7 -7
  219. package/assets/docs/tree/test-template-in-app.doc.mjs +5 -5
  220. package/assets/docs/tree/themes.doc.mjs +4 -0
  221. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  222. package/assets/docs/tree/troubleshooting.doc.mjs +29 -13
  223. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  224. package/assets/docs/tree/uninstall.doc.mjs +109 -0
  225. package/assets/docs/tree/updates.doc.mjs +92 -0
  226. package/assets/docs/tree/upgrading.doc.mjs +6 -6
  227. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +14 -2
  228. package/assets/docs/tree/verify-packed-template.doc.mjs +3 -3
  229. package/assets/docs/tree/versioning.doc.mjs +10 -9
  230. package/assets/docs/tree/who-answers.doc.mjs +119 -0
  231. package/assets/docs/tree/write-the-template-file.doc.mjs +7 -6
  232. package/assets/docs/use-a-theme.doc.mjs +2 -2
  233. package/assets/docs/working-with-ai.doc.mjs +66 -13
  234. package/assets/theme.template.ts +1 -1
  235. package/authoring/codemod/codemod.doc.mjs +7 -3
  236. package/authoring/config/config.doc.mjs +9 -6
  237. package/authoring/config/type.ts +4 -2
  238. package/authoring/debug/parse.d.mts +5 -5
  239. package/authoring/discover/discover.doc.mjs +7 -1
  240. package/authoring/doctypes/_schema.d.mts +8 -3
  241. package/authoring/doctypes/_schema.mjs +1 -0
  242. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  243. package/authoring/doctypes/command/command.doc.mjs +20 -12
  244. package/authoring/doctypes/command/type.ts +14 -0
  245. package/authoring/doctypes/component/component.doc.mjs +30 -27
  246. package/authoring/doctypes/component/type.ts +21 -8
  247. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  248. package/authoring/doctypes/function/function.doc.mjs +9 -8
  249. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  250. package/authoring/doctypes/hook/type.ts +4 -0
  251. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  252. package/authoring/doctypes/reference/reference.doc.mjs +12 -11
  253. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  254. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  255. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  256. package/authoring/doctypes/template/parse.d.mts +6 -2
  257. package/authoring/doctypes/template/parse.mjs +3 -1
  258. package/authoring/doctypes/template/template.doc.mjs +3 -3
  259. package/authoring/doctypes/template/type.ts +22 -0
  260. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  261. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  262. package/authoring/identity/identity.doc.mjs +3 -3
  263. package/authoring/index.d.ts +2 -0
  264. package/authoring/integration/integration.doc.mjs +30 -6
  265. package/authoring/integration/parse.test.mjs +21 -0
  266. package/authoring/integration/schema.d.mts +16 -3
  267. package/authoring/integration/schema.mjs +34 -2
  268. package/authoring/integration/type.ts +12 -0
  269. package/clients/cli/bin/astryx.mjs +20 -3
  270. package/clients/cli/cli-command.test.mjs +166 -0
  271. package/clients/cli/commands/blog.doc.mjs +1 -0
  272. package/clients/cli/commands/build-theme.batch-report.test.mjs +17 -3
  273. package/clients/cli/commands/build-theme.font-warning.test.mjs +3 -3
  274. package/clients/cli/commands/build-theme.mjs +26 -13
  275. package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
  276. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  277. package/clients/cli/commands/build.doc.mjs +1 -0
  278. package/clients/cli/commands/build.exit-codes-doc.test.mjs +4 -0
  279. package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
  280. package/clients/cli/commands/component.doc.mjs +1 -0
  281. package/clients/cli/commands/discover.broken-integration.test.mjs +11 -7
  282. package/clients/cli/commands/discover.doc.mjs +9 -1
  283. package/clients/cli/commands/discover.mjs +144 -93
  284. package/clients/cli/commands/discover.no-source.test.mjs +448 -33
  285. package/clients/cli/commands/discover.sources.test.mjs +20 -1
  286. package/clients/cli/commands/docs.doc.mjs +1 -0
  287. package/clients/cli/commands/docs.mjs +49 -19
  288. package/clients/cli/commands/docs.test.mjs +2 -2
  289. package/clients/cli/commands/doctor-integration-components.doc.mjs +19 -4
  290. package/clients/cli/commands/doctor-integration-docs.doc.mjs +20 -5
  291. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -0
  292. package/clients/cli/commands/doctor-integration-validate.doc.mjs +22 -2
  293. package/clients/cli/commands/doctor-integration.test.mjs +888 -3
  294. package/clients/cli/commands/doctor.doc.mjs +12 -2
  295. package/clients/cli/commands/doctor.mjs +292 -7
  296. package/clients/cli/commands/doctor.test.mjs +10 -4
  297. package/clients/cli/commands/gap-report.doc.mjs +15 -2
  298. package/clients/cli/commands/gap-report.mjs +24 -2
  299. package/clients/cli/commands/gap-report.test.mjs +79 -0
  300. package/clients/cli/commands/hook.doc.mjs +1 -0
  301. package/clients/cli/commands/init.behavior.test.mjs +5 -0
  302. package/clients/cli/commands/init.doc.mjs +29 -3
  303. package/clients/cli/commands/integration-add.doc.mjs +7 -0
  304. package/clients/cli/commands/integration-authoring.test.mjs +49 -3
  305. package/clients/cli/commands/integration-check.doc.mjs +93 -0
  306. package/clients/cli/commands/integration-check.test.mjs +441 -0
  307. package/clients/cli/commands/integration-pack.doc.mjs +8 -1
  308. package/clients/cli/commands/integration-verify.doc.mjs +8 -1
  309. package/clients/cli/commands/integration.doc.mjs +6 -2
  310. package/clients/cli/commands/integration.mjs +119 -7
  311. package/clients/cli/commands/json-contract.test.mjs +11 -2
  312. package/clients/cli/commands/manifest.doc.mjs +1 -0
  313. package/clients/cli/commands/outside-project.test.mjs +774 -0
  314. package/clients/cli/commands/search.doc.mjs +1 -0
  315. package/clients/cli/commands/search.mjs +16 -11
  316. package/clients/cli/commands/search.test.mjs +429 -296
  317. package/clients/cli/commands/setup-nudge.test.mjs +102 -2
  318. package/clients/cli/commands/swizzle.doc.mjs +7 -0
  319. package/clients/cli/commands/swizzle.mjs +5 -3
  320. package/clients/cli/commands/swizzle.routing.test.mjs +19 -2
  321. package/clients/cli/commands/template.doc.mjs +1 -0
  322. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  323. package/clients/cli/commands/theme-add-lifecycle.test.mjs +3 -3
  324. package/clients/cli/commands/theme-add.doc.mjs +8 -1
  325. package/clients/cli/commands/theme-build.doc.mjs +3 -2
  326. package/clients/cli/commands/theme-eject.doc.mjs +1 -0
  327. package/clients/cli/commands/theme-list.behavior.test.mjs +6 -0
  328. package/clients/cli/commands/theme-list.doc.mjs +1 -0
  329. package/clients/cli/commands/theme-palette-generate.doc.mjs +1 -0
  330. package/clients/cli/commands/theme-remove.doc.mjs +7 -0
  331. package/clients/cli/commands/theme-targets.doc.mjs +1 -0
  332. package/clients/cli/commands/theme-template.doc.mjs +1 -0
  333. package/clients/cli/commands/theme-use.doc.mjs +7 -0
  334. package/clients/cli/commands/upgrade.config-ordering.test.mjs +2 -1
  335. package/clients/cli/commands/upgrade.doc.mjs +22 -1
  336. package/clients/cli/commands/upgrade.file-protection.test.mjs +3 -2
  337. package/clients/cli/commands/upgrade.hook-output.test.mjs +2 -1
  338. package/clients/cli/commands/upgrade.older-cli.test.mjs +160 -0
  339. package/clients/cli/index.mjs +112 -16
  340. package/clients/cli/lib/cli-error.mjs +4 -1
  341. package/clients/cli/lib/component-format.test.mjs +1 -1
  342. package/clients/cli/lib/define-command.mjs +2 -2
  343. package/clients/cli/lib/json-shim.mjs +3 -1
  344. package/clients/cli/lib/manifest.d.ts +9 -0
  345. package/clients/cli/lib/manifest.mjs +17 -1
  346. package/clients/cli/lib/manifest.test.mjs +25 -0
  347. package/foundation/agent-docs/agent-doc-state.d.mts +35 -6
  348. package/foundation/agent-docs/agent-doc-state.mjs +66 -6
  349. package/foundation/agent-docs/agent-docs.d.mts +4 -3
  350. package/foundation/agent-docs/agent-docs.mjs +11 -9
  351. package/foundation/agent-docs/computer-agent-text.d.mts +133 -0
  352. package/foundation/agent-docs/computer-agent-text.mjs +623 -0
  353. package/foundation/agent-docs/computer-agent-text.test.mjs +602 -0
  354. package/foundation/config/project.d.mts +35 -3
  355. package/foundation/config/project.mjs +226 -7
  356. package/foundation/config/project.test.mjs +66 -0
  357. package/foundation/config/theme-state.mjs +5 -4
  358. package/foundation/discovery/authoring-self-docs.mjs +2 -1
  359. package/foundation/discovery/cli-self-docs.mjs +20 -3
  360. package/foundation/discovery/cli-self-docs.test.mjs +3 -2
  361. package/foundation/discovery/component-discovery.d.mts +190 -10
  362. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  363. package/foundation/discovery/component-discovery.mjs +864 -56
  364. package/foundation/discovery/component-discovery.test.mjs +96 -0
  365. package/foundation/discovery/component-replacement.mjs +20 -1
  366. package/foundation/discovery/component-replacement.test.mjs +40 -0
  367. package/foundation/discovery/copied-passages.d.mts +216 -0
  368. package/foundation/discovery/copied-passages.mjs +720 -0
  369. package/foundation/discovery/copied-passages.test.mjs +124 -0
  370. package/foundation/discovery/doc-consistency.d.mts +34 -0
  371. package/foundation/discovery/doc-consistency.mjs +513 -0
  372. package/foundation/discovery/doc-consistency.test.mjs +347 -0
  373. package/foundation/discovery/doc-fluff.d.mts +112 -0
  374. package/foundation/discovery/doc-fluff.mjs +328 -0
  375. package/foundation/discovery/doc-fluff.test.mjs +142 -0
  376. package/foundation/discovery/doc-types.d.mts +157 -0
  377. package/foundation/discovery/doc-types.mjs +1231 -0
  378. package/foundation/discovery/doc-types.test.mjs +744 -0
  379. package/foundation/discovery/docs-discovery.d.mts +88 -0
  380. package/foundation/discovery/docs-discovery.mjs +83 -2
  381. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  382. package/foundation/discovery/integration-doc-files.mjs +231 -0
  383. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  384. package/foundation/discovery/module-exports.d.mts +27 -0
  385. package/foundation/discovery/module-exports.mjs +356 -31
  386. package/foundation/discovery/module-exports.test.mjs +172 -0
  387. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  388. package/foundation/discovery/package-tsconfig.mjs +228 -0
  389. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  390. package/foundation/discovery/project-typescript.d.mts +32 -0
  391. package/foundation/discovery/project-typescript.mjs +89 -0
  392. package/foundation/discovery/prop-drift.d.mts +297 -0
  393. package/foundation/discovery/prop-drift.mjs +2428 -0
  394. package/foundation/discovery/prop-drift.test.mjs +2337 -0
  395. package/foundation/discovery/runner-allowlist.d.mts +40 -0
  396. package/foundation/discovery/runner-allowlist.mjs +53 -0
  397. package/foundation/discovery/stray-pointers.d.mts +52 -0
  398. package/foundation/discovery/stray-pointers.mjs +293 -0
  399. package/foundation/discovery/stray-pointers.test.mjs +86 -0
  400. package/foundation/discovery/template-adapter.mjs +74 -5
  401. package/foundation/discovery/template-needs.mjs +5 -2
  402. package/foundation/discovery/theme-imports.mjs +8 -4
  403. package/foundation/discovery/theme-imports.test.mjs +1 -1
  404. package/foundation/discovery/type-match.d.mts +53 -0
  405. package/foundation/discovery/type-match.mjs +660 -0
  406. package/foundation/discovery/type-match.test.mjs +169 -0
  407. package/foundation/discovery/type-text.d.mts +68 -0
  408. package/foundation/discovery/type-text.mjs +658 -0
  409. package/foundation/discovery/type-text.test.mjs +196 -0
  410. package/foundation/discovery/unresolved-references.d.mts +62 -0
  411. package/foundation/discovery/unresolved-references.mjs +790 -0
  412. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  413. package/foundation/doc-compiler/doc-loads.test.mjs +51 -1
  414. package/foundation/doc-compiler/inputs.mjs +27 -1
  415. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  416. package/foundation/doc-compiler/links.d.mts +41 -5
  417. package/foundation/doc-compiler/links.mjs +167 -28
  418. package/foundation/doc-compiler/links.test.mjs +97 -4
  419. package/foundation/doc-compiler/read.d.mts +1 -1
  420. package/foundation/doc-compiler/tree.mjs +30 -1
  421. package/foundation/doc-compiler/tree.test.mjs +8 -1
  422. package/foundation/env/cli-command.d.mts +83 -0
  423. package/foundation/env/cli-command.mjs +152 -0
  424. package/foundation/env/cli-command.test.mjs +129 -0
  425. package/foundation/env/node-version.test.mjs +15 -3
  426. package/foundation/env/package-manager.d.mts +93 -9
  427. package/foundation/env/package-manager.global.test.mjs +66 -0
  428. package/foundation/env/package-manager.mjs +184 -13
  429. package/foundation/env/package-manager.test.mjs +341 -15
  430. package/foundation/env/project-scope.d.mts +9 -0
  431. package/foundation/env/project-scope.mjs +33 -0
  432. package/foundation/env/search-path.d.mts +42 -0
  433. package/foundation/env/search-path.mjs +205 -0
  434. package/foundation/env/search-path.test.mjs +122 -0
  435. package/foundation/env/user-dirs.d.mts +39 -0
  436. package/foundation/env/user-dirs.mjs +93 -0
  437. package/foundation/env/user-dirs.test.mjs +72 -0
  438. package/foundation/env/user-settings.d.mts +46 -0
  439. package/foundation/env/user-settings.mjs +106 -0
  440. package/foundation/env/user-settings.test.mjs +64 -0
  441. package/foundation/fs/file-protection.mjs +15 -1
  442. package/foundation/fs/module-loader.d.mts +2 -1
  443. package/foundation/fs/module-loader.mjs +60 -4
  444. package/foundation/fs/module-loader.stdout.test.mjs +3 -2
  445. package/foundation/fs/module-loader.test.mjs +194 -0
  446. package/foundation/fs/path-compare.d.mts +33 -0
  447. package/foundation/fs/path-compare.mjs +73 -0
  448. package/foundation/fs/paths.d.mts +57 -1
  449. package/foundation/fs/paths.mjs +131 -3
  450. package/foundation/fs/paths.test.mjs +10 -1
  451. package/foundation/integrations/computer-integrations.d.mts +78 -0
  452. package/foundation/integrations/computer-integrations.mjs +316 -0
  453. package/foundation/integrations/computer-integrations.test.mjs +292 -0
  454. package/foundation/integrations/contribution-inventory.d.mts +11 -0
  455. package/foundation/integrations/contribution-inventory.mjs +56 -3
  456. package/foundation/integrations/integration-warnings.mjs +3 -2
  457. package/foundation/integrations/integration-warnings.test.mjs +8 -9
  458. package/foundation/integrations/integrations.d.mts +25 -1
  459. package/foundation/integrations/integrations.mjs +46 -3
  460. package/foundation/integrations/issue.ts +6 -0
  461. package/foundation/integrations/validate-contributions.d.mts +2 -0
  462. package/foundation/integrations/validate-contributions.mjs +41 -1
  463. package/foundation/release/agent-text-worker.d.mts +4 -0
  464. package/foundation/release/agent-text-worker.mjs +52 -0
  465. package/foundation/release/cache.d.mts +310 -0
  466. package/foundation/release/cache.mjs +1216 -0
  467. package/foundation/release/cache.test.mjs +920 -0
  468. package/foundation/release/choose.d.mts +207 -0
  469. package/foundation/release/choose.mjs +677 -0
  470. package/foundation/release/choose.test.mjs +427 -0
  471. package/foundation/release/core-worker.d.mts +4 -0
  472. package/foundation/release/core-worker.mjs +38 -0
  473. package/foundation/release/global-state.d.mts +259 -0
  474. package/foundation/release/global-state.mjs +472 -0
  475. package/foundation/release/global-state.test.mjs +381 -0
  476. package/foundation/release/handoff.d.mts +60 -0
  477. package/foundation/release/handoff.e2e.test.mjs +278 -0
  478. package/foundation/release/handoff.mjs +357 -0
  479. package/foundation/release/lock-lease.d.mts +23 -0
  480. package/foundation/release/lock-lease.mjs +80 -0
  481. package/foundation/release/notices.d.mts +253 -0
  482. package/foundation/release/notices.mjs +596 -0
  483. package/foundation/release/notices.test.mjs +565 -0
  484. package/foundation/release/npm-guard.d.mts +4 -0
  485. package/foundation/release/npm-guard.mjs +126 -0
  486. package/foundation/release/project.d.mts +141 -0
  487. package/foundation/release/project.mjs +466 -0
  488. package/foundation/release/project.test.mjs +288 -0
  489. package/foundation/release/refresh-worker.d.mts +4 -0
  490. package/foundation/release/refresh-worker.mjs +27 -0
  491. package/foundation/release/run-context.d.mts +47 -0
  492. package/foundation/release/run-context.mjs +149 -0
  493. package/foundation/release/update-notice-env.d.mts +28 -0
  494. package/foundation/release/update-notice-env.mjs +39 -0
  495. package/foundation/response/base.d.ts +26 -3
  496. package/foundation/response/error-codes.d.mts +3 -1
  497. package/foundation/response/error-codes.doc.mjs +12 -0
  498. package/foundation/response/error-codes.mjs +17 -0
  499. package/foundation/response/error-codes.test.mjs +2 -0
  500. package/foundation/response/json-contract.test.mjs +7 -2
  501. package/foundation/response/json.mjs +4 -1
  502. package/foundation/response/response-types.doc.mjs +22 -11
  503. package/foundation/response/response-types.doc.test.mjs +46 -0
  504. package/foundation/response/response.doc.mjs +3 -3
  505. package/foundation/xle/browser-imports.test.mjs +74 -0
  506. package/foundation/xle/registry.mjs +4 -0
  507. package/foundation/xle/registry.test.mjs +28 -0
  508. package/package.json +11 -11
  509. package/scripts/postinstall.mjs +24 -5
@@ -0,0 +1,920 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect, afterEach} from 'vitest';
4
+ import {spawn} from 'node:child_process';
5
+ import * as fs from 'node:fs';
6
+ import * as os from 'node:os';
7
+ import * as path from 'node:path';
8
+ import {
9
+ cachedRelease,
10
+ forgetVerifiedReleases,
11
+ ensureRelease,
12
+ isInReleaseCache,
13
+ releasesDir,
14
+ resolvePublishedVersion,
15
+ isVersionRange,
16
+ npmCommand,
17
+ resolveLatestRelease,
18
+ runNpm,
19
+ tryLock,
20
+ endRecordedInstall,
21
+ isReleaseInstall,
22
+ INSTALL_TIMEOUT_MS,
23
+ } from './cache.mjs';
24
+ import {
25
+ cleanupFixtures,
26
+ installCore,
27
+ installStandInCli,
28
+ seedRelease,
29
+ tempDir,
30
+ writeJson,
31
+ } from '../../test-utils/release-fixtures.mjs';
32
+ import {LEASE_SILENCE_MS, renewLock} from './lock-lease.mjs';
33
+
34
+ afterEach(cleanupFixtures);
35
+
36
+ /**
37
+ * A stand-in for npm: `install` lays down stand-in CLI and Core packages of
38
+ * the requested version (or fails the way `outcome` says), and records each
39
+ * call.
40
+ * @param {{outcome?: 'ok' | 'offline' | 'missing' | 'wrong-version', engines?: string}} [options]
41
+ */
42
+ function fakeNpm(options = {}) {
43
+ const {outcome = 'ok', engines} = options;
44
+ /** @type {string[][]} */
45
+ const calls = [];
46
+ /** @type {import('./cache.mjs').runNpm} */
47
+ const run = (args, opts = {}) => {
48
+ calls.push(args);
49
+ if (outcome === 'offline') {
50
+ return {
51
+ status: 1,
52
+ stdout: '',
53
+ stderr: 'npm error code ENOTFOUND\nnpm error network request failed',
54
+ };
55
+ }
56
+ if (outcome === 'missing') {
57
+ return {
58
+ status: 1,
59
+ stdout: '',
60
+ stderr:
61
+ 'npm error code ETARGET\nnpm error notarget No matching version',
62
+ };
63
+ }
64
+ const spec = args.find(a => a.startsWith('@astryxdesign/cli@')) ?? '';
65
+ const version = spec.split('@').pop() ?? '';
66
+ const cwd = /** @type {string} */ (opts.cwd);
67
+ const cli = installStandInCli(
68
+ cwd,
69
+ outcome === 'wrong-version' ? '0.0.1' : version,
70
+ );
71
+ if (engines) {
72
+ const pkg = JSON.parse(
73
+ fs.readFileSync(path.join(cli, 'package.json'), 'utf-8'),
74
+ );
75
+ writeJson(path.join(cli, 'package.json'), {
76
+ ...pkg,
77
+ engines: {node: engines},
78
+ });
79
+ }
80
+ installCore(cwd, version);
81
+ return {status: 0, stdout: '', stderr: ''};
82
+ };
83
+ return {run, calls};
84
+ }
85
+
86
+ describe('ensureRelease', () => {
87
+ it('verifies a release with update notices off, so the check prints and uses up none (FR10)', () => {
88
+ const cacheDir = tempDir('cache');
89
+ const npm = fakeNpm();
90
+ /** @type {typeof npm.run} */
91
+ const run = (args, opts = {}) => {
92
+ const result = npm.run(args, opts);
93
+ // A release whose --version answers only when notices are off.
94
+ const bin = path.join(
95
+ /** @type {string} */ (opts.cwd),
96
+ 'node_modules',
97
+ '@astryxdesign',
98
+ 'cli',
99
+ 'bin.cjs',
100
+ );
101
+ fs.writeFileSync(
102
+ bin,
103
+ `if (process.env.NO_UPDATE_NOTIFIER === undefined) { console.log('notices-on'); process.exit(0); }\nconsole.log('0.6.8');\n`,
104
+ );
105
+ return result;
106
+ };
107
+ const release = ensureRelease('0.6.8', {cacheDir, run});
108
+ expect('bin' in release && release.version).toBe('0.6.8');
109
+ });
110
+
111
+ it('installs, verifies, and keeps a release, then reuses it', () => {
112
+ const cacheDir = tempDir('cache');
113
+ const npm = fakeNpm();
114
+ const release = ensureRelease('0.6.8', {cacheDir, run: npm.run});
115
+ expect('bin' in release && release.version).toBe('0.6.8');
116
+ expect(npm.calls[0]).toEqual(
117
+ expect.arrayContaining([
118
+ 'install',
119
+ '--ignore-scripts',
120
+ '--prefer-offline',
121
+ '@astryxdesign/cli@0.6.8',
122
+ '@astryxdesign/core@0.6.8',
123
+ ]),
124
+ );
125
+ expect(cachedRelease('0.6.8', cacheDir)?.coreDir).toBe(
126
+ path.join(
127
+ releasesDir(cacheDir),
128
+ '0.6.8',
129
+ 'node_modules',
130
+ '@astryxdesign',
131
+ 'core',
132
+ ),
133
+ );
134
+ ensureRelease('0.6.8', {cacheDir, run: npm.run});
135
+ expect(npm.calls).toHaveLength(1);
136
+ expect(
137
+ fs.readdirSync(releasesDir(cacheDir)).filter(n => n.startsWith('.')),
138
+ ).toEqual([]);
139
+ });
140
+
141
+ it('says why a release is unavailable and keeps nothing', () => {
142
+ const cases = /** @type {const} */ ([
143
+ ['offline', /could not be reached/],
144
+ ['missing', /does not have it/],
145
+ ['wrong-version', /another version/],
146
+ ]);
147
+ for (const [outcome, reason] of cases) {
148
+ const cacheDir = tempDir('cache');
149
+ const result = ensureRelease('0.6.8', {
150
+ cacheDir,
151
+ run: fakeNpm({outcome}).run,
152
+ });
153
+ expect('reason' in result && result.reason).toMatch(reason);
154
+ expect(cachedRelease('0.6.8', cacheDir)).toBeNull();
155
+ }
156
+ });
157
+
158
+ it("refuses a release this computer's Node cannot run", () => {
159
+ const cacheDir = tempDir('cache');
160
+ const result = ensureRelease('0.9.0', {
161
+ cacheDir,
162
+ run: fakeNpm({engines: '>=99.0.0'}).run,
163
+ });
164
+ expect('reason' in result && result.reason).toMatch(
165
+ /needs Node >=99\.0\.0/,
166
+ );
167
+ });
168
+
169
+ it('refuses a version that is not a release version', () => {
170
+ const result = ensureRelease('../../etc', {
171
+ cacheDir: tempDir('cache'),
172
+ run: fakeNpm().run,
173
+ });
174
+ expect('reason' in result && result.reason).toMatch(
175
+ /not a release version/,
176
+ );
177
+ });
178
+
179
+ it('waits for another run installing the same release', () => {
180
+ const cacheDir = tempDir('cache');
181
+ fs.mkdirSync(path.join(releasesDir(cacheDir), '.lock-0.6.8'), {
182
+ recursive: true,
183
+ });
184
+ const result = ensureRelease('0.6.8', {
185
+ cacheDir,
186
+ run: fakeNpm().run,
187
+ waitMs: 300,
188
+ });
189
+ expect('reason' in result && result.reason).toMatch(/still installing/);
190
+ });
191
+ });
192
+
193
+ describe('cachedRelease', () => {
194
+ it('counts a release only once it is verified', () => {
195
+ const cacheDir = tempDir('cache');
196
+ const dir = seedRelease(cacheDir, '0.7.1');
197
+ expect(cachedRelease('0.7.1', cacheDir)).not.toBeNull();
198
+ fs.rmSync(path.join(dir, 'release.json'));
199
+ forgetVerifiedReleases();
200
+ expect(cachedRelease('0.7.1', cacheDir)).toBeNull();
201
+ });
202
+
203
+ it('knows a folder inside the cache', () => {
204
+ const cacheDir = tempDir('cache');
205
+ const dir = seedRelease(cacheDir, '0.7.1');
206
+ expect(isInReleaseCache(dir, cacheDir)).toBe(true);
207
+ expect(isInReleaseCache(tempDir(), cacheDir)).toBe(false);
208
+ });
209
+ });
210
+
211
+ describe.skipIf(process.platform === 'win32')(
212
+ 'a stale lock ends a recorded process group only when it is surely the install',
213
+ () => {
214
+ /** @type {import('node:child_process').ChildProcess[]} */
215
+ const started = [];
216
+ afterEach(() => {
217
+ for (const child of started.splice(0)) {
218
+ try {
219
+ process.kill(-(/** @type {number} */ (child.pid)), 'SIGKILL');
220
+ } catch {
221
+ // Already gone.
222
+ }
223
+ }
224
+ });
225
+ const SLEEP = 'setTimeout(() => {}, 60000)';
226
+ /**
227
+ * A process in a group of its own, started as `program args…`.
228
+ * @param {string} program @param {string[]} args
229
+ */
230
+ const startGroup = (program, args) => {
231
+ const child = spawn(program, args, {detached: true, stdio: 'ignore'});
232
+ started.push(child);
233
+ return /** @type {number} */ (child.pid);
234
+ };
235
+ /** A sleeping script at `dir/name`. @param {string} name */
236
+ const script = name => {
237
+ const file = path.join(tempDir('bin'), name);
238
+ fs.writeFileSync(file, SLEEP);
239
+ return file;
240
+ };
241
+ /** @param {number} pid @param {number} at */
242
+ const lockFor = (pid, at) => {
243
+ const lockDir = path.join(tempDir('lock'), '.lock-9.9.9');
244
+ fs.mkdirSync(lockDir, {recursive: true});
245
+ fs.writeFileSync(path.join(lockDir, 'npm.pid'), String(pid));
246
+ fs.writeFileSync(
247
+ path.join(lockDir, 'owner.json'),
248
+ JSON.stringify({pid: 999999, host: os.hostname(), at}),
249
+ );
250
+ return lockDir;
251
+ };
252
+ const alive = (/** @type {number} */ pid) => {
253
+ try {
254
+ process.kill(pid, 0);
255
+ return true;
256
+ } catch {
257
+ return false;
258
+ }
259
+ };
260
+ const INSTALL = ['install', '--no-save', '@astryxdesign/cli@9.9.9'];
261
+ const until = async (/** @type {() => boolean} */ done) => {
262
+ for (let i = 0; i < 50 && !done(); i++) {
263
+ await new Promise(resolve => setTimeout(resolve, 100));
264
+ }
265
+ return done();
266
+ };
267
+
268
+ it('reads the install only from its exact program and arguments', () => {
269
+ const ok = ['/usr/bin/node', '/x/npm/bin/npm-cli.js', ...INSTALL];
270
+ expect(isReleaseInstall(ok, '9.9.9')).toBe(true);
271
+ // npm's own process title, split on whitespace.
272
+ expect(
273
+ isReleaseInstall(
274
+ 'npm install --no-save @astryxdesign/cli@9.9.9 @astryxdesign/core@9.9.9'.split(
275
+ ' ',
276
+ ),
277
+ '9.9.9',
278
+ ),
279
+ ).toBe(true);
280
+ expect(isReleaseInstall(['C:\\npm\\npm.cmd', ...INSTALL], '9.9.9')).toBe(
281
+ true,
282
+ );
283
+ for (const argv of [
284
+ ['/opt/my-npm-tool', ...INSTALL],
285
+ ['python3', '-c', 'sleep', '/home/me/notes/npm-debug.log', ...INSTALL],
286
+ ['/usr/bin/node', '-e', 'x', 'npm', ...INSTALL],
287
+ [
288
+ '/usr/bin/node',
289
+ '/x/npm-cli.js',
290
+ 'install',
291
+ '@astryxdesign/cli@9.9.90',
292
+ ],
293
+ ['/usr/bin/node', '/x/npm-cli.js', 'ci', '@astryxdesign/cli@9.9.9'],
294
+ ]) {
295
+ expect(isReleaseInstall(argv, '9.9.9'), argv.join(' ')).toBe(false);
296
+ }
297
+ });
298
+
299
+ it.skipIf(!fs.existsSync('/proc/self/stat'))(
300
+ "reads npm's rewritten process title from /proc, one argument, and ends that install",
301
+ async () => {
302
+ const at = Date.now() - 1000;
303
+ const npm = startGroup(process.execPath, ['-e', SLEEP]);
304
+ await new Promise(resolve => setTimeout(resolve, 300));
305
+ // A process table whose cmdline is npm's title: one NUL-terminated string.
306
+ const procRoot = tempDir('proc');
307
+ fs.mkdirSync(path.join(procRoot, String(npm)));
308
+ fs.writeFileSync(
309
+ path.join(procRoot, String(npm), 'cmdline'),
310
+ 'npm install --no-save @astryxdesign/cli@9.9.9 @astryxdesign/core@9.9.9\u0000',
311
+ );
312
+ // procfs files report size 0, so they are read, not copied.
313
+ for (const [from, to] of [
314
+ [`/proc/${npm}/stat`, path.join(procRoot, String(npm), 'stat')],
315
+ ['/proc/stat', path.join(procRoot, 'stat')],
316
+ ]) {
317
+ fs.writeFileSync(to, fs.readFileSync(from, 'utf8'));
318
+ }
319
+ expect(endRecordedInstall(lockFor(npm, at), {procRoot})).toBe('ended');
320
+ expect(await until(() => !alive(npm))).toBe(true);
321
+ },
322
+ );
323
+
324
+ it('leaves a program named my-npm-tool, and one with npm only in an argument', async () => {
325
+ const now = Date.now();
326
+ const tool = path.join(tempDir('bin'), 'my-npm-tool');
327
+ fs.symlinkSync(process.execPath, tool);
328
+ const named = startGroup(tool, [script('sleep.js'), ...INSTALL]);
329
+ const argued = startGroup(process.execPath, [
330
+ '-e',
331
+ SLEEP,
332
+ 'npm',
333
+ '/home/me/notes/npm-debug.log',
334
+ ...INSTALL,
335
+ ]);
336
+ await new Promise(resolve => setTimeout(resolve, 300));
337
+ for (const pid of [named, argued]) {
338
+ expect(endRecordedInstall(lockFor(pid, now - 5000), {now})).toBe(
339
+ 'left',
340
+ );
341
+ expect(alive(pid)).toBe(true);
342
+ }
343
+ });
344
+
345
+ it("ends the lock's own install: node running npm-cli.js install of that release, started after the lock", async () => {
346
+ const at = Date.now() - 1000;
347
+ const npm = startGroup(process.execPath, [
348
+ script('npm-cli.js'),
349
+ ...INSTALL,
350
+ ]);
351
+ await new Promise(resolve => setTimeout(resolve, 300));
352
+ // An old lock, or one with no recorded start: not trusted.
353
+ expect(
354
+ endRecordedInstall(lockFor(npm, Date.now() - INSTALL_TIMEOUT_MS - 1)),
355
+ ).toBe('left');
356
+ const unknown = lockFor(npm, at);
357
+ fs.writeFileSync(
358
+ path.join(unknown, 'owner.json'),
359
+ JSON.stringify({pid: 999999, host: os.hostname()}),
360
+ );
361
+ expect(endRecordedInstall(unknown)).toBe('left');
362
+ // A lock taken after the process started: the id names an older process.
363
+ expect(
364
+ endRecordedInstall(lockFor(npm, Date.now() + 60_000), {
365
+ now: Date.now() + 61_000,
366
+ }),
367
+ ).toBe('left');
368
+ expect(alive(npm)).toBe(true);
369
+ expect(endRecordedInstall(lockFor(npm, at))).toBe('ended');
370
+ expect(await until(() => !alive(npm))).toBe(true);
371
+ });
372
+
373
+ it('decides from the process facts alone, however they were read (no /proc: ps or PowerShell)', async () => {
374
+ const at = Date.now() - 1000;
375
+ // A real group to end, with facts given as ps would report them, so the
376
+ // outcome never depends on what this computer's ps prints.
377
+ const npm = startGroup(process.execPath, ['-e', SLEEP]);
378
+ await new Promise(resolve => setTimeout(resolve, 300));
379
+ const factsOf =
380
+ (/** @type {string[]} */ argv, startedAt = at + 500) =>
381
+ () => ({argv, startedAt});
382
+ const installArgv = [
383
+ '/usr/bin/node',
384
+ '/x/npm/bin/npm-cli.js',
385
+ ...INSTALL,
386
+ ];
387
+ for (const [argv, startedAt] of /** @type {const} */ ([
388
+ [['/opt/my-npm-tool', ...INSTALL], at + 500],
389
+ [['/usr/bin/node', '-e', 'x', 'npm', ...INSTALL], at + 500],
390
+ [
391
+ [
392
+ 'python3',
393
+ '-c',
394
+ 'sleep',
395
+ '/home/me/notes/npm-debug.log',
396
+ ...INSTALL,
397
+ ],
398
+ at + 500,
399
+ ],
400
+ // Started before the lock: an older process that reuses the id.
401
+ [installArgv, at - 5000],
402
+ ])) {
403
+ expect(
404
+ endRecordedInstall(lockFor(npm, at), {
405
+ factsOf: factsOf([...argv], startedAt),
406
+ }),
407
+ argv.join(' '),
408
+ ).toBe('left');
409
+ }
410
+ // Facts that cannot be read: left alone.
411
+ expect(
412
+ endRecordedInstall(lockFor(npm, at), {
413
+ factsOf: () => ({argv: null, startedAt: null}),
414
+ }),
415
+ ).toBe('left');
416
+ expect(alive(npm)).toBe(true);
417
+ expect(
418
+ endRecordedInstall(lockFor(npm, at), {factsOf: factsOf(installArgv)}),
419
+ ).toBe('ended');
420
+ expect(await until(() => !alive(npm))).toBe(true);
421
+ });
422
+ },
423
+ );
424
+
425
+ describe('a declared range never reaches a shell', () => {
426
+ it('reads only version ranges as ranges', () => {
427
+ for (const ok of [
428
+ '^1.2.3',
429
+ '>=1 <2',
430
+ '1.x || 2.x',
431
+ '*',
432
+ '1.0.0-beta.1',
433
+ 'latest',
434
+ ]) {
435
+ expect(isVersionRange(ok), ok).toBe(true);
436
+ }
437
+ for (const bad of [
438
+ '^1 & echo hi',
439
+ '1 && 2',
440
+ '1|2',
441
+ '1;2',
442
+ '"1"',
443
+ '$(id)',
444
+ '`id`',
445
+ 'https://x',
446
+ '',
447
+ ' ',
448
+ ]) {
449
+ expect(isVersionRange(bad), bad).toBe(false);
450
+ }
451
+ });
452
+
453
+ it('asks npm nothing for a range with shell syntax in it', () => {
454
+ /** @type {string[][]} */
455
+ const asked = [];
456
+ const run = (/** @type {string[]} */ args) => {
457
+ asked.push(args);
458
+ return {status: 0, stdout: '"1.0.0"', stderr: ''};
459
+ };
460
+ for (const spec of ['^1 & echo hi', '^1 | tee x', '^1; rm x']) {
461
+ expect(
462
+ resolvePublishedVersion('@astryxdesign/core', spec, {run}),
463
+ ).toBeNull();
464
+ }
465
+ expect(
466
+ resolvePublishedVersion('@astryxdesign/core && x', '^1', {run}),
467
+ ).toBeNull();
468
+ expect(asked).toEqual([]);
469
+ });
470
+
471
+ it.skipIf(process.platform === 'win32')(
472
+ 'takes an npm beside the running Node, or on the PATH, that links to its entry file',
473
+ () => {
474
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-npm-link-'));
475
+ try {
476
+ // A distribution layout: npm's files away from Node's folder, and an
477
+ // npm link into them on the PATH.
478
+ const entry = path.join(
479
+ root,
480
+ 'lib',
481
+ 'node_modules_22',
482
+ 'npm',
483
+ 'bin',
484
+ 'npm-cli.js',
485
+ );
486
+ fs.mkdirSync(path.dirname(entry), {recursive: true});
487
+ fs.writeFileSync(entry, '');
488
+ const bin = path.join(root, 'bin');
489
+ fs.mkdirSync(bin);
490
+ fs.symlinkSync(entry, path.join(bin, 'npm'));
491
+ // A Node with no npm beside it.
492
+ const node = path.join(root, 'node', 'bin', 'node');
493
+ fs.mkdirSync(path.dirname(node), {recursive: true});
494
+ expect(
495
+ npmCommand({env: {PATH: bin}, platform: 'linux', execPath: node}),
496
+ ).toEqual({
497
+ command: node,
498
+ prefix: [fs.realpathSync(entry)],
499
+ shell: false,
500
+ });
501
+ // The same link beside the running Node.
502
+ fs.symlinkSync(entry, path.join(path.dirname(node), 'npm'));
503
+ expect(
504
+ npmCommand({env: {PATH: ''}, platform: 'linux', execPath: node}),
505
+ ).toEqual({
506
+ command: node,
507
+ prefix: [fs.realpathSync(entry)],
508
+ shell: false,
509
+ });
510
+ // Nothing anywhere: npm on the PATH, started directly.
511
+ expect(
512
+ npmCommand({
513
+ env: {PATH: ''},
514
+ platform: 'linux',
515
+ execPath: path.join(root, 'none', 'node'),
516
+ }),
517
+ ).toEqual({command: 'npm', prefix: [], shell: false});
518
+ } finally {
519
+ fs.rmSync(root, {recursive: true, force: true});
520
+ }
521
+ },
522
+ );
523
+
524
+ it('names why the newest release could not be asked for', () => {
525
+ /** @type {string[]} */
526
+ const reasons = [];
527
+ const onFailure = (/** @type {string} */ r) => reasons.push(r);
528
+ const missing = Object.assign(new Error('spawn npm ENOENT'), {
529
+ code: 'ENOENT',
530
+ });
531
+ expect(
532
+ resolveLatestRelease({
533
+ run: () => ({status: null, stdout: '', stderr: '', error: missing}),
534
+ onFailure,
535
+ }),
536
+ ).toBeNull();
537
+ expect(
538
+ resolveLatestRelease({
539
+ run: () => ({
540
+ status: 1,
541
+ stdout: '',
542
+ stderr: 'npm error code ECONNREFUSED\nnpm error errno ECONNREFUSED\n',
543
+ }),
544
+ onFailure,
545
+ }),
546
+ ).toBeNull();
547
+ expect(
548
+ resolveLatestRelease({
549
+ run: () => ({status: 0, stdout: '"latest"', stderr: ''}),
550
+ onFailure,
551
+ }),
552
+ ).toBeNull();
553
+ expect(reasons).toEqual([
554
+ 'npm-missing',
555
+ 'npm-failed: ECONNREFUSED',
556
+ 'bad-reply',
557
+ ]);
558
+ });
559
+
560
+ it('starts npm from its entry file with this Node, never through a shell', () => {
561
+ const npm = npmCommand();
562
+ expect(npm.shell).toBe(false);
563
+ if (npm.prefix.length > 0) {
564
+ expect(npm.command).toBe(process.execPath);
565
+ expect(npm.prefix[0]).toMatch(/npm-cli\.js$/u);
566
+ }
567
+ });
568
+ });
569
+
570
+ describe('resolvePublishedVersion', () => {
571
+ it('takes the newest version npm reports for a range', () => {
572
+ /** @type {import('./cache.mjs').runNpm} */
573
+ const run = args => {
574
+ expect(args).toEqual([
575
+ 'view',
576
+ '@astryxdesign/core@^0.6.0',
577
+ 'version',
578
+ '--json',
579
+ '--fetch-retries=1',
580
+ '--fetch-timeout=30000',
581
+ '--fetch-retry-mintimeout=2000',
582
+ '--fetch-retry-maxtimeout=10000',
583
+ ]);
584
+ return {status: 0, stdout: '["0.6.3","0.6.8"]', stderr: ''};
585
+ };
586
+ expect(resolvePublishedVersion('@astryxdesign/core', '^0.6.0', {run})).toBe(
587
+ '0.6.8',
588
+ );
589
+ });
590
+
591
+ it('is null when npm fails or answers nothing usable', () => {
592
+ expect(
593
+ resolvePublishedVersion('@astryxdesign/cli', 'latest', {
594
+ run: () => ({status: 1, stdout: '', stderr: ''}),
595
+ }),
596
+ ).toBeNull();
597
+ expect(
598
+ resolvePublishedVersion('@astryxdesign/cli', 'latest', {
599
+ run: () => ({status: 0, stdout: '""', stderr: ''}),
600
+ }),
601
+ ).toBeNull();
602
+ });
603
+ });
604
+
605
+ describe('tryLock', () => {
606
+ it('takes a free lock once, keeps a live holder, and reclaims a stale one', () => {
607
+ const dir = path.join(tempDir('lock'), '.lock-x');
608
+ expect(tryLock(dir)).toBe(true);
609
+ expect(tryLock(dir)).toBe(false);
610
+ // A live holder on this computer keeps its lock, however long it takes.
611
+ const slow = new Date(Date.now() - 25 * 60_000);
612
+ fs.utimesSync(dir, slow, slow);
613
+ expect(tryLock(dir)).toBe(false);
614
+ // Past the hard bound, even a live holder's lock is reclaimed.
615
+ const old = new Date(Date.now() - 31 * 60_000);
616
+ fs.utimesSync(dir, old, old);
617
+ expect(tryLock(dir)).toBe(true);
618
+ });
619
+
620
+ it('takes over a lock from another computer once its holder stops renewing it (b12)', () => {
621
+ const dir = path.join(tempDir('lock'), '.lock-z');
622
+ fs.mkdirSync(dir, {recursive: true});
623
+ const owner = path.join(dir, 'owner.json');
624
+ fs.writeFileSync(owner, JSON.stringify({pid: 4242, host: 'other-host'}));
625
+ const t0 = Date.now();
626
+ expect(tryLock(dir, {now: t0})).toBe(false);
627
+ expect(tryLock(dir, {now: t0 + LEASE_SILENCE_MS - 1})).toBe(false);
628
+ // The holder renews: the wait starts again from the change.
629
+ renewLock(dir);
630
+ expect(JSON.parse(fs.readFileSync(owner, 'utf8'))).toEqual({
631
+ pid: 4242,
632
+ host: 'other-host',
633
+ beat: 1,
634
+ });
635
+ expect(tryLock(dir, {now: t0 + LEASE_SILENCE_MS})).toBe(false);
636
+ expect(tryLock(dir, {now: t0 + 2 * LEASE_SILENCE_MS - 1})).toBe(false);
637
+ // Unrenewed for the whole lease, as this run saw it: taken over.
638
+ expect(tryLock(dir, {now: t0 + 2 * LEASE_SILENCE_MS})).toBe(true);
639
+ expect(JSON.parse(fs.readFileSync(owner, 'utf8'))).toEqual({
640
+ pid: process.pid,
641
+ host: os.hostname(),
642
+ at: expect.any(Number),
643
+ });
644
+ });
645
+
646
+ it('reclaims a lock with no owner record after a few seconds', () => {
647
+ const dir = path.join(tempDir('lock'), '.lock-y');
648
+ fs.mkdirSync(dir, {recursive: true});
649
+ expect(tryLock(dir)).toBe(false);
650
+ const old = new Date(Date.now() - 11_000);
651
+ fs.utimesSync(dir, old, old);
652
+ expect(tryLock(dir)).toBe(true);
653
+ });
654
+ });
655
+
656
+ describe('a slow install', () => {
657
+ it('ends in a timeout reason, never a stopped run, when npm hits the time limit', () => {
658
+ const cacheDir = tempDir('cache');
659
+ /** @type {import('./cache.mjs').runNpm} */
660
+ const run = () => ({
661
+ status: null,
662
+ signal: 'SIGTERM',
663
+ stdout: '',
664
+ stderr: '',
665
+ error: Object.assign(new Error('spawnSync npm ETIMEDOUT'), {
666
+ code: 'ETIMEDOUT',
667
+ }),
668
+ });
669
+ const result = ensureRelease('0.6.8', {cacheDir, run});
670
+ expect('reason' in result && result.reason).toMatch(
671
+ /did not finish within 15 minutes/,
672
+ );
673
+ expect('signal' in result && result.signal).toBeFalsy();
674
+ });
675
+
676
+ it('tells the caller once when it installs and once when it waits', () => {
677
+ const cacheDir = tempDir('cache');
678
+ /** @type {string[]} */
679
+ const events = [];
680
+ ensureRelease('0.6.8', {
681
+ cacheDir,
682
+ run: fakeNpm().run,
683
+ onProgress: (e, v) => events.push(`${e} ${v}`),
684
+ });
685
+ expect(events).toEqual(['install 0.6.8']);
686
+ const other = tempDir('cache');
687
+ const lock = path.join(releasesDir(other), '.lock-0.6.8');
688
+ fs.mkdirSync(lock, {recursive: true});
689
+ fs.writeFileSync(
690
+ path.join(lock, 'owner.json'),
691
+ JSON.stringify({pid: process.pid, host: os.hostname()}),
692
+ );
693
+ events.length = 0;
694
+ ensureRelease('0.6.8', {
695
+ cacheDir: other,
696
+ run: fakeNpm().run,
697
+ waitMs: 500,
698
+ onProgress: (e, v) => events.push(`${e} ${v}`),
699
+ });
700
+ expect(events).toEqual(['wait 0.6.8']);
701
+ });
702
+ });
703
+
704
+ describe('a damaged library file', () => {
705
+ it('is caught by the size record and replaced', () => {
706
+ const cacheDir = tempDir('cache');
707
+ const npm = fakeNpm();
708
+ ensureRelease('0.6.8', {cacheDir, run: npm.run});
709
+ const cli = path.join(
710
+ releasesDir(cacheDir),
711
+ '0.6.8',
712
+ 'node_modules',
713
+ '@astryxdesign',
714
+ 'cli',
715
+ );
716
+ fs.writeFileSync(path.join(cli, 'extra-module.mjs'), 'export {};\n');
717
+ forgetVerifiedReleases();
718
+ expect(cachedRelease('0.6.8', cacheDir)).toBeNull();
719
+ expect('bin' in ensureRelease('0.6.8', {cacheDir, run: npm.run})).toBe(
720
+ true,
721
+ );
722
+ expect(npm.calls).toHaveLength(2);
723
+ });
724
+ });
725
+
726
+ describe('stopped and damaged installs heal', () => {
727
+ it('takes over a lock whose holder has gone, and clears its scratch folder', () => {
728
+ const cacheDir = tempDir('cache');
729
+ const root = releasesDir(cacheDir);
730
+ const lock = path.join(root, '.lock-0.6.8');
731
+ fs.mkdirSync(lock, {recursive: true});
732
+ // A process id no run has: the holder was killed.
733
+ fs.writeFileSync(
734
+ path.join(lock, 'owner.json'),
735
+ JSON.stringify({pid: 2 ** 22 + 7, host: os.hostname()}),
736
+ );
737
+ fs.mkdirSync(path.join(root, '.tmp-0.6.8-999-abc'), {recursive: true});
738
+ const started = Date.now();
739
+ const result = ensureRelease('0.6.8', {
740
+ cacheDir,
741
+ run: fakeNpm().run,
742
+ waitMs: 60_000,
743
+ });
744
+ expect('bin' in result).toBe(true);
745
+ expect(Date.now() - started).toBeLessThan(10_000);
746
+ expect(fs.readdirSync(root).filter(n => n.startsWith('.'))).toEqual([]);
747
+ });
748
+
749
+ it('waits for a holder that is still running', () => {
750
+ const cacheDir = tempDir('cache');
751
+ const lock = path.join(releasesDir(cacheDir), '.lock-0.6.8');
752
+ fs.mkdirSync(lock, {recursive: true});
753
+ fs.writeFileSync(
754
+ path.join(lock, 'owner.json'),
755
+ JSON.stringify({pid: process.pid, host: os.hostname()}),
756
+ );
757
+ const result = ensureRelease('0.6.8', {
758
+ cacheDir,
759
+ run: fakeNpm().run,
760
+ waitMs: 300,
761
+ });
762
+ expect('reason' in result && result.reason).toMatch(/still installing/);
763
+ });
764
+
765
+ it('replaces a release whose marker, Core, or entry file is damaged', () => {
766
+ /** @type {Array<(dir: string) => void>} */
767
+ const damage = [
768
+ dir =>
769
+ fs.writeFileSync(path.join(dir, 'release.json'), '{"version": "0.6'),
770
+ dir =>
771
+ fs.rmSync(path.join(dir, 'node_modules', '@astryxdesign', 'core'), {
772
+ recursive: true,
773
+ }),
774
+ dir =>
775
+ fs.writeFileSync(
776
+ path.join(dir, 'node_modules', '@astryxdesign', 'cli', 'bin.cjs'),
777
+ '#!/usr/bin/env node\n',
778
+ ),
779
+ ];
780
+ for (const harm of damage) {
781
+ const cacheDir = tempDir('cache');
782
+ const npm = fakeNpm();
783
+ const first = ensureRelease('0.6.8', {cacheDir, run: npm.run});
784
+ expect('bin' in first).toBe(true);
785
+ harm(path.join(releasesDir(cacheDir), '0.6.8'));
786
+ forgetVerifiedReleases();
787
+ expect(cachedRelease('0.6.8', cacheDir)).toBeNull();
788
+ const healed = ensureRelease('0.6.8', {cacheDir, run: npm.run});
789
+ expect('bin' in healed && healed.version).toBe('0.6.8');
790
+ expect(npm.calls).toHaveLength(2);
791
+ expect(cachedRelease('0.6.8', cacheDir)).not.toBeNull();
792
+ }
793
+ });
794
+
795
+ it('says the cache cannot be written instead of throwing', () => {
796
+ const parent = tempDir('ro');
797
+ const cacheDir = path.join(parent, 'cache');
798
+ // A file where the cache folder should be: no folder can be made there.
799
+ fs.writeFileSync(cacheDir, '');
800
+ const result = ensureRelease('0.6.8', {cacheDir, run: fakeNpm().run});
801
+ expect('reason' in result && result.reason).toMatch(/cannot be written/);
802
+ });
803
+ });
804
+
805
+ describe('runNpm time limit', () => {
806
+ it('ends a package manager that ignores SIGTERM, leaving no process behind', () => {
807
+ const marker = tempDir('npm-pid');
808
+ const pidFile = path.join(marker, 'pid');
809
+ const result = runNpm([], {
810
+ timeoutMs: 500,
811
+ npm: {
812
+ command: process.execPath,
813
+ prefix: [
814
+ '-e',
815
+ `require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, String(process.pid)); process.on('SIGTERM', () => {}); setTimeout(() => {}, 60000);`,
816
+ ],
817
+ shell: false,
818
+ },
819
+ });
820
+ expect(/** @type {any} */ (result.error)?.code).toBe('ETIMEDOUT');
821
+ const pid = Number(fs.readFileSync(pidFile, 'utf-8'));
822
+ expect(() => process.kill(pid, 0)).toThrow();
823
+ });
824
+ });
825
+
826
+ describe('no install outlives its run', () => {
827
+ /** @param {number} pid */
828
+ const alive = pid => {
829
+ try {
830
+ process.kill(pid, 0);
831
+ return true;
832
+ } catch {
833
+ return false;
834
+ }
835
+ };
836
+ /** @param {() => boolean} done @param {number} ms */
837
+ const waitFor = async (done, ms) => {
838
+ const end = Date.now() + ms;
839
+ while (Date.now() < end) {
840
+ if (done()) return true;
841
+ await new Promise(r => setTimeout(r, 100));
842
+ }
843
+ return done();
844
+ };
845
+
846
+ it('ends npm when the run that started it is killed outright, and removes its process-id record', async () => {
847
+ const dir = tempDir('orphan');
848
+ const pidFile = path.join(dir, 'npm-pid');
849
+ // The record a release lock keeps of npm's process id.
850
+ const record = path.join(dir, 'npm.pid');
851
+ const cacheModule = new URL('./cache.mjs', import.meta.url).href;
852
+ const sleeper = `require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, String(process.pid)); setTimeout(() => {}, 120000);`;
853
+ const script = `import(${JSON.stringify(cacheModule)}).then(m => m.runNpm([], {timeoutMs: 120000, pidFile: ${JSON.stringify(record)}, npm: {command: process.execPath, prefix: ['-e', ${JSON.stringify(sleeper)}], shell: false}}));`;
854
+ const cli = spawn(process.execPath, ['-e', script], {stdio: 'ignore'});
855
+ expect(await waitFor(() => fs.existsSync(pidFile), 10_000)).toBe(true);
856
+ expect(await waitFor(() => fs.existsSync(record), 5_000)).toBe(true);
857
+ const npmPid = Number(fs.readFileSync(pidFile, 'utf-8'));
858
+ expect(alive(npmPid)).toBe(true);
859
+ cli.kill('SIGKILL');
860
+ expect(await waitFor(() => !alive(npmPid), 5_000)).toBe(true);
861
+ // A later takeover finds no id that could now name another process.
862
+ expect(await waitFor(() => !fs.existsSync(record), 5_000)).toBe(true);
863
+ }, 30_000);
864
+
865
+ it('ends npm, which ignores Ctrl-C, when the run gets Ctrl-C', async () => {
866
+ const dir = tempDir('ctrlc');
867
+ const pidFile = path.join(dir, 'npm-pid');
868
+ const cacheModule = new URL('./cache.mjs', import.meta.url).href;
869
+ const sleeper = `require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, String(process.pid)); process.on('SIGINT', () => {}); setTimeout(() => {}, 120000);`;
870
+ const script = `import(${JSON.stringify(cacheModule)}).then(m => m.runNpm([], {timeoutMs: 120000, npm: {command: process.execPath, prefix: ['-e', ${JSON.stringify(sleeper)}], shell: false}}));`;
871
+ // Its own group, as a terminal's foreground job: Ctrl-C reaches the group.
872
+ const cli = spawn(process.execPath, ['-e', script], {
873
+ stdio: 'ignore',
874
+ detached: true,
875
+ });
876
+ expect(await waitFor(() => fs.existsSync(pidFile), 10_000)).toBe(true);
877
+ const npmPid = Number(fs.readFileSync(pidFile, 'utf-8'));
878
+ const exited = new Promise(resolve => cli.on('exit', resolve));
879
+ process.kill(-(/** @type {number} */ (cli.pid)), 'SIGINT');
880
+ expect(await waitFor(() => !alive(npmPid), 5_000)).toBe(true);
881
+ await exited;
882
+ }, 30_000);
883
+
884
+ it("a run that takes over a dead run's lock ends the install it recorded", async () => {
885
+ const cacheDir = tempDir('cache');
886
+ const lock = path.join(releasesDir(cacheDir), '.lock-0.6.8');
887
+ fs.mkdirSync(lock, {recursive: true});
888
+ // An install left running by a run that was killed: its own group, node
889
+ // running npm's entry file to install this release.
890
+ const at = Date.now() - 1000;
891
+ const npmCli = path.join(tempDir('npm'), 'npm-cli.js');
892
+ fs.writeFileSync(npmCli, 'setTimeout(() => {}, 120000)');
893
+ const leftover = spawn(
894
+ process.execPath,
895
+ [
896
+ npmCli,
897
+ 'install',
898
+ '@astryxdesign/cli@0.6.8',
899
+ '@astryxdesign/core@0.6.8',
900
+ ],
901
+ {
902
+ detached: true,
903
+ stdio: 'ignore',
904
+ },
905
+ );
906
+ leftover.unref();
907
+ await new Promise(resolve => setTimeout(resolve, 300));
908
+ fs.writeFileSync(
909
+ path.join(lock, 'owner.json'),
910
+ JSON.stringify({pid: 2 ** 22 + 7, host: os.hostname(), at}),
911
+ );
912
+ fs.writeFileSync(path.join(lock, 'npm.pid'), String(leftover.pid));
913
+ expect(alive(/** @type {number} */ (leftover.pid))).toBe(true);
914
+ const result = ensureRelease('0.6.8', {cacheDir, run: fakeNpm().run});
915
+ expect('bin' in result).toBe(true);
916
+ expect(
917
+ await waitFor(() => !alive(/** @type {number} */ (leftover.pid)), 5_000),
918
+ ).toBe(true);
919
+ }, 30_000);
920
+ });