one-code-extension 0.4.1 → 0.6.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 (439) hide show
  1. package/README.md +16 -5
  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 +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -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 +58 -31
  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 +124 -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 +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -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 +56 -0
  63. package/extensions/background/wakeup.ts +178 -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 +66 -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 +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -0,0 +1,885 @@
1
+ /**
2
+ * Bash command lines turned into simple commands, on tree-sitter-bash
3
+ * (pure apart from the grammar, lib/bash-parser.ts).
4
+ *
5
+ * `parseCommand` walks the syntax tree and returns one {@link Segment} per
6
+ * simple command, in source order: its words with quoting removed, the files
7
+ * its redirects write and read, and where it sits (inside a loop, a subshell,
8
+ * a command substitution). Every consumer (the auto-mode pre-gate, the
9
+ * permission matcher's deny forms, the bash and worktree guards, the safety
10
+ * floor) judges those segments, so the walker only has to read bash right.
11
+ *
12
+ * ## Fail closed
13
+ *
14
+ * The grammar is not bash, and the walker trusts it only as far as it has to:
15
+ *
16
+ * - A tree with an ERROR or MISSING node is `parseFailed`. The segments found
17
+ * around the error are still returned, best effort, for the deny forms,
18
+ * which may only widen; every other caller ignores them.
19
+ * - A construct the pre-gate does not model (a loop, a subshell, a function,
20
+ * `[[ ]]`, `$(( ))`, brace expansion, …) sets `complex`; the pre-gate
21
+ * escalates on it. The commands inside are still walked, so deny forms and
22
+ * guards see them.
23
+ * - The grammar's known gaps are corrected here: words after a redirect's
24
+ * target belong to the command (`cmd 2>/dev/null arg`), `$"…"` keeps its
25
+ * `$` only as a separate token, and `{a,b}` is plain words to the grammar
26
+ * (findings §30).
27
+ */
28
+
29
+ import { bashParserUnavailable, parseBash } from "../lib/bash-parser.ts";
30
+
31
+ type SyntaxNode = NonNullable<ReturnType<NonNullable<ReturnType<typeof parseBash>>["rootNode"]["child"]>>;
32
+
33
+ /**
34
+ * What makes a word's value unknown until bash runs it, most telling first:
35
+ * a parameter or arithmetic expansion (`$HOME`, `$1`, `$((…))`), whose value
36
+ * comes from the environment; a command substitution (`$(…)`, a backtick,
37
+ * `>(…)`), whose value is its command's output; or a whole word that is one
38
+ * input process substitution (`<(cmd)`), which bash replaces with a pipe
39
+ * path the command reads.
40
+ */
41
+ export type Dynamic = "variable" | "substitution" | "process-input";
42
+
43
+ const DYNAMIC_RANK: Record<Dynamic, number> = { variable: 3, substitution: 2, "process-input": 1 };
44
+
45
+ /** The more telling of two kinds (see {@link Dynamic}). */
46
+ function mergeDynamic(a: Dynamic | undefined, b: Dynamic | undefined): Dynamic | undefined {
47
+ if (!a) return b;
48
+ if (!b) return a;
49
+ return DYNAMIC_RANK[a] >= DYNAMIC_RANK[b] ? a : b;
50
+ }
51
+
52
+ export interface Token {
53
+ /** The word with quotes removed, escapes applied and `$'…'` decoded; expansions keep their source text (`$HOME`). */
54
+ value: string;
55
+ /** True when an unquoted, unescaped `*`, `?` or `[` makes the token a glob bash expands. */
56
+ glob?: boolean;
57
+ /** Set when part of the value is only known when bash runs it. */
58
+ dynamic?: Dynamic;
59
+ }
60
+
61
+ export interface Segment {
62
+ /** Leading `NAME=value` assignments, then the command word, then its arguments. */
63
+ tokens: Token[];
64
+ /** Source ranges of the command words only, never redirects or stdin. Offsets are into the parsed line. */
65
+ wordRanges?: { start: number; end: number }[];
66
+ /**
67
+ * Write targets (`>`, `>>`, `>|`, `&>`, `&>>`, `>&word`), in written order.
68
+ * A redirect on a compound command (`{ …; } > f`) gets a segment of its own
69
+ * with no tokens, like a bare `> f`.
70
+ */
71
+ redirects: string[];
72
+ /**
73
+ * Input targets (`<`, `<&word`): files the shell opens on the command's
74
+ * stdin, whatever the command is. Kept out of `tokens`, where `< f cmd` would
75
+ * make `f` the command word.
76
+ */
77
+ inputs: Token[];
78
+ /** The command's source text: its words and its own redirects (a heredoc up to its delimiter). */
79
+ raw: string;
80
+ /**
81
+ * The constructs around the command, outermost first, as tree-sitter node
82
+ * types (`while_statement`, `do_group`, `subshell`, `command_substitution`,
83
+ * …). Lists, pipelines and redirects are not constructs.
84
+ */
85
+ enclosing: string[];
86
+ /**
87
+ * The subshells the command runs in, outermost first, each by a number
88
+ * unique within the parse: `( … )`, a command or process substitution, and
89
+ * each command of a multi-command pipeline. A `cd` changes the directory of
90
+ * later commands only in its own scope.
91
+ */
92
+ scopes: number[];
93
+ /**
94
+ * Set for a command inside a command or process substitution: the index,
95
+ * in `ParseResult.substitutions`, of the outermost substitution around it.
96
+ */
97
+ substitution?: number;
98
+ /**
99
+ * True for a command in the last member of a multi-command pipeline. Bash
100
+ * runs that member in a subshell by default but in the current shell under
101
+ * `shopt -s lastpipe`, so a `cd` there may or may not move the commands
102
+ * after the pipeline; directory tracking treats it as unknown.
103
+ */
104
+ lastInPipeline?: boolean;
105
+ /**
106
+ * Set for a command in the last member of a multi-command pipeline in any
107
+ * shell, a substitution's or a `( … )`'s included: the scopes (as in
108
+ * `scopes`) of the shell that member runs in under `lastpipe`, the one
109
+ * around the pipeline. A `cd` there may move the later commands of that
110
+ * shell. `lastInPipeline` is the subset that can move the outer shell.
111
+ */
112
+ pipelineShell?: number[];
113
+ /**
114
+ * True when the command may not run even though the line reaches it: the
115
+ * right side of `&&` or `||`, a branch of `if` or `case`, a loop body, a
116
+ * function body. A `cd` here may or may not move the commands after it.
117
+ */
118
+ conditional?: boolean;
119
+ /**
120
+ * Set for a simple command that is itself a member of an `&&` list with no
121
+ * `||` (`a && b && c`): the list's number, unique within the parse. A member
122
+ * runs only once every earlier member ran and succeeded, so a `cd` earlier
123
+ * in the same list surely moved it, `conditional` or not.
124
+ */
125
+ andList?: number;
126
+ /** True when a redirect target's value is only known when bash runs it (`> "$f"`, `< $(ls)`). */
127
+ unknownTarget?: boolean;
128
+ /**
129
+ * True when a heredoc with an unquoted delimiter or a here-string expands a
130
+ * parameter into the command's input (`cat <<< "$TOKEN"`).
131
+ */
132
+ expandsIntoInput?: boolean;
133
+ /**
134
+ * The text each heredoc and here-string feeds the command's input, as
135
+ * written: data to most commands, a script to a shell reading its input
136
+ * (`sh <<'EOF'`, `cat <<EOF | sh`).
137
+ */
138
+ stdin?: string[];
139
+ }
140
+
141
+ export interface ParseResult {
142
+ segments: Segment[];
143
+ /**
144
+ * The command did not parse cleanly (an unbalanced quote, a syntax error, a
145
+ * construct the grammar lacks such as `<>`), or the grammar is not loaded.
146
+ * Nothing about it is known; `segments` is a best-effort reading that only
147
+ * the deny forms may use.
148
+ */
149
+ parseFailed: boolean;
150
+ /** Set with `parseFailed` when the grammar is not loaded: why. */
151
+ unavailable?: string;
152
+ /**
153
+ * Why a word was quoted in a way whose value this check cannot know, or
154
+ * undefined: `$"…"` locale quoting (bash translates it through a message
155
+ * catalog), or a `$'…'` escape whose meaning differs between bash
156
+ * versions. The whole command escalates.
157
+ */
158
+ unknownQuoting?: string;
159
+ /** The first construct the pre-gate does not model, as a reason ("uses a for loop"), or undefined. */
160
+ complex?: string;
161
+ /** An expansion outside simple-command words (e.g. `((…))` or a case subject) may change shell state. */
162
+ unattributedExpansion?: boolean;
163
+ /**
164
+ * True when the line defines a shell function, which may replace any later
165
+ * command. Set even when the body holds no simple command (`cat() ((1))`),
166
+ * so no segment's `enclosing` shows it.
167
+ */
168
+ definesFunction?: boolean;
169
+ /** True when a command runs in the background (`cmd &`). */
170
+ background: boolean;
171
+ /**
172
+ * The command text of each outermost substitution (inside `$(…)`, the
173
+ * backticks or `<(…)`), indexed by `Segment.substitution`.
174
+ */
175
+ substitutions: string[];
176
+ /**
177
+ * The member scopes (as in `Segment.scopes`) of each multi-command
178
+ * pipeline, in order: which subshells feed which.
179
+ */
180
+ pipelines: number[][];
181
+ }
182
+
183
+ const ANSI_C_SIMPLE: Record<string, string> = {
184
+ a: "\x07",
185
+ b: "\b",
186
+ e: "\x1b",
187
+ E: "\x1b",
188
+ f: "\f",
189
+ n: "\n",
190
+ r: "\r",
191
+ t: "\t",
192
+ v: "\v",
193
+ "\\": "\\",
194
+ "'": "'",
195
+ '"': '"',
196
+ "?": "?",
197
+ };
198
+
199
+ /** Consecutive characters from `body[from]` that match `pattern`, at most `max` of them. */
200
+ function scanWhile(body: string, from: number, pattern: RegExp, max: number): string {
201
+ let j = from;
202
+ while (j < body.length && j < from + max && pattern.test(body[j])) j++;
203
+ return body.slice(from, j);
204
+ }
205
+
206
+ /**
207
+ * Decode a `$'…'` string starting at `start` (the character after the opening
208
+ * quote) the way bash does: the simple escapes, `\NNN` (one to three octal
209
+ * digits), `\xHH`, `\uHHHH`, `\UHHHHHHHH` and `\cX`; a backslash escapes the
210
+ * closing quote (`$'it\'s'`); any other backslash stays literal. The word ends
211
+ * at a decoded NUL, as bash's C strings do. Returns the decoded text, the index
212
+ * just past the closing quote (`body.length` when it is unterminated), and
213
+ * whether an escape's meaning depends on the bash version.
214
+ *
215
+ * Until 2026-09-24 `\0` matched alone, so `\057` decoded to NUL plus "57"
216
+ * instead of `/`, and a path spelled that way skipped every path check
217
+ * (PREGATE-REVIEW-2026-09-23 P1).
218
+ */
219
+ export function decodeAnsiC(body: string, start = 0): { text: string; end: number; versionDependent: boolean } {
220
+ let out = "";
221
+ let versionDependent = false;
222
+ // Set once a decoded NUL ends the string value; the scan continues to the
223
+ // real closing quote so the caller resumes the command in the right place.
224
+ let truncated = false;
225
+ const emit = (text: string) => {
226
+ if (!truncated) out += text;
227
+ };
228
+ let i = start;
229
+ for (; i < body.length; i++) {
230
+ const ch = body[i];
231
+ if (ch === "'") return { text: out, end: i + 1, versionDependent };
232
+ if (ch !== "\\" || i + 1 >= body.length) {
233
+ emit(ch);
234
+ continue;
235
+ }
236
+ const next = body[i + 1];
237
+ let decoded: string | undefined;
238
+ let width = 2;
239
+ if (next in ANSI_C_SIMPLE) {
240
+ decoded = ANSI_C_SIMPLE[next];
241
+ } else if (/[0-7]/.test(next)) {
242
+ const octal = scanWhile(body, i + 1, /[0-7]/, 3);
243
+ decoded = String.fromCharCode(Number.parseInt(octal, 8) & 0xff);
244
+ width = 1 + octal.length;
245
+ } else if (next === "x" || next === "u" || next === "U") {
246
+ if (next !== "x") versionDependent = true;
247
+ const hex = scanWhile(body, i + 2, /[0-9a-fA-F]/, next === "x" ? 2 : next === "u" ? 4 : 8);
248
+ if (hex) {
249
+ decoded = String.fromCodePoint(Math.min(Number.parseInt(hex, 16), 0x10ffff));
250
+ width = 2 + hex.length;
251
+ }
252
+ } else if (next === "c" && i + 2 < body.length) {
253
+ const target = body[i + 2];
254
+ if (target === "?" || target === "\\") versionDependent = true;
255
+ decoded = target === "?" ? "\x7f" : String.fromCharCode(target.toUpperCase().charCodeAt(0) & 0x1f);
256
+ width = 3;
257
+ }
258
+ if (decoded === undefined) {
259
+ emit(ch);
260
+ continue;
261
+ }
262
+ if (decoded === "\0") truncated = true;
263
+ else emit(decoded);
264
+ i += width - 1;
265
+ }
266
+ return { text: out, end: body.length, versionDependent };
267
+ }
268
+
269
+ /** Nodes that are commands or hold commands; everything else in command position is a word. */
270
+ const STATEMENTS = new Set([
271
+ "program",
272
+ "list",
273
+ "pipeline",
274
+ "redirected_statement",
275
+ "command",
276
+ "declaration_command",
277
+ "unset_command",
278
+ "test_command",
279
+ "variable_assignment",
280
+ "variable_assignments",
281
+ "negated_command",
282
+ "subshell",
283
+ "compound_statement",
284
+ "for_statement",
285
+ "c_style_for_statement",
286
+ "while_statement",
287
+ "if_statement",
288
+ "elif_clause",
289
+ "else_clause",
290
+ "case_statement",
291
+ "case_item",
292
+ "do_group",
293
+ "function_definition",
294
+ ]);
295
+
296
+ /** Constructs the pre-gate does not model, and how its escalation note names them. */
297
+ const COMPLEX: Record<string, string> = {
298
+ subshell: "runs commands in a ( … ) subshell",
299
+ compound_statement: "groups commands in { … }",
300
+ for_statement: "uses a for loop",
301
+ c_style_for_statement: "uses a for loop",
302
+ while_statement: "uses a while/until loop",
303
+ if_statement: "uses an if statement",
304
+ case_statement: "uses a case statement",
305
+ function_definition: "defines a shell function",
306
+ test_command: "uses a [ ]/[[ ]] test",
307
+ declaration_command: "declares or exports variables",
308
+ unset_command: "unsets variables",
309
+ arithmetic_expansion: "uses $(( )) arithmetic expansion",
310
+ brace_expression: "uses brace expansion, whose expanded paths cannot be checked",
311
+ };
312
+
313
+ /** Constructs bash always runs in a subshell. */
314
+ const ALWAYS_SUBSHELL = new Set(["subshell", "command_substitution", "process_substitution"]);
315
+
316
+ /** Constructs whose commands may not run when the line reaches them (`Segment.conditional`). */
317
+ const CONDITIONAL = new Set(["if_statement", "case_statement", "function_definition", "for_statement", "c_style_for_statement", "while_statement"]);
318
+
319
+ /** Constructs recorded in `Segment.enclosing`. */
320
+ const ENCLOSING = new Set([
321
+ ...Object.keys(COMPLEX),
322
+ "elif_clause",
323
+ "else_clause",
324
+ "case_item",
325
+ "do_group",
326
+ "negated_command",
327
+ "command_substitution",
328
+ "process_substitution",
329
+ ]);
330
+
331
+ const LOCALE_QUOTING = 'uses $"…" locale quoting, whose translation this check cannot see';
332
+
333
+ interface Context {
334
+ enclosing: string[];
335
+ scopes: number[];
336
+ /** The outermost substitution the walk is inside, as an index into `Walker.substitutions`. */
337
+ substitution?: number;
338
+ /** Inside the last member of a multi-command pipeline (`Segment.lastInPipeline`). */
339
+ lastInPipeline?: boolean;
340
+ /** Inside the last member of a multi-command pipeline in any shell: the shell it runs in (`Segment.pipelineShell`). */
341
+ pipelineShell?: number[];
342
+ /** Inside something that may not run (`Segment.conditional`). */
343
+ conditional?: boolean;
344
+ /** A direct member of an `&&`-only list (`Segment.andList`); `enter` drops it. */
345
+ andList?: number;
346
+ /**
347
+ * Inside a context bash always runs in a subshell (`( … )`, a substitution,
348
+ * a pipeline member other than the last): nothing here, a later pipeline's
349
+ * last member included, can move the outer shell.
350
+ */
351
+ isolated?: boolean;
352
+ }
353
+
354
+ class Walker {
355
+ readonly segments: (Segment & { start: number })[] = [];
356
+ unknownQuoting: string | undefined;
357
+ complex: string | undefined;
358
+ unattributedExpansion = false;
359
+ definesFunction = false;
360
+ background = false;
361
+ /** Set when a part parsed on its own (a heredoc's backtick body) did not parse. */
362
+ failed = false;
363
+ readonly substitutions: string[] = [];
364
+ readonly pipelines: number[][] = [];
365
+ private nextScope = 1;
366
+ private nextAndList = 1;
367
+ private readonly source: string;
368
+
369
+ constructor(source: string) {
370
+ this.source = source;
371
+ }
372
+
373
+ private markComplex(type: string): void {
374
+ if (type === "function_definition") this.definesFunction = true;
375
+ this.complex ??=COMPLEX[type] ?? `uses ${type.replace(/_/g, " ")}, which this check does not model`;
376
+ }
377
+
378
+ private enter(ctx: Context, node: SyntaxNode, subshell: boolean): Context {
379
+ return {
380
+ enclosing: ENCLOSING.has(node.type) ? [...ctx.enclosing, node.type] : ctx.enclosing,
381
+ scopes: subshell ? [...ctx.scopes, this.nextScope++] : ctx.scopes,
382
+ substitution: ctx.substitution,
383
+ // `( … )` and substitutions are always subshells, whatever pipeline they sit in.
384
+ lastInPipeline: ALWAYS_SUBSHELL.has(node.type) ? false : ctx.lastInPipeline,
385
+ pipelineShell: ALWAYS_SUBSHELL.has(node.type) ? undefined : ctx.pipelineShell,
386
+ conditional: ctx.conditional || CONDITIONAL.has(node.type),
387
+ isolated: ctx.isolated || ALWAYS_SUBSHELL.has(node.type),
388
+ };
389
+ }
390
+
391
+ /** Enter a command or process substitution whose command text is `body`. */
392
+ private enterSubstitution(ctx: Context, type: string, body: string): Context {
393
+ const inner = this.enter(ctx, { type } as SyntaxNode, true);
394
+ if (inner.substitution === undefined) {
395
+ inner.substitution = this.substitutions.length;
396
+ this.substitutions.push(body);
397
+ }
398
+ return inner;
399
+ }
400
+
401
+ /** Walk a node that holds commands. */
402
+ statement(node: SyntaxNode, ctx: Context): void {
403
+ switch (node.type) {
404
+ case "comment":
405
+ return;
406
+ case "program":
407
+ for (const child of node.children) {
408
+ if (child.type === "&") this.background = true;
409
+ else if (child.isNamed) this.statement(child, ctx);
410
+ }
411
+ return;
412
+ case "list": {
413
+ // `a && b`, `a || b`: only the first command is sure to run. The
414
+ // grammar nests a longer list on the left (`(a && b) && c`); an
415
+ // `&&`-only one is walked flat, its members numbered as one list.
416
+ const members: SyntaxNode[] = [];
417
+ const operators: string[] = [];
418
+ const flatten = (list: SyntaxNode) => {
419
+ for (const child of list.children) {
420
+ if (child.type === "&") this.background = true;
421
+ else if (child.type === "list" && members.length === 0) flatten(child);
422
+ else if (child.isNamed) members.push(child);
423
+ else operators.push(child.type);
424
+ }
425
+ };
426
+ flatten(node);
427
+ const andList = operators.length > 0 && operators.every((operator) => operator === "&&") ? this.nextAndList++ : undefined;
428
+ for (const [index, child] of members.entries()) {
429
+ this.statement(child, { ...ctx, conditional: index > 0 || ctx.conditional || undefined, andList });
430
+ }
431
+ return;
432
+ }
433
+ case "pipeline": {
434
+ const members = node.namedChildren.filter((child) => child.type !== "comment");
435
+ const memberScopes: number[] = [];
436
+ if (members.length > 1) this.pipelines.push(memberScopes);
437
+ for (const [index, child] of members.entries()) {
438
+ if (members.length === 1) {
439
+ this.statement(child, ctx);
440
+ continue;
441
+ }
442
+ const member = this.enter(ctx, node, true);
443
+ memberScopes.push(member.scopes[member.scopes.length - 1]);
444
+ // Only the last member can run in the current shell (`lastpipe`),
445
+ // and only when the pipeline is not itself inside a subshell.
446
+ const last = index === members.length - 1;
447
+ member.lastInPipeline = last && !ctx.isolated;
448
+ // A tail inside another tail runs, under lastpipe, where that one does.
449
+ member.pipelineShell = last ? (ctx.pipelineShell ?? ctx.scopes) : undefined;
450
+ member.isolated = ctx.isolated || !last;
451
+ this.statement(child, member);
452
+ }
453
+ return;
454
+ }
455
+ case "redirected_statement":
456
+ this.redirected(node, ctx);
457
+ return;
458
+ case "command":
459
+ case "declaration_command":
460
+ case "unset_command":
461
+ case "test_command":
462
+ case "variable_assignment":
463
+ case "variable_assignments":
464
+ this.simple(node, [], ctx);
465
+ return;
466
+ case "negated_command":
467
+ for (const child of node.namedChildren) this.statement(child, this.enter(ctx, node, false));
468
+ return;
469
+ case "subshell":
470
+ this.markComplex(node.type);
471
+ this.container(node, this.enter(ctx, node, true));
472
+ return;
473
+ case "ERROR":
474
+ this.container(node, ctx);
475
+ return;
476
+ default:
477
+ if (!STATEMENTS.has(node.type)) {
478
+ // A word in statement position (a `for` list, a `case` subject):
479
+ // walk its substitutions, but keep any unmodelled state change
480
+ // visible to consumers proving commands independently.
481
+ if (this.word(node, ctx).dynamic) this.unattributedExpansion = true;
482
+ return;
483
+ }
484
+ this.markComplex(node.type);
485
+ this.container(node, this.enter(ctx, node, false));
486
+ }
487
+ }
488
+
489
+ /** Walk every child of a construct: statements as statements, words for their substitutions. */
490
+ private container(node: SyntaxNode, ctx: Context): void {
491
+ for (const child of node.children) {
492
+ if (child.type === "&") this.background = true;
493
+ else if (child.isNamed) this.statement(child, ctx);
494
+ }
495
+ }
496
+
497
+ private redirected(node: SyntaxNode, ctx: Context): void {
498
+ const body = node.childForFieldName("body");
499
+ // Node objects are fresh wrappers on every access: compare ids, not identity.
500
+ const redirects = node.children.filter((child) => child.id !== body?.id && child.isNamed && child.type !== "comment");
501
+ if (body && (body.type === "command" || body.type === "declaration_command" || body.type === "unset_command" || body.type === "test_command" || body.type === "variable_assignment")) {
502
+ this.simple(body, redirects, ctx);
503
+ return;
504
+ }
505
+ // A bare redirect (`> f`) or one on a compound command (`{ …; } > f`,
506
+ // `while …; done < f`): the redirect is a segment of its own.
507
+ if (body) this.statement(body, ctx);
508
+ this.simple(undefined, redirects, ctx, redirects[0]?.startIndex ?? node.startIndex);
509
+ }
510
+
511
+ /**
512
+ * One simple command, with the redirects that apply to it. `node` is
513
+ * undefined for a redirect-only segment.
514
+ */
515
+ private simple(node: SyntaxNode | undefined, outerRedirects: SyntaxNode[], ctx: Context, at = node?.startIndex ?? 0): void {
516
+ const segment: Segment & { start: number } = { tokens: [], redirects: [], inputs: [], raw: "", enclosing: ctx.enclosing, scopes: ctx.scopes, start: at };
517
+ if (ctx.substitution !== undefined) segment.substitution = ctx.substitution;
518
+ if (ctx.lastInPipeline) segment.lastInPipeline = true;
519
+ if (ctx.pipelineShell) segment.pipelineShell = ctx.pipelineShell;
520
+ if (ctx.conditional) segment.conditional = true;
521
+ if (ctx.andList !== undefined) segment.andList = ctx.andList;
522
+ if (node?.type === "variable_assignment" || node?.type === "variable_assignments") {
523
+ // A line that only assigns: `a=1`, `a=1 b=2`.
524
+ const assignments = node.type === "variable_assignment" ? [node] : node.namedChildren.filter((child) => child.type === "variable_assignment");
525
+ for (const assignment of assignments) this.addWord(segment, assignment, this.assignment(assignment, ctx));
526
+ } else if (node) {
527
+ if (node.type in COMPLEX) this.markComplex(node.type);
528
+ let locale = false;
529
+ for (const child of node.children) {
530
+ if (child.type === "$") {
531
+ // `$"…"`: the grammar leaves the `$` outside the string node.
532
+ locale = true;
533
+ continue;
534
+ }
535
+ if (!child.isNamed) {
536
+ // The keyword of `export`/`unset`/`[[`: part of the command's words.
537
+ if (node.type !== "command" && /^[A-Za-z[\]]/.test(child.type)) this.addWord(segment, child, { value: child.text });
538
+ continue;
539
+ }
540
+ if (child.type === "comment") continue;
541
+ if (child.type === "file_redirect") {
542
+ this.redirect(child, segment, ctx);
543
+ continue;
544
+ }
545
+ if (child.type === "herestring_redirect") {
546
+ this.redirect(child, segment, ctx);
547
+ continue;
548
+ }
549
+ if (child.type === "variable_assignment") {
550
+ this.addWord(segment, child, this.assignment(child, ctx));
551
+ continue;
552
+ }
553
+ // `[[ -f x ]]`: its expressions are words for the deny forms.
554
+ if (child.type.endsWith("_expression") && node.type === "test_command") {
555
+ for (const leaf of leaves(child)) this.addWord(segment, leaf, this.word(leaf, ctx));
556
+ continue;
557
+ }
558
+ const target = child.type === "command_name" ? (child.firstNamedChild ?? child) : child;
559
+ const word = this.word(target, ctx);
560
+ if (locale && target.type === "string") this.unknownQuoting ??= LOCALE_QUOTING;
561
+ locale = false;
562
+ this.addWord(segment, child, word);
563
+ }
564
+ }
565
+ for (const redirect of outerRedirects) this.redirect(redirect, segment, ctx);
566
+ segment.raw = this.rawText(node, outerRedirects, at);
567
+ this.segments.push(segment);
568
+ }
569
+
570
+ private addWord(segment: Segment, node: SyntaxNode, word: Token): void {
571
+ segment.tokens.push(word);
572
+ (segment.wordRanges ??= []).push({ start: node.startIndex, end: node.endIndex });
573
+ }
574
+
575
+ /** The source of a command plus its own redirects, a heredoc cut at its delimiter word. */
576
+ private rawText(node: SyntaxNode | undefined, redirects: SyntaxNode[], at: number): string {
577
+ let end = node?.endIndex ?? at;
578
+ for (const redirect of redirects) {
579
+ if (redirect.type === "heredoc_redirect") {
580
+ // Up to the delimiter word, and any redirect after it (`cat <<EOF > f`),
581
+ // but not the body or a pipeline the grammar nests there.
582
+ for (const child of redirect.children) {
583
+ if (child.type === "heredoc_start" || child.type === "file_redirect" || child.type === "herestring_redirect") end = Math.max(end, child.endIndex);
584
+ }
585
+ } else {
586
+ end = Math.max(end, redirect.endIndex);
587
+ }
588
+ }
589
+ return this.source.slice(node?.startIndex ?? at, end).trim();
590
+ }
591
+
592
+ private assignment(node: SyntaxNode, ctx: Context): Token {
593
+ const value = node.childForFieldName("value");
594
+ const operator = node.children.find((child) => !child.isNamed && (child.type === "=" || child.type === "+="));
595
+ const name = node.childForFieldName("name")?.text ?? "";
596
+ if (!value) return { value: `${name}${operator?.type ?? "="}` };
597
+ const word = this.word(value, ctx);
598
+ return { value: `${name}${operator?.type ?? "="}${word.value}`, glob: word.glob, dynamic: word.dynamic };
599
+ }
600
+
601
+ private redirect(node: SyntaxNode, segment: Segment, ctx: Context): void {
602
+ if (node.type === "heredoc_redirect") {
603
+ this.heredoc(node, segment, ctx);
604
+ return;
605
+ }
606
+ if (node.type === "herestring_redirect") {
607
+ for (const part of node.namedChildren) {
608
+ const word = this.word(part, ctx);
609
+ if (word.dynamic === "variable") segment.expandsIntoInput = true;
610
+ (segment.stdin ??= []).push(word.value);
611
+ }
612
+ return;
613
+ }
614
+ if (node.type !== "file_redirect") {
615
+ this.statement(node, ctx);
616
+ return;
617
+ }
618
+ const operator = node.children.find((child) => !child.isNamed)?.type ?? "";
619
+ const destinations = node.childrenForFieldName("destination");
620
+ const [target, ...rest] = destinations;
621
+ // The grammar hangs every word after the target on the redirect; in bash
622
+ // they are the command's own arguments (`cmd 2>/dev/null arg`).
623
+ for (const word of rest) this.addWord(segment, word, this.word(word, ctx));
624
+ if (!target) return; // `>&-`, `<&-`: close a descriptor.
625
+ const word = this.word(target, ctx);
626
+ if (word.dynamic) segment.unknownTarget = true;
627
+ // `>&2`, `<&3`, `>&-`: descriptor duplication, no file involved.
628
+ const duplication = (operator === ">&" || operator === "<&") && /^[0-9]*-?$/.test(word.value) && word.value !== "";
629
+ if (duplication) return;
630
+ if (operator === "<" || operator === "<&") segment.inputs.push(word);
631
+ else segment.redirects.push(word.value);
632
+ }
633
+
634
+ private heredoc(node: SyntaxNode, segment: Segment, ctx: Context): void {
635
+ // A quoted delimiter (`<<'EOF'`, `<<"EOF"`, `<<E\OF`) makes the body literal.
636
+ const start = node.namedChildren.find((child) => child.type === "heredoc_start");
637
+ const literal = !start || /['"\\]/.test(start.text);
638
+ for (const child of node.namedChildren) {
639
+ if (child.type === "heredoc_start" || child.type === "heredoc_end") continue;
640
+ if (child.type === "heredoc_body") {
641
+ (segment.stdin ??= []).push(child.text);
642
+ if (literal) continue;
643
+ for (const part of child.namedChildren) {
644
+ if (part.type !== "heredoc_content" && this.word(part, ctx).dynamic === "variable") segment.expandsIntoInput = true;
645
+ }
646
+ this.heredocBackticks(child, ctx);
647
+ continue;
648
+ }
649
+ // `cat <<EOF > out`, `cat <<EOF | sh`: the grammar nests the rest of the
650
+ // line inside the heredoc redirect.
651
+ if (child.type === "file_redirect" || child.type === "herestring_redirect") this.redirect(child, segment, ctx);
652
+ else this.statement(child, ctx);
653
+ }
654
+ }
655
+
656
+ /**
657
+ * The grammar parses `$(…)` in an unquoted heredoc body but leaves
658
+ * backticks as plain text (findings §30), though bash runs them. The body's
659
+ * text outside its parsed expansions is scanned, each backtick body parsed
660
+ * on its own, and its commands join the segments.
661
+ */
662
+ private heredocBackticks(body: SyntaxNode, ctx: Context): void {
663
+ let text = "";
664
+ let at = body.startIndex;
665
+ for (const part of body.namedChildren) {
666
+ if (part.type === "heredoc_content") continue;
667
+ // A parsed expansion is blanked out, keeping every other offset.
668
+ text += this.source.slice(at, part.startIndex) + " ".repeat(part.endIndex - part.startIndex);
669
+ at = part.endIndex;
670
+ }
671
+ text += this.source.slice(at, body.endIndex);
672
+ for (let i = 0; i < text.length; i++) {
673
+ if (text[i] === "\\") {
674
+ i++;
675
+ continue;
676
+ }
677
+ if (text[i] !== "`") continue;
678
+ let end = i + 1;
679
+ while (end < text.length && text[end] !== "`") end += text[end] === "\\" ? 2 : 1;
680
+ if (end >= text.length) {
681
+ // An unterminated backtick: bash reports a syntax error.
682
+ this.failed = true;
683
+ return;
684
+ }
685
+ const inner = parseCommand(text.slice(i + 1, end));
686
+ const scope = this.enterSubstitution(ctx, "command_substitution", text.slice(i + 1, end));
687
+ // The body's `&&` lists are numbered by its own parse, not this one: dropped.
688
+ for (const { andList: _andList, ...segment } of inner.segments) {
689
+ this.segments.push({
690
+ ...segment,
691
+ // These words were parsed from a backtick body, not from this line.
692
+ wordRanges: undefined,
693
+ enclosing: [...scope.enclosing, ...segment.enclosing],
694
+ scopes: [...scope.scopes, ...segment.scopes],
695
+ substitution: scope.substitution,
696
+ ...(segment.pipelineShell ? { pipelineShell: [...scope.scopes, ...segment.pipelineShell] } : {}),
697
+ ...(scope.conditional || segment.conditional ? { conditional: true } : {}),
698
+ start: body.startIndex + i,
699
+ });
700
+ }
701
+ this.unknownQuoting ??= inner.unknownQuoting;
702
+ this.complex ??= inner.complex;
703
+ this.unattributedExpansion ||= !!inner.unattributedExpansion;
704
+ this.definesFunction ||= !!inner.definesFunction;
705
+ this.background ||= inner.background;
706
+ // `echo hi <> f` in the backticks is as unparseable as it is on its own line.
707
+ this.failed ||= inner.parseFailed;
708
+ i = end;
709
+ }
710
+ }
711
+
712
+ /** Read a word node: its value, and the commands it substitutes. */
713
+ word(node: SyntaxNode, ctx: Context): Token {
714
+ // Punctuation the grammar keeps as its own node (a `$` before a closing
715
+ // quote) is literal text.
716
+ if (!node.isNamed) return { value: node.text };
717
+ switch (node.type) {
718
+ case "word":
719
+ return unquotedWord(node.text);
720
+ case "number":
721
+ return { value: node.text };
722
+ case "raw_string":
723
+ return { value: node.text.slice(1, -1) };
724
+ case "ansi_c_string": {
725
+ const decoded = decodeAnsiC(node.text, 2);
726
+ if (decoded.versionDependent) this.unknownQuoting ??= "uses a $'…' escape (\\u, \\U or \\c?) whose meaning depends on the bash version";
727
+ return { value: decoded.text };
728
+ }
729
+ case "string": {
730
+ let value = "";
731
+ let dynamic: Dynamic | undefined;
732
+ for (const child of node.children) {
733
+ if (child.type === '"') continue;
734
+ if (child.type === "string_content") {
735
+ value += doubleQuoted(child.text);
736
+ continue;
737
+ }
738
+ const piece = this.word(child, ctx);
739
+ value += piece.value;
740
+ // Quoted, `"<(x)"` is the literal text, never a pipe path.
741
+ dynamic = mergeDynamic(dynamic, piece.dynamic === "process-input" ? undefined : piece.dynamic);
742
+ }
743
+ return { value, dynamic };
744
+ }
745
+ case "concatenation": {
746
+ let value = "";
747
+ let glob = false;
748
+ let dynamic: Dynamic | undefined;
749
+ let locale = false;
750
+ const pieces = node.children;
751
+ for (const child of pieces) {
752
+ if (child.type === "$") {
753
+ locale = true;
754
+ continue;
755
+ }
756
+ const piece = this.word(child, ctx);
757
+ if (locale) {
758
+ // `x$"y"` is locale quoting; any other `$` is literal (`x$`).
759
+ if (child.type === "string") {
760
+ this.unknownQuoting ??= LOCALE_QUOTING;
761
+ } else value += "$";
762
+ locale = false;
763
+ }
764
+ value += piece.value;
765
+ glob ||= !!piece.glob;
766
+ // Joined to other text, a process substitution's pipe path is part of an unknown word.
767
+ dynamic = mergeDynamic(dynamic, piece.dynamic === "process-input" ? "substitution" : piece.dynamic);
768
+ }
769
+ if (locale) value += "$";
770
+ // `{a,b}` is three plain words to the grammar; bash expands it.
771
+ if (pieces.some((child) => child.type === "word" && child.text === "{") && pieces.some((child) => child.type === "word" && child.text.includes(","))) {
772
+ this.markComplex("brace_expression");
773
+ }
774
+ return { value, glob: glob || undefined, dynamic };
775
+ }
776
+ case "command_substitution":
777
+ case "process_substitution": {
778
+ // The command text between `$(`/`<(`/`>(` or a backtick and the close.
779
+ const opener = node.text.startsWith("`") ? 1 : 2;
780
+ this.container(node, this.enterSubstitution(ctx, node.type, node.text.slice(opener, -1)));
781
+ const dynamic: Dynamic = node.type === "process_substitution" && node.text.startsWith("<(") ? "process-input" : "substitution";
782
+ return { value: node.text, dynamic };
783
+ }
784
+ case "simple_expansion":
785
+ case "expansion":
786
+ case "variable_name":
787
+ case "special_variable_name":
788
+ // `${x:-$(cmd)}`: the default's command still runs.
789
+ for (const child of node.namedChildren) if (child.type !== "variable_name" && child.type !== "special_variable_name") this.word(child, ctx);
790
+ return { value: node.text, dynamic: "variable" };
791
+ default:
792
+ // Arithmetic, brace expansion and anything unrecognised: unknown value.
793
+ this.markComplex(node.type);
794
+ for (const child of node.namedChildren) {
795
+ if (STATEMENTS.has(child.type)) this.statement(child, ctx);
796
+ else this.word(child, ctx);
797
+ }
798
+ return { value: node.text, dynamic: "variable" };
799
+ }
800
+ }
801
+ }
802
+
803
+ /** The named leaves of a test expression, in order. */
804
+ function leaves(node: SyntaxNode): SyntaxNode[] {
805
+ if (node.namedChildCount === 0 || !node.type.endsWith("_expression")) return [node];
806
+ return node.namedChildren.flatMap(leaves);
807
+ }
808
+
809
+ /** An unquoted word: backslash escapes applied, a line continuation removed, globs noticed. */
810
+ function unquotedWord(text: string): Token {
811
+ let value = "";
812
+ let glob = false;
813
+ for (let i = 0; i < text.length; i++) {
814
+ const ch = text[i];
815
+ if (ch === "\\" && i + 1 < text.length) {
816
+ if (text[i + 1] !== "\n") value += text[i + 1];
817
+ i++;
818
+ continue;
819
+ }
820
+ if (ch === "*" || ch === "?" || ch === "[") glob = true;
821
+ value += ch;
822
+ }
823
+ return { value, glob: glob || undefined };
824
+ }
825
+
826
+ /** Double-quoted text: a backslash escapes only `$`, `` ` ``, `"`, `\` and a newline. */
827
+ function doubleQuoted(text: string): string {
828
+ return text.replace(/\\([$`"\\\n])/g, (_, ch: string) => (ch === "\n" ? "" : ch));
829
+ }
830
+
831
+ /**
832
+ * A value tracked per subshell scope (`Segment.scopes`), such as the
833
+ * directory a `cd` sets. A scope starts with its parent's current value, and
834
+ * setting it never reaches the parent or a sibling: in `echo "$(cd /tmp)";
835
+ * cat x`, `x` is still read from the starting directory. Segments must be
836
+ * visited in source order.
837
+ */
838
+ export function scopedTracker<T>(initial: T): { get(segment: Pick<Segment, "scopes">): T; set(segment: Pick<Segment, "scopes">, value: T): void } {
839
+ const values = new Map<string, T>([["", initial]]);
840
+ return {
841
+ get(segment) {
842
+ for (let n = segment.scopes.length; n >= 0; n--) {
843
+ const key = segment.scopes.slice(0, n).join(".");
844
+ if (values.has(key)) return values.get(key) as T;
845
+ }
846
+ return initial;
847
+ },
848
+ set(segment, value) {
849
+ values.set(segment.scopes.join("."), value);
850
+ },
851
+ };
852
+ }
853
+
854
+ /** Loop constructs, as `Segment.enclosing` names them: their bodies run more than once. */
855
+ export const LOOPS = new Set(["for_statement", "c_style_for_statement", "while_statement"]);
856
+
857
+ /**
858
+ * Split a command into its simple commands (see the module header). Returns
859
+ * `parseFailed` with no segments while the grammar is not loaded.
860
+ */
861
+ export function parseCommand(command: string): ParseResult {
862
+ const tree = parseBash(command);
863
+ if (!tree) return { segments: [], parseFailed: true, unavailable: bashParserUnavailable(), background: false, substitutions: [], pipelines: [] };
864
+ try {
865
+ const walker = new Walker(command);
866
+ walker.statement(tree.rootNode, { enclosing: [], scopes: [] });
867
+ const segments = walker.segments
868
+ .filter((segment) => segment.tokens.length > 0 || segment.redirects.length > 0 || segment.inputs.length > 0)
869
+ .sort((a, b) => a.start - b.start)
870
+ .map(({ start: _start, ...segment }) => segment);
871
+ return {
872
+ segments,
873
+ parseFailed: tree.rootNode.hasError || walker.failed,
874
+ unknownQuoting: walker.unknownQuoting,
875
+ complex: walker.complex,
876
+ unattributedExpansion: walker.unattributedExpansion,
877
+ definesFunction: walker.definesFunction,
878
+ background: walker.background,
879
+ substitutions: walker.substitutions,
880
+ pipelines: walker.pipelines,
881
+ };
882
+ } finally {
883
+ tree.delete();
884
+ }
885
+ }