one-code-extension 0.4.0 → 0.5.0

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 (419) hide show
  1. package/README.md +219 -219
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +66 -34
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +123 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +52 -0
  63. package/extensions/background/wakeup.ts +159 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +88 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +19 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -0,0 +1,570 @@
1
+ /**
2
+ * The local artifact store (pure fs, no pi imports): self-contained HTML pages
3
+ * the model publishes, kept under `~/.onecode/artifacts/` and viewed in the
4
+ * user's own browser through `file:` URLs. One Code's counterpart of Claude
5
+ * Code's hosted Artifact tool (`working-docs/decisions/tools.md`, "Artifacts").
6
+ *
7
+ * artifacts/index.html the gallery (gallery.ts), rewritten on every change
8
+ * artifacts/<id>/index.html the current version
9
+ * artifacts/<id>/view.html the page under a viewer bar (viewer.ts), what the browser opens
10
+ * artifacts/<id>/meta.json title, description, source path, project, version
11
+ * artifacts/<id>/download.js the current page as a script payload (see gallery.ts)
12
+ * artifacts/<id>/versions/v<N>.html every earlier version, never rewritten
13
+ * artifacts/<id>/versions/v<N>.js its download payload
14
+ *
15
+ * The meta files are the only source of truth; the gallery is regenerated from
16
+ * them, so two sessions publishing at once cannot leave it naming a missing
17
+ * page for longer than the next write. A publish stages every file before it
18
+ * replaces any (commitFiles), and an update holds the artifact's `.lock`, so
19
+ * a failed write or a concurrent publish never loses a version.
20
+ *
21
+ * Publishing the same source file again updates that artifact in place, as
22
+ * Claude Code's republish-by-path does; an artifact from an earlier session is
23
+ * updated by passing its id.
24
+ */
25
+
26
+ import { randomUUID } from "node:crypto";
27
+ import { chmodSync, copyFileSync, existsSync, constants as fsConstants, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
28
+ import { uptime } from "node:os";
29
+ import { basename, extname, join, posix, resolve } from "node:path";
30
+ import { fileURLToPath, pathToFileURL } from "node:url";
31
+ import { readJsonFile, writeTextAtomic } from "../lib/atomic-write.ts";
32
+ import { comparablePath, isPathAtOrUnder, oneCodeStateDir, tildify } from "../lib/paths.ts";
33
+ import { sanitizeDisplayText } from "../lib/terminal-text.ts";
34
+ import { renderDownloadPayload, renderGallery } from "./gallery.ts";
35
+ import { renderViewer } from "./viewer.ts";
36
+
37
+ export interface ArtifactMeta {
38
+ id: string;
39
+ title: string;
40
+ description?: string;
41
+ /** The absolute path the page was last published from. */
42
+ sourcePath: string;
43
+ /** The project (git root, else cwd) of the session that first published it. */
44
+ project?: string;
45
+ createdAt: string;
46
+ updatedAt: string;
47
+ /** The current version's number; versions/v1 … v<version - 1> hold the earlier ones. */
48
+ version: number;
49
+ }
50
+
51
+ /** Claude Code's page size limit for a published artifact. */
52
+ export const MAX_ARTIFACT_BYTES = 16 * 1024 * 1024;
53
+
54
+ const ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
55
+ const HTML_EXTENSIONS = new Set([".html", ".htm"]);
56
+
57
+ /** `~/.onecode/artifacts`, honouring `ONECODE_STATE_DIR`. */
58
+ export function artifactsRoot(env: Record<string, string | undefined> = process.env, home?: string): string {
59
+ return join(oneCodeStateDir(env, home), "artifacts");
60
+ }
61
+
62
+ function isArtifactId(id: string): boolean {
63
+ return ID_PATTERN.test(id);
64
+ }
65
+
66
+ export function pagePath(root: string, id: string): string {
67
+ return join(root, id, "index.html");
68
+ }
69
+
70
+ /** The page with the viewer bar over it, what the browser opens. */
71
+ export function viewerPath(root: string, id: string): string {
72
+ return join(root, id, "view.html");
73
+ }
74
+
75
+ export function versionPath(root: string, id: string, version: number): string {
76
+ return join(root, id, "versions", `v${version}.html`);
77
+ }
78
+
79
+ export function galleryPath(root: string): string {
80
+ return join(root, "index.html");
81
+ }
82
+
83
+ export function fileUrl(path: string): string {
84
+ return pathToFileURL(path).href;
85
+ }
86
+
87
+ /**
88
+ * Create the store private to this user (0700), tightening one created
89
+ * looser, so other users on the machine cannot read the pages, which carry
90
+ * project code and data. Windows keeps the profile's own ACLs.
91
+ */
92
+ function ensureStore(root: string): void {
93
+ mkdirSync(root, { recursive: true, mode: 0o700 });
94
+ if (process.platform === "win32") return;
95
+ if (statSync(root).mode & 0o077) chmodSync(root, 0o700);
96
+ }
97
+
98
+ /** A lowercase, hyphenated slug of `text`, at most 40 characters; "page" when nothing survives. */
99
+ export function slugify(text: string): string {
100
+ const slug = text
101
+ .normalize("NFKD")
102
+ .toLowerCase()
103
+ .replace(/[^a-z0-9]+/g, "-")
104
+ .replace(/^-+|-+$/g, "")
105
+ .slice(0, 40)
106
+ .replace(/-+$/, "");
107
+ return slug || "page";
108
+ }
109
+
110
+ const ENTITIES: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
111
+
112
+ /** The page's `<title>` text, entity-decoded and whitespace-collapsed, or undefined. */
113
+ export function pageTitle(html: string): string | undefined {
114
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(html);
115
+ if (!match) return undefined;
116
+ const text = match[1]
117
+ .replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (whole, name: string) => {
118
+ if (name[0] === "#") {
119
+ const code = name[1] === "x" || name[1] === "X" ? Number.parseInt(name.slice(2), 16) : Number.parseInt(name.slice(1), 10);
120
+ return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : whole;
121
+ }
122
+ return ENTITIES[name.toLowerCase()] ?? whole;
123
+ })
124
+ .replace(/\s+/g, " ")
125
+ .trim();
126
+ return text || undefined;
127
+ }
128
+
129
+ function readMeta(root: string, id: string): ArtifactMeta | undefined {
130
+ const meta = readJsonFile<ArtifactMeta>(join(root, id, "meta.json"));
131
+ if (!meta || typeof meta !== "object" || meta.id !== id || !Number.isInteger(meta.version)) return undefined;
132
+ if ([meta.title, meta.sourcePath, meta.createdAt, meta.updatedAt].some((field) => typeof field !== "string")) return undefined;
133
+ return meta;
134
+ }
135
+
136
+ /** One artifact's meta, or undefined when the id is malformed or unknown. */
137
+ export function getArtifact(root: string, id: string): ArtifactMeta | undefined {
138
+ return isArtifactId(id) ? readMeta(root, id) : undefined;
139
+ }
140
+
141
+ /** Every artifact with a readable meta file, most recently updated first. */
142
+ export function listArtifacts(root: string): ArtifactMeta[] {
143
+ let names: string[];
144
+ try {
145
+ names = readdirSync(root);
146
+ } catch {
147
+ return [];
148
+ }
149
+ return names
150
+ .filter(isArtifactId)
151
+ .map((id) => readMeta(root, id))
152
+ .filter((meta): meta is ArtifactMeta => meta !== undefined)
153
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id));
154
+ }
155
+
156
+ /**
157
+ * The artifact id a reference names: a bare id, or a `file:` URL or path to
158
+ * anything inside that artifact's folder (what `publish` returned). Undefined
159
+ * when it names no stored artifact.
160
+ */
161
+ export function resolveArtifactRef(root: string, ref: string): string | undefined {
162
+ const trimmed = ref.trim();
163
+ if (getArtifact(root, trimmed)) return trimmed;
164
+ let path: string;
165
+ try {
166
+ path = trimmed.startsWith("file:") ? fileURLToPath(trimmed) : resolve(trimmed);
167
+ } catch {
168
+ return undefined;
169
+ }
170
+ if (!isInsideStore(root, path)) return undefined;
171
+ // The first segment below the root, on comparablePath forms (ids are lowercase).
172
+ const id = posix.relative(comparablePath(root), comparablePath(path)).split("/")[0];
173
+ return getArtifact(root, id) ? id : undefined;
174
+ }
175
+
176
+ /** Whether `path` is inside the store (a page there cannot be published as its own source). */
177
+ function isInsideStore(root: string, path: string): boolean {
178
+ return isPathAtOrUnder(path, root);
179
+ }
180
+
181
+ export interface PublishInput {
182
+ /** Absolute path of the HTML file being published. */
183
+ sourcePath: string;
184
+ html: string;
185
+ /** Fallback title for a page with no `<title>`. */
186
+ title?: string;
187
+ description?: string;
188
+ project?: string;
189
+ /** Update this artifact instead of the one published from `sourcePath`. */
190
+ id?: string;
191
+ now: Date;
192
+ /** A short random suffix that keeps two same-titled artifacts apart. */
193
+ suffix: () => string;
194
+ }
195
+
196
+ export interface PublishResult {
197
+ meta: ArtifactMeta;
198
+ created: boolean;
199
+ }
200
+
201
+ /**
202
+ * Check a page before it is stored: the reason it cannot be published, or
203
+ * undefined. Every reason names the fix.
204
+ */
205
+ export function publishProblem(root: string, sourcePath: string, bytes: number): string | undefined {
206
+ if (!HTML_EXTENSIONS.has(extname(sourcePath).toLowerCase())) {
207
+ return `${sourcePath} is not an .html file. Write the page as a single self-contained .html file (inline CSS and JS) and publish that.`;
208
+ }
209
+ if (isInsideStore(root, sourcePath)) {
210
+ return `${sourcePath} is inside the artifact store. Edit a copy in your scratchpad or the project and publish that copy, passing the artifact's id to update it in place.`;
211
+ }
212
+ if (bytes === 0) return `${sourcePath} is empty. Write the page first, then publish it.`;
213
+ if (bytes > MAX_ARTIFACT_BYTES) {
214
+ return `${sourcePath} is ${(bytes / 1024 / 1024).toFixed(1)} MB; an artifact is at most 16 MB. Move large data or media out of the page, or shrink it.`;
215
+ }
216
+ return undefined;
217
+ }
218
+
219
+ /** Store a page: a new artifact, or a new version of an existing one. */
220
+ export function publishArtifact(root: string, input: PublishInput): PublishResult {
221
+ const sourcePath = resolve(input.sourcePath);
222
+ ensureStore(root);
223
+ let found: ArtifactMeta | undefined;
224
+ if (input.id !== undefined) {
225
+ found = getArtifact(root, input.id);
226
+ if (!found) throw new Error(`No artifact has the id "${input.id}". Run the artifact tool with action "list" to see the ids.`);
227
+ } else {
228
+ // The lookup and a creation are one step under a store-wide lock, so two
229
+ // first publishes of one file cannot both miss and make two artifacts.
230
+ const key = comparablePath(sourcePath);
231
+ const outcome = withLock(join(root, ".create-lock"), "Another session is creating an artifact. Publish again in a moment.", () => {
232
+ const match = listArtifacts(root).find((meta) => comparablePath(meta.sourcePath) === key);
233
+ return match ? { match } : { created: createArtifact(root, input, sourcePath) };
234
+ });
235
+ if (outcome.created) {
236
+ writeGallery(root);
237
+ return outcome.created;
238
+ }
239
+ found = outcome.match;
240
+ }
241
+ const id = found.id;
242
+ // Re-read under the lock: another session may have published a version since.
243
+ const result = withLock(join(root, id, ".lock"), `Another session is publishing the artifact "${id}". Publish again in a moment.`, (held) => {
244
+ const existing = getArtifact(root, id);
245
+ if (!existing) throw new Error(`The artifact "${id}" was deleted while this publish ran. Publish again to create a new one.`);
246
+ return updateArtifact(root, existing, input, sourcePath, held);
247
+ });
248
+ writeGallery(root);
249
+ return result;
250
+ }
251
+
252
+ /** The page's `<title>`, else a non-blank `title` parameter, else the earlier title, else the file name. */
253
+ function titleFor(input: PublishInput, sourcePath: string, existing?: ArtifactMeta): string {
254
+ const title = pageTitle(input.html) ?? (input.title?.trim() || undefined) ?? existing?.title ?? basename(sourcePath, extname(sourcePath));
255
+ // The title reaches terminal menus: no escape sequence or control character survives.
256
+ return sanitizeDisplayText(title).replace(/\s+/g, " ").trim() || basename(sourcePath, extname(sourcePath));
257
+ }
258
+
259
+ function createArtifact(root: string, input: PublishInput, sourcePath: string): PublishResult {
260
+ const title = titleFor(input, sourcePath);
261
+ const now = input.now.toISOString();
262
+ let id = `${slugify(title)}-${input.suffix()}`;
263
+ while (existsSync(join(root, id))) id = `${slugify(title)}-${input.suffix()}`;
264
+ const meta: ArtifactMeta = { id, title, description: input.description?.trim() || undefined, sourcePath, project: input.project, createdAt: now, updatedAt: now, version: 1 };
265
+ mkdirSync(join(root, id), { recursive: true });
266
+ try {
267
+ // meta.json last: until it lands, the folder is not an artifact.
268
+ commitFiles([...pageFiles(root, meta, input.html), viewerFile(root, meta), metaFile(root, meta)]);
269
+ } catch (error) {
270
+ rmSync(join(root, id), { recursive: true, force: true });
271
+ throw error;
272
+ }
273
+ return { meta, created: true };
274
+ }
275
+
276
+ function updateArtifact(root: string, existing: ArtifactMeta, input: PublishInput, sourcePath: string, held: () => boolean): PublishResult {
277
+ const title = titleFor(input, sourcePath, existing);
278
+ const description = input.description?.trim() || existing.description;
279
+ const now = input.now.toISOString();
280
+ const current = pagePath(root, existing.id);
281
+ // A size match first, so a changed page (the usual case) is never read back.
282
+ if (existsSync(current) && statSync(current).size === Buffer.byteLength(input.html) && readFileSync(current, "utf-8") === input.html) {
283
+ // Same bytes: refresh the details without a version that differs from nothing.
284
+ // The payload is rewritten too: it names the download after the title.
285
+ const meta: ArtifactMeta = { ...existing, title, description, sourcePath, updatedAt: now };
286
+ const files = [metaFile(root, meta), pageFiles(root, meta, input.html)[1], viewerFile(root, meta)];
287
+ // The same three files as version N had them, for a rename that fails partway.
288
+ const before = [metaFile(root, existing), pageFiles(root, existing, input.html)[1], viewerFile(root, existing)];
289
+ commitFiles(files, {
290
+ beforeReplace: () => assertHeld(held, existing.id),
291
+ rollback: before.map(([target, text]) => () => writeTextAtomic(target, text)),
292
+ });
293
+ return { meta, created: false };
294
+ }
295
+ const meta: ArtifactMeta = { ...existing, title, description, sourcePath, updatedAt: now, version: existing.version + 1 };
296
+ const archivedPage = versionPath(root, existing.id, existing.version);
297
+ const livePayload = join(root, existing.id, "download.js");
298
+ const archivedPayload = join(root, existing.id, "versions", `v${existing.version}.js`);
299
+ commitFiles([metaFile(root, meta), ...pageFiles(root, meta, input.html), viewerFile(root, meta)], {
300
+ beforeReplace: () => {
301
+ // Checked before the archive is touched: a session that took the lock
302
+ // over while this one was paused owns version N and N+1 now.
303
+ assertHeld(held, existing.id);
304
+ if (!existsSync(current)) return;
305
+ mkdirSync(join(root, existing.id, "versions"), { recursive: true });
306
+ copyFileSync(current, archivedPage);
307
+ if (existsSync(livePayload)) copyFileSync(livePayload, archivedPayload);
308
+ assertHeld(held, existing.id);
309
+ },
310
+ // A rename failed partway: put every live file back as version N had it.
311
+ rollback: [
312
+ () => writeTextAtomic(join(root, existing.id, "meta.json"), metaFile(root, existing)[1]),
313
+ () => existsSync(archivedPage) && copyFileSync(archivedPage, current),
314
+ () => existsSync(archivedPayload) && copyFileSync(archivedPayload, livePayload),
315
+ () => writeTextAtomic(viewerPath(root, existing.id), viewerFile(root, existing)[1]),
316
+ ],
317
+ });
318
+ return { meta, created: false };
319
+ }
320
+
321
+ function assertHeld(held: () => boolean, id: string): void {
322
+ if (!held()) throw new Error(`Another session took over publishing the artifact "${id}". Publish again.`);
323
+ }
324
+
325
+ type StoreFile = [target: string, text: string];
326
+
327
+ function pageFiles(root: string, meta: ArtifactMeta, html: string): StoreFile[] {
328
+ return [
329
+ [pagePath(root, meta.id), html],
330
+ [join(root, meta.id, "download.js"), renderDownloadPayload(`${slugify(meta.title)}.html`, html)],
331
+ ];
332
+ }
333
+
334
+ function viewerFile(root: string, meta: ArtifactMeta): StoreFile {
335
+ return [viewerPath(root, meta.id), renderViewer(meta, join(root, meta.id))];
336
+ }
337
+
338
+ function metaFile(root: string, meta: ArtifactMeta): StoreFile {
339
+ return [join(root, meta.id, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`];
340
+ }
341
+
342
+ /**
343
+ * Write every file to a temp sibling first, run `beforeReplace` (the lock
344
+ * check and the version archive), then rename them into place in the given
345
+ * order. A failure while staging or archiving removes the temps and leaves
346
+ * the artifact untouched. A failed rename removes the remaining temps and runs
347
+ * every `rollback` step, each on its own: the file whose rename failed was
348
+ * never replaced, and its restore failing must not stop the others.
349
+ */
350
+ function commitFiles(files: StoreFile[], hooks: { beforeReplace?: () => void; rollback?: Array<() => unknown> } = {}): void {
351
+ const staged: Array<[tmp: string, target: string]> = [];
352
+ const discard = () => {
353
+ for (const [tmp] of staged) rmSync(tmp, { force: true });
354
+ };
355
+ try {
356
+ for (const [target, text] of files) {
357
+ const tmp = `${target}.next-${process.pid}`;
358
+ writeFileSync(tmp, text);
359
+ staged.push([tmp, target]);
360
+ }
361
+ hooks.beforeReplace?.();
362
+ } catch (error) {
363
+ discard();
364
+ throw error;
365
+ }
366
+ try {
367
+ for (const [tmp, target] of staged) renameSync(tmp, target);
368
+ } catch (error) {
369
+ discard();
370
+ for (const step of hooks.rollback ?? []) {
371
+ try {
372
+ step();
373
+ } catch {
374
+ // The rest still run; the original error is the one reported.
375
+ }
376
+ }
377
+ throw error;
378
+ }
379
+ }
380
+
381
+ /** Another session held the lock past the wait. */
382
+ export class LockBusyError extends Error {}
383
+
384
+ /** How long a publish waits for another session's lock. */
385
+ const LOCK_WAIT_MS = 5_000;
386
+ /** How long an ownerless lock (a crash between `mkdir` and writing the owner) is left alone. */
387
+ const LOCK_ORPHAN_MS = 30_000;
388
+ /**
389
+ * Past this age a lock is abandoned even if its pid is alive: a crashed
390
+ * holder's pid may have been reused. A publish holds its lock for
391
+ * milliseconds, so only a holder asleep this long could still resume.
392
+ */
393
+ const LOCK_MAX_AGE_MS = 10 * 60_000;
394
+
395
+ /**
396
+ * The process id in a lock's owner token (`<pid>-<uuid>`), or undefined.
397
+ */
398
+ function ownerPid(token: string): number | undefined {
399
+ const pid = Number.parseInt(token.split("-")[0] ?? "", 10);
400
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
401
+ }
402
+
403
+ /** Whether a process with this id is running (EPERM: it runs as another user). */
404
+ function processAlive(pid: number): boolean {
405
+ try {
406
+ process.kill(pid, 0);
407
+ return true;
408
+ } catch (error) {
409
+ return (error as NodeJS.ErrnoException).code === "EPERM";
410
+ }
411
+ }
412
+
413
+ /**
414
+ * The owner token of the lock at `lock` ("" for none) when it was left by a
415
+ * holder that can no longer act on it, else undefined: it predates this boot (its pid may name an unrelated process now), its
416
+ * owner process has exited, or it is this process (a lock body is
417
+ * synchronous, so this process cannot be inside one while it asks), or it has
418
+ * had no owner for 30 s. A lock whose owner is alive is never taken, however
419
+ * old: a paused holder resumes into its commit, and no check before a rename
420
+ * could fence it.
421
+ */
422
+ function abandonedToken(lock: string): string | undefined {
423
+ let mtime: number;
424
+ try {
425
+ mtime = statSync(lock).mtimeMs;
426
+ } catch {
427
+ return undefined; // released meanwhile
428
+ }
429
+ const token = readOwner(lock);
430
+ // A lock from before this boot, or older than any real publish, is
431
+ // abandoned whatever its pid now names.
432
+ if (mtime < Date.now() - uptime() * 1000 || Date.now() - mtime > LOCK_MAX_AGE_MS) return token;
433
+ if (token === "") return Date.now() - mtime > LOCK_ORPHAN_MS ? token : undefined;
434
+ const pid = ownerPid(token);
435
+ return pid === undefined || pid === process.pid || !processAlive(pid) ? token : undefined;
436
+ }
437
+
438
+ /** A lock's owner token, or "" when it has none (yet). */
439
+ function readOwner(lock: string): string {
440
+ try {
441
+ return readFileSync(join(lock, "owner"), "utf-8");
442
+ } catch {
443
+ return "";
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Run `fn` holding the lock at `lock`: a directory (`mkdir` is atomic across
449
+ * processes on one machine, which the store under `~/.onecode` is) holding an
450
+ * `owner` file with this holder's `<pid>-<uuid>` token. An abandoned lock
451
+ * (`abandonedToken`) is claimed by renaming it aside, so only one taker wins,
452
+ * and put back if what moved turns out to be a newer lock; a
453
+ * live holder's lock is waited on for 5 s, then the call fails with
454
+ * `LockBusyError`. `fn` gets `held()`, and the lock is released only while it
455
+ * is still this holder's.
456
+ */
457
+ export function withLock<T>(lock: string, busy: string, fn: (held: () => boolean) => T, waitMs = LOCK_WAIT_MS): T {
458
+ const token = `${process.pid}-${randomUUID()}`;
459
+ const owner = join(lock, "owner");
460
+ const deadline = Date.now() + waitMs;
461
+ for (;;) {
462
+ try {
463
+ mkdirSync(lock);
464
+ writeFileSync(owner, token);
465
+ break;
466
+ } catch (error) {
467
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
468
+ const judged = abandonedToken(lock);
469
+ if (judged !== undefined) {
470
+ const aside = `${lock}.stale-${token}`;
471
+ try {
472
+ renameSync(lock, aside);
473
+ } catch {
474
+ continue; // another session claimed it first; wait on theirs
475
+ }
476
+ // Between the judgement and the rename another session may have
477
+ // reclaimed the lock and made a new one here: if what moved is not
478
+ // the lock judged abandoned, put it back for its live owner.
479
+ // If the put-back fails (yet another lock is there now), the moved one
480
+ // is left aside, never removed: its owner's held() turns false and
481
+ // its publish aborts before touching live files.
482
+ if (readOwner(aside) === judged) rmSync(aside, { recursive: true, force: true });
483
+ else {
484
+ try {
485
+ renameSync(aside, lock);
486
+ } catch {
487
+ // left aside on purpose
488
+ }
489
+ }
490
+ continue;
491
+ }
492
+ if (Date.now() > deadline) throw new LockBusyError(busy);
493
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
494
+ }
495
+ }
496
+ const held = () => {
497
+ try {
498
+ return readFileSync(owner, "utf-8") === token;
499
+ } catch {
500
+ return false;
501
+ }
502
+ };
503
+ try {
504
+ return fn(held);
505
+ } finally {
506
+ if (held()) rmSync(lock, { recursive: true, force: true });
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Remove an artifact and every version of it, under its publish lock so a
512
+ * publish in flight finishes first. False when there was nothing to remove.
513
+ */
514
+ export function deleteArtifact(root: string, id: string): boolean {
515
+ if (!getArtifact(root, id)) return false;
516
+ const removed = withLock(join(root, id, ".lock"), `Another session is publishing the artifact "${id}". Delete it again in a moment.`, () => {
517
+ if (!getArtifact(root, id)) return false;
518
+ rmSync(join(root, id), { recursive: true, force: true });
519
+ return true;
520
+ });
521
+ if (removed) writeGallery(root);
522
+ return removed;
523
+ }
524
+
525
+ /**
526
+ * Rewrite the gallery page from the meta files. The snapshot is taken and
527
+ * written under a store-wide lock, so a slower session cannot put an older
528
+ * snapshot over a newer one. A gallery still locked after the wait is left to
529
+ * that session: the gallery is derived, and the next change or `open`
530
+ * rewrites it, so a publish that already landed does not report failure.
531
+ */
532
+ export function writeGallery(root: string): void {
533
+ ensureStore(root);
534
+ try {
535
+ withLock(join(root, ".gallery-lock"), "The artifact gallery is locked by another session.", (held) => {
536
+ const html = renderGallery(listArtifacts(root));
537
+ // A session that took the lock over while this one was paused has a newer snapshot.
538
+ if (held()) writeTextAtomic(galleryPath(root), html);
539
+ });
540
+ } catch (error) {
541
+ if (!(error instanceof LockBusyError)) throw error;
542
+ }
543
+ }
544
+
545
+ /** One artifact as a line of the tool's `list` output and the non-interactive /artifacts. */
546
+ export function listLine(root: string, meta: ArtifactMeta, home: string): string {
547
+ const project = meta.project ? ` [${tildify(meta.project, home)}]` : "";
548
+ return `- ${meta.id}: "${meta.title}" (v${meta.version}, updated ${meta.updatedAt.slice(0, 16).replace("T", " ")} UTC)${project}\n ${fileUrl(viewerPath(root, meta.id))}`;
549
+ }
550
+
551
+ /**
552
+ * Copy an artifact's current version to `<home>/Downloads` (`home` itself when
553
+ * there is none) as `<slug>.html`, adding -2, -3 … rather than overwriting.
554
+ * Returns the path written.
555
+ */
556
+ export function exportArtifact(root: string, meta: ArtifactMeta, home: string): string {
557
+ const downloads = join(home, "Downloads");
558
+ const dir = existsSync(downloads) ? downloads : home;
559
+ const base = slugify(meta.title);
560
+ // An exclusive copy, so two exports of one name cannot both take it.
561
+ for (let n = 1; ; n++) {
562
+ const target = join(dir, n === 1 ? `${base}.html` : `${base}-${n}.html`);
563
+ try {
564
+ copyFileSync(pagePath(root, meta.id), target, fsConstants.COPYFILE_EXCL);
565
+ return target;
566
+ } catch (error) {
567
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
568
+ }
569
+ }
570
+ }