@namzu/sdk 51.0.0 → 52.0.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 (352) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts +1 -1
  6. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/examples/SupervisorAgent.d.ts +42 -0
  9. package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
  10. package/dist/agents/examples/SupervisorAgent.js +80 -4
  11. package/dist/agents/examples/SupervisorAgent.js.map +1 -1
  12. package/dist/agents/index.d.ts +1 -1
  13. package/dist/agents/index.d.ts.map +1 -1
  14. package/dist/agents/index.js.map +1 -1
  15. package/dist/authorization/protected-paths.d.ts +36 -0
  16. package/dist/authorization/protected-paths.d.ts.map +1 -0
  17. package/dist/authorization/protected-paths.js +107 -0
  18. package/dist/authorization/protected-paths.js.map +1 -0
  19. package/dist/authorization/readonly-commands.d.ts +39 -0
  20. package/dist/authorization/readonly-commands.d.ts.map +1 -0
  21. package/dist/authorization/readonly-commands.js +797 -0
  22. package/dist/authorization/readonly-commands.js.map +1 -0
  23. package/dist/authorization/readonly-flags.d.ts +19 -0
  24. package/dist/authorization/readonly-flags.d.ts.map +1 -0
  25. package/dist/authorization/readonly-flags.js +178 -0
  26. package/dist/authorization/readonly-flags.js.map +1 -0
  27. package/dist/authorization/removal.d.ts +52 -0
  28. package/dist/authorization/removal.d.ts.map +1 -0
  29. package/dist/authorization/removal.js +283 -0
  30. package/dist/authorization/removal.js.map +1 -0
  31. package/dist/authorization/shell-paths.d.ts +66 -0
  32. package/dist/authorization/shell-paths.d.ts.map +1 -0
  33. package/dist/authorization/shell-paths.js +697 -0
  34. package/dist/authorization/shell-paths.js.map +1 -0
  35. package/dist/bridge/acp/update.d.ts.map +1 -1
  36. package/dist/bridge/acp/update.js +1 -0
  37. package/dist/bridge/acp/update.js.map +1 -1
  38. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  39. package/dist/bridge/sse/mapper.js +1 -0
  40. package/dist/bridge/sse/mapper.js.map +1 -1
  41. package/dist/compaction/context-window.d.ts +24 -0
  42. package/dist/compaction/context-window.d.ts.map +1 -1
  43. package/dist/compaction/context-window.js +34 -0
  44. package/dist/compaction/context-window.js.map +1 -1
  45. package/dist/compaction/manual.d.ts.map +1 -1
  46. package/dist/compaction/manual.js +10 -2
  47. package/dist/compaction/manual.js.map +1 -1
  48. package/dist/compaction/plan.d.ts +4 -0
  49. package/dist/compaction/plan.d.ts.map +1 -1
  50. package/dist/compaction/plan.js +6 -2
  51. package/dist/compaction/plan.js.map +1 -1
  52. package/dist/compaction/verifier.d.ts +11 -1
  53. package/dist/compaction/verifier.d.ts.map +1 -1
  54. package/dist/compaction/verifier.js +19 -7
  55. package/dist/compaction/verifier.js.map +1 -1
  56. package/dist/config/runtime.d.ts +55 -7
  57. package/dist/config/runtime.d.ts.map +1 -1
  58. package/dist/config/runtime.js +23 -2
  59. package/dist/config/runtime.js.map +1 -1
  60. package/dist/connector/web/guarded-fetch.d.ts.map +1 -1
  61. package/dist/connector/web/guarded-fetch.js +24 -0
  62. package/dist/connector/web/guarded-fetch.js.map +1 -1
  63. package/dist/connector/web/index.d.ts +1 -1
  64. package/dist/connector/web/index.d.ts.map +1 -1
  65. package/dist/connector/web/page.d.ts +59 -0
  66. package/dist/connector/web/page.d.ts.map +1 -0
  67. package/dist/connector/web/page.js +112 -0
  68. package/dist/connector/web/page.js.map +1 -0
  69. package/dist/connector/web/types.d.ts +47 -0
  70. package/dist/connector/web/types.d.ts.map +1 -1
  71. package/dist/connector/web/types.js.map +1 -1
  72. package/dist/constants/continuation.d.ts +8 -0
  73. package/dist/constants/continuation.d.ts.map +1 -1
  74. package/dist/constants/continuation.js +8 -0
  75. package/dist/constants/continuation.js.map +1 -1
  76. package/dist/constants/plugin/index.d.ts +1 -1
  77. package/dist/constants/plugin/index.d.ts.map +1 -1
  78. package/dist/constants/plugin/index.js +1 -1
  79. package/dist/constants/plugin/index.js.map +1 -1
  80. package/dist/manager/agent/lifecycle.d.ts +7 -0
  81. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  82. package/dist/manager/agent/lifecycle.js +11 -2
  83. package/dist/manager/agent/lifecycle.js.map +1 -1
  84. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  85. package/dist/manager/session/turn-recorder.js +4 -2
  86. package/dist/manager/session/turn-recorder.js.map +1 -1
  87. package/dist/plugin/index.d.ts +1 -1
  88. package/dist/plugin/index.d.ts.map +1 -1
  89. package/dist/plugin/index.js +1 -1
  90. package/dist/plugin/index.js.map +1 -1
  91. package/dist/plugin/lifecycle.d.ts +6 -0
  92. package/dist/plugin/lifecycle.d.ts.map +1 -1
  93. package/dist/plugin/lifecycle.js +98 -114
  94. package/dist/plugin/lifecycle.js.map +1 -1
  95. package/dist/plugin/shell-hook.d.ts +90 -22
  96. package/dist/plugin/shell-hook.d.ts.map +1 -1
  97. package/dist/plugin/shell-hook.js +434 -41
  98. package/dist/plugin/shell-hook.js.map +1 -1
  99. package/dist/provider/errors.d.ts +10 -0
  100. package/dist/provider/errors.d.ts.map +1 -1
  101. package/dist/provider/errors.js +16 -0
  102. package/dist/provider/errors.js.map +1 -1
  103. package/dist/provider/retry.d.ts +27 -0
  104. package/dist/provider/retry.d.ts.map +1 -1
  105. package/dist/provider/retry.js +19 -3
  106. package/dist/provider/retry.js.map +1 -1
  107. package/dist/public-runtime.d.ts +4 -1
  108. package/dist/public-runtime.d.ts.map +1 -1
  109. package/dist/public-runtime.js +4 -1
  110. package/dist/public-runtime.js.map +1 -1
  111. package/dist/public-types.d.ts +2 -2
  112. package/dist/public-types.d.ts.map +1 -1
  113. package/dist/runtime/jobs/registry.d.ts +21 -0
  114. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  115. package/dist/runtime/jobs/registry.js +15 -2
  116. package/dist/runtime/jobs/registry.js.map +1 -1
  117. package/dist/runtime/query/executor/tool-call-admission.d.ts +10 -0
  118. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  119. package/dist/runtime/query/executor/tool-call-admission.js +74 -13
  120. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  121. package/dist/runtime/query/executor.d.ts +41 -2
  122. package/dist/runtime/query/executor.d.ts.map +1 -1
  123. package/dist/runtime/query/executor.js +190 -6
  124. package/dist/runtime/query/executor.js.map +1 -1
  125. package/dist/runtime/query/finalize-turn.d.ts +6 -0
  126. package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
  127. package/dist/runtime/query/finalize-turn.js +1 -1
  128. package/dist/runtime/query/finalize-turn.js.map +1 -1
  129. package/dist/runtime/query/index.d.ts +29 -0
  130. package/dist/runtime/query/index.d.ts.map +1 -1
  131. package/dist/runtime/query/index.js +15 -3
  132. package/dist/runtime/query/index.js.map +1 -1
  133. package/dist/runtime/query/iteration/index.d.ts +31 -0
  134. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  135. package/dist/runtime/query/iteration/index.js +216 -7
  136. package/dist/runtime/query/iteration/index.js.map +1 -1
  137. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +58 -0
  138. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -0
  139. package/dist/runtime/query/iteration/phases/compaction-breaker.js +91 -0
  140. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -0
  141. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  142. package/dist/runtime/query/iteration/phases/compaction.js +89 -7
  143. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  144. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  145. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  146. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  147. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  148. package/dist/runtime/query/iteration/phases/tool-review.js +165 -2
  149. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  150. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  151. package/dist/runtime/query/iteration/stream-turn.js +24 -1
  152. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  153. package/dist/runtime/query/path-escalation.d.ts +36 -0
  154. package/dist/runtime/query/path-escalation.d.ts.map +1 -0
  155. package/dist/runtime/query/path-escalation.js +184 -0
  156. package/dist/runtime/query/path-escalation.js.map +1 -0
  157. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
  158. package/dist/runtime/query/plugin-hooks.js +8 -0
  159. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  160. package/dist/runtime/query/reminders.d.ts +35 -0
  161. package/dist/runtime/query/reminders.d.ts.map +1 -0
  162. package/dist/runtime/query/reminders.js +7 -0
  163. package/dist/runtime/query/reminders.js.map +1 -0
  164. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  165. package/dist/runtime/query/resume-pending.js +26 -0
  166. package/dist/runtime/query/resume-pending.js.map +1 -1
  167. package/dist/runtime/query/review-policy.d.ts +26 -0
  168. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  169. package/dist/runtime/query/review-policy.js +79 -5
  170. package/dist/runtime/query/review-policy.js.map +1 -1
  171. package/dist/runtime/query/tool-output-budget.d.ts +46 -0
  172. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  173. package/dist/runtime/query/tool-output-budget.js +85 -1
  174. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  175. package/dist/runtime/query/tooling.d.ts +1 -0
  176. package/dist/runtime/query/tooling.d.ts.map +1 -1
  177. package/dist/runtime/query/tooling.js +5 -0
  178. package/dist/runtime/query/tooling.js.map +1 -1
  179. package/dist/session/paths.d.ts +5 -1
  180. package/dist/session/paths.d.ts.map +1 -1
  181. package/dist/session/paths.js +8 -0
  182. package/dist/session/paths.js.map +1 -1
  183. package/dist/store/memory/disk.d.ts.map +1 -1
  184. package/dist/store/memory/disk.js +2 -2
  185. package/dist/store/memory/disk.js.map +1 -1
  186. package/dist/store/memory/markdown.d.ts.map +1 -1
  187. package/dist/store/memory/markdown.js +2 -2
  188. package/dist/store/memory/markdown.js.map +1 -1
  189. package/dist/store/memory/naming.d.ts +9 -0
  190. package/dist/store/memory/naming.d.ts.map +1 -1
  191. package/dist/store/memory/naming.js +12 -0
  192. package/dist/store/memory/naming.js.map +1 -1
  193. package/dist/tools/builtins/bash.d.ts +30 -1
  194. package/dist/tools/builtins/bash.d.ts.map +1 -1
  195. package/dist/tools/builtins/bash.js +170 -26
  196. package/dist/tools/builtins/bash.js.map +1 -1
  197. package/dist/tools/builtins/content-fingerprint.d.ts +2 -0
  198. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
  199. package/dist/tools/builtins/content-fingerprint.js +4 -0
  200. package/dist/tools/builtins/content-fingerprint.js.map +1 -1
  201. package/dist/tools/builtins/edit.d.ts.map +1 -1
  202. package/dist/tools/builtins/edit.js +60 -18
  203. package/dist/tools/builtins/edit.js.map +1 -1
  204. package/dist/tools/builtins/grep.d.ts +8 -1
  205. package/dist/tools/builtins/grep.d.ts.map +1 -1
  206. package/dist/tools/builtins/grep.js +152 -33
  207. package/dist/tools/builtins/grep.js.map +1 -1
  208. package/dist/tools/builtins/read-file.d.ts +1 -0
  209. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  210. package/dist/tools/builtins/read-file.js +44 -5
  211. package/dist/tools/builtins/read-file.js.map +1 -1
  212. package/dist/tools/builtins/read-render.d.ts +52 -0
  213. package/dist/tools/builtins/read-render.d.ts.map +1 -1
  214. package/dist/tools/builtins/read-render.js +143 -0
  215. package/dist/tools/builtins/read-render.js.map +1 -1
  216. package/dist/tools/builtins/web.d.ts +13 -2
  217. package/dist/tools/builtins/web.d.ts.map +1 -1
  218. package/dist/tools/builtins/web.js +225 -32
  219. package/dist/tools/builtins/web.js.map +1 -1
  220. package/dist/tools/coordinator/ask-user-question.d.ts +2 -0
  221. package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
  222. package/dist/tools/coordinator/ask-user-question.js +256 -92
  223. package/dist/tools/coordinator/ask-user-question.js.map +1 -1
  224. package/dist/tools/coordinator/question-options.d.ts +1 -0
  225. package/dist/tools/coordinator/question-options.d.ts.map +1 -1
  226. package/dist/tools/coordinator/question-options.js +6 -1
  227. package/dist/tools/coordinator/question-options.js.map +1 -1
  228. package/dist/tools/defineTool.d.ts +4 -1
  229. package/dist/tools/defineTool.d.ts.map +1 -1
  230. package/dist/tools/defineTool.js +6 -1
  231. package/dist/tools/defineTool.js.map +1 -1
  232. package/dist/tools/paths.d.ts +6 -0
  233. package/dist/tools/paths.d.ts.map +1 -1
  234. package/dist/tools/paths.js +1 -1
  235. package/dist/tools/paths.js.map +1 -1
  236. package/dist/types/agent/query.d.ts +3 -0
  237. package/dist/types/agent/query.d.ts.map +1 -1
  238. package/dist/types/errors/catalog.js +1 -1
  239. package/dist/types/errors/catalog.js.map +1 -1
  240. package/dist/types/hitl/index.d.ts +104 -0
  241. package/dist/types/hitl/index.d.ts.map +1 -1
  242. package/dist/types/hitl/index.js.map +1 -1
  243. package/dist/types/message/index.d.ts +1 -1
  244. package/dist/types/message/index.d.ts.map +1 -1
  245. package/dist/types/message/index.js +2 -0
  246. package/dist/types/message/index.js.map +1 -1
  247. package/dist/types/plugin/index.d.ts +93 -19
  248. package/dist/types/plugin/index.d.ts.map +1 -1
  249. package/dist/types/plugin/index.js +3 -0
  250. package/dist/types/plugin/index.js.map +1 -1
  251. package/dist/types/provider/error.d.ts +6 -0
  252. package/dist/types/provider/error.d.ts.map +1 -1
  253. package/dist/types/provider/errors.d.ts +19 -0
  254. package/dist/types/provider/errors.d.ts.map +1 -1
  255. package/dist/types/provider/errors.js +39 -1
  256. package/dist/types/provider/errors.js.map +1 -1
  257. package/dist/types/provider/stream.d.ts +6 -0
  258. package/dist/types/provider/stream.d.ts.map +1 -1
  259. package/dist/types/session/events.d.ts +15 -2
  260. package/dist/types/session/events.d.ts.map +1 -1
  261. package/dist/types/session/events.js.map +1 -1
  262. package/dist/types/session/records.d.ts +9 -9
  263. package/dist/types/session/records.d.ts.map +1 -1
  264. package/dist/types/session/records.js +1 -0
  265. package/dist/types/session/records.js.map +1 -1
  266. package/dist/types/session/stop-reason.d.ts +5 -0
  267. package/dist/types/session/stop-reason.d.ts.map +1 -1
  268. package/dist/types/tool/index.d.ts +52 -0
  269. package/dist/types/tool/index.d.ts.map +1 -1
  270. package/dist/types/tool/index.js.map +1 -1
  271. package/dist/utils/backoff.d.ts +12 -0
  272. package/dist/utils/backoff.d.ts.map +1 -1
  273. package/dist/utils/backoff.js +15 -0
  274. package/dist/utils/backoff.js.map +1 -1
  275. package/package.json +4 -2
  276. package/src/agents/QueryAgent.ts +5 -1
  277. package/src/agents/SupervisorAgent.ts +1 -1
  278. package/src/agents/examples/SupervisorAgent.ts +217 -106
  279. package/src/agents/index.ts +1 -1
  280. package/src/authorization/protected-paths.ts +110 -0
  281. package/src/authorization/readonly-commands.ts +771 -0
  282. package/src/authorization/readonly-flags.ts +196 -0
  283. package/src/authorization/removal.ts +326 -0
  284. package/src/authorization/shell-paths.ts +927 -0
  285. package/src/bridge/acp/update.ts +1 -0
  286. package/src/bridge/sse/mapper.ts +1 -0
  287. package/src/compaction/context-window.ts +41 -0
  288. package/src/compaction/manual.ts +10 -2
  289. package/src/compaction/plan.ts +11 -2
  290. package/src/compaction/verifier.ts +28 -3
  291. package/src/config/runtime.ts +23 -2
  292. package/src/connector/web/guarded-fetch.ts +26 -0
  293. package/src/connector/web/index.ts +3 -0
  294. package/src/connector/web/page.ts +130 -0
  295. package/src/connector/web/types.ts +47 -0
  296. package/src/constants/continuation.ts +10 -0
  297. package/src/constants/plugin/index.ts +1 -1
  298. package/src/manager/agent/lifecycle.ts +12 -4
  299. package/src/manager/session/turn-recorder.ts +10 -2
  300. package/src/plugin/index.ts +1 -0
  301. package/src/plugin/lifecycle.ts +107 -119
  302. package/src/plugin/shell-hook.ts +477 -43
  303. package/src/provider/errors.ts +18 -0
  304. package/src/provider/retry.ts +53 -3
  305. package/src/public-runtime.ts +8 -0
  306. package/src/public-types.ts +4 -0
  307. package/src/runtime/jobs/registry.ts +39 -2
  308. package/src/runtime/query/executor/tool-call-admission.ts +79 -13
  309. package/src/runtime/query/executor.ts +242 -8
  310. package/src/runtime/query/finalize-turn.ts +7 -1
  311. package/src/runtime/query/index.ts +45 -3
  312. package/src/runtime/query/iteration/index.ts +251 -12
  313. package/src/runtime/query/iteration/phases/compaction-breaker.ts +106 -0
  314. package/src/runtime/query/iteration/phases/compaction.ts +99 -12
  315. package/src/runtime/query/iteration/phases/context.ts +11 -0
  316. package/src/runtime/query/iteration/phases/tool-review.ts +177 -2
  317. package/src/runtime/query/iteration/stream-turn.ts +24 -1
  318. package/src/runtime/query/path-escalation.ts +204 -0
  319. package/src/runtime/query/plugin-hooks.ts +8 -0
  320. package/src/runtime/query/reminders.ts +45 -0
  321. package/src/runtime/query/resume-pending.ts +27 -0
  322. package/src/runtime/query/review-policy.ts +106 -5
  323. package/src/runtime/query/tool-output-budget.ts +115 -1
  324. package/src/runtime/query/tooling.ts +6 -0
  325. package/src/session/paths.ts +11 -1
  326. package/src/store/memory/disk.ts +2 -1
  327. package/src/store/memory/markdown.ts +2 -1
  328. package/src/store/memory/naming.ts +13 -0
  329. package/src/tools/builtins/bash.ts +203 -34
  330. package/src/tools/builtins/content-fingerprint.ts +5 -0
  331. package/src/tools/builtins/edit.ts +69 -19
  332. package/src/tools/builtins/grep.ts +159 -38
  333. package/src/tools/builtins/read-file.ts +48 -5
  334. package/src/tools/builtins/read-render.ts +193 -0
  335. package/src/tools/builtins/web.ts +258 -33
  336. package/src/tools/coordinator/ask-user-question.ts +309 -103
  337. package/src/tools/coordinator/question-options.ts +8 -1
  338. package/src/tools/defineTool.ts +11 -2
  339. package/src/tools/paths.ts +1 -1
  340. package/src/types/agent/query.ts +3 -0
  341. package/src/types/errors/catalog.ts +1 -1
  342. package/src/types/hitl/index.ts +104 -0
  343. package/src/types/message/index.ts +2 -0
  344. package/src/types/plugin/index.ts +101 -18
  345. package/src/types/provider/error.ts +6 -0
  346. package/src/types/provider/errors.ts +49 -1
  347. package/src/types/provider/stream.ts +6 -0
  348. package/src/types/session/events.ts +15 -2
  349. package/src/types/session/records.ts +1 -0
  350. package/src/types/session/stop-reason.ts +5 -0
  351. package/src/types/tool/index.ts +53 -2
  352. package/src/utils/backoff.ts +21 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Per-flag allowlists for the shell commands that may run without a prompt.
3
+ *
4
+ * Read from the reference terminal (version 2.1.302): for each command, the
5
+ * flags it vets and what each takes, `none` (a switch), `string` or `number`.
6
+ * A flag not listed here is not read-only, which is how `git diff --output=f`
7
+ * (writes a file), `git -c`, `--exec-path` and `--config-env` stay out: they
8
+ * are not in any table. Space-separated lists per kind keep the file readable;
9
+ * `readonly-commands.ts` holds the callbacks that need more than a flag name.
10
+ */
11
+
12
+ export type FlagKind = 'none' | 'string' | 'number'
13
+
14
+ export interface FlagTable {
15
+ readonly flags: Readonly<Record<string, FlagKind>>
16
+ /** False when `--` does not end the options for this command. */
17
+ readonly doubleDash: boolean
18
+ }
19
+
20
+ function table(spec: {
21
+ none?: string
22
+ string?: string
23
+ number?: string
24
+ doubleDash?: false
25
+ }): FlagTable {
26
+ const flags: Record<string, FlagKind> = {}
27
+ for (const kind of ['none', 'string', 'number'] as const) {
28
+ for (const flag of (spec[kind] ?? '').split(' ')) if (flag !== '') flags[flag] = kind
29
+ }
30
+ return { flags, doubleDash: spec.doubleDash !== false }
31
+ }
32
+
33
+ /** Keyed by the command words that select the table (`git diff`, `rg`). */
34
+ export const FLAG_TABLES: Readonly<Record<string, FlagTable>> = {
35
+ 'git diff': table({
36
+ none: '--stat --numstat --shortstat --name-only --name-status --color --no-color --dirstat --summary --patch-with-stat --word-diff --color-words --no-renames --no-ext-diff --check --full-index --binary --break-rewrites --find-renames --find-copies --find-copies-harder --irreversible-delete --histogram --patience --minimal --ignore-space-at-eol --ignore-space-change --ignore-all-space --ignore-blank-lines --function-context --exit-code --quiet --cached --staged --pickaxe-regex --pickaxe-all --no-index -p -u -s -M -C -B -D -l -R',
37
+ string:
38
+ '--word-diff-regex --ws-error-highlight --diff-algorithm --relative --diff-filter -S -G -O',
39
+ number: '--abbrev --inter-hunk-context',
40
+ }),
41
+ 'git log': table({
42
+ none: '--oneline --graph --decorate --no-decorate --relative-date --all --branches --tags --remotes --stat --numstat --shortstat --name-only --name-status --color --no-color --patch -p --no-patch --no-ext-diff -s --abbrev-commit --full-history --dense --sparse --simplify-merges --ancestry-path --source --first-parent --merges --no-merges --reverse --walk-reflogs --no-min-parents --no-max-parents --follow --no-walk --left-right --cherry-mark --cherry-pick --boundary --topo-order --date-order --author-date-order --pickaxe-regex --pickaxe-all',
43
+ string:
44
+ '--date --since --after --until --before --author --committer --grep --pretty --format --diff-filter -S -G',
45
+ number: '--max-count -n --skip --max-age --min-age',
46
+ }),
47
+ 'git show': table({
48
+ none: '--oneline --graph --decorate --no-decorate --relative-date --stat --numstat --shortstat --name-only --name-status --color --no-color --patch -p --no-patch --no-ext-diff -s --abbrev-commit --word-diff --color-words --first-parent --raw -m --quiet',
49
+ string: '--date --word-diff-regex --pretty --format --diff-filter',
50
+ }),
51
+ 'git shortlog': table({
52
+ none: '--all --branches --tags --remotes -s --summary -n --numbered -e --email -c --committer --no-merges',
53
+ string: '--since --after --until --before --group --format --author',
54
+ }),
55
+ 'git reflog': table({
56
+ none: '--oneline --graph --decorate --no-decorate --relative-date --all --branches --tags --remotes',
57
+ string: '--date --since --after --until --before --author --committer --grep',
58
+ number: '--max-count -n',
59
+ }),
60
+ 'git stash list': table({
61
+ none: '--oneline --graph --decorate --no-decorate --relative-date --all --branches --tags --remotes',
62
+ string: '--date',
63
+ number: '--max-count -n',
64
+ doubleDash: false,
65
+ }),
66
+ 'git ls-remote': table({
67
+ none: '--branches -b --tags -t --heads -h --refs --quiet -q --exit-code --get-url --symref',
68
+ string: '--sort',
69
+ }),
70
+ 'git status': table({
71
+ none: '--short -s --branch -b --porcelain --long --verbose -v --ignored --column --no-column --ahead-behind --no-ahead-behind --renames --no-renames',
72
+ string: '--untracked-files -u --ignore-submodules --find-renames -M',
73
+ }),
74
+ 'git blame': table({
75
+ none: '--color --no-color --porcelain -p --line-porcelain --incremental --root --show-stats --show-name --show-number -n --show-email -e -f -w -M -C --score-debug -s -l -t',
76
+ string: '-L --date --ignore-rev --ignore-revs-file',
77
+ number: '--abbrev',
78
+ doubleDash: false,
79
+ }),
80
+ 'git ls-files': table({
81
+ none: '--cached -c --deleted -d --modified -m --others -o --ignored -i --stage -s --killed -k --unmerged -u --directory --no-empty-directory --eol --full-name --debug -z -t -v -f --exclude-standard --error-unmatch --recurse-submodules',
82
+ string: '--exclude -x --exclude-from -X --exclude-per-directory',
83
+ number: '--abbrev',
84
+ }),
85
+ 'git config --get': table({
86
+ none: '--local --global --system --worktree --bool --int --bool-or-int --path --expiry-date -z --null --name-only --show-origin --show-scope',
87
+ string: '--default --type',
88
+ }),
89
+ 'git remote show': table({
90
+ none: '-n',
91
+ }),
92
+ 'git remote': table({
93
+ none: '-v --verbose',
94
+ }),
95
+ 'git merge-base': table({
96
+ none: '--is-ancestor --fork-point --octopus --independent --all',
97
+ }),
98
+ 'git rev-parse': table({
99
+ none: '--verify --abbrev-ref --symbolic --symbolic-full-name --show-toplevel --show-cdup --show-prefix --git-dir --git-common-dir --absolute-git-dir --show-superproject-working-tree --is-inside-work-tree --is-inside-git-dir --is-bare-repository --is-shallow-repository --is-shallow-update --path-prefix',
100
+ string: '--short',
101
+ }),
102
+ 'git rev-list': table({
103
+ none: '--all --branches --tags --remotes --count --reverse --first-parent --ancestry-path --merges --no-merges --no-min-parents --no-max-parents --walk-reflogs --oneline --abbrev-commit --full-history --dense --sparse --source --graph',
104
+ string: '--since --after --until --before --author --committer --grep --pretty --format',
105
+ number: '--max-count -n --min-parents --max-parents --skip --max-age --min-age --abbrev',
106
+ }),
107
+ 'git describe': table({
108
+ none: '--tags --long --always --contains --first-match --exact-match --dirty --broken',
109
+ string: '--match --exclude',
110
+ number: '--abbrev --candidates',
111
+ }),
112
+ 'git cat-file': table({
113
+ none: '-t -s -p -e --batch-check --allow-undetermined-type',
114
+ }),
115
+ 'git for-each-ref': table({
116
+ string: '--format --sort --contains --no-contains --merged --no-merged --points-at',
117
+ number: '--count',
118
+ }),
119
+ 'git grep': table({
120
+ none: '-E --extended-regexp -G --basic-regexp -F --fixed-strings -P --perl-regexp -i --ignore-case -v --invert-match -w --word-regexp -n --line-number -c --count -l --files-with-matches -L --files-without-match -h -H --heading --break --full-name --color --no-color -o --only-matching --and --or --not --untracked --no-index --recurse-submodules --cached -q --quiet',
121
+ string: '-e',
122
+ number: '-A --after-context -B --before-context -C --context --max-depth --threads',
123
+ }),
124
+ 'git stash show': table({
125
+ none: '--stat --numstat --shortstat --name-only --name-status --color --no-color --patch -p --no-patch --no-ext-diff -s --word-diff',
126
+ string: '--word-diff-regex --diff-filter',
127
+ number: '--abbrev',
128
+ }),
129
+ 'git worktree list': table({
130
+ none: '--porcelain -v --verbose',
131
+ string: '--expire',
132
+ }),
133
+ 'git tag': table({
134
+ none: '-l --list --column --no-column -i --ignore-case',
135
+ string: '--contains --no-contains --merged --no-merged --sort --format --points-at',
136
+ number: '-n',
137
+ }),
138
+ 'git branch': table({
139
+ none: '-l --list -a --all -r --remotes -v -vv --verbose --color --no-color --column --no-column --no-abbrev --merged --no-merged --show-current -i --ignore-case',
140
+ string: '--contains --no-contains --points-at --sort',
141
+ number: '--abbrev',
142
+ }),
143
+ file: table({
144
+ none: '--brief -b --mime -i --mime-type --mime-encoding --apple --check-encoding -c --print0 -0 --help --version -v --no-dereference -h --dereference -L --keep-going -k --list -l --no-buffer -n --preserve-date -p --raw -r -s --special-files',
145
+ string: '--exclude --exclude-quiet -F --separator',
146
+ }),
147
+ sort: table({
148
+ none: '--ignore-leading-blanks -b --dictionary-order -d --ignore-case -f --general-numeric-sort -g --human-numeric-sort -h --ignore-nonprinting -i --month-sort -M --numeric-sort -n --random-sort -R --reverse -r --stable -s --unique -u --version-sort -V --zero-terminated -z --check -c --check-char-order -C --merge -m --help --version',
149
+ string: '--sort --key -k --field-separator -t --buffer-size -S',
150
+ number: '--parallel --batch-size',
151
+ }),
152
+ base64: table({
153
+ none: '-d -D --decode --ignore-garbage -h --help --version',
154
+ string: '-i --input',
155
+ number: '-b --break -w --wrap',
156
+ doubleDash: false,
157
+ }),
158
+ grep: table({
159
+ none: '-F --fixed-strings -G --basic-regexp -E --extended-regexp -P --perl-regexp -i --ignore-case --no-ignore-case -v --invert-match -w --word-regexp -x --line-regexp -c --count -L --files-without-match -l --files-with-matches -o --only-matching -q --quiet --silent -s --no-messages -b --byte-offset -H --with-filename -h --no-filename -n --line-number -T --initial-tab -u --unix-byte-offsets -Z --null -z --null-data --no-group-separator -a --text -I -r --recursive -R --dereference-recursive --line-buffered -U --binary --help -V --version',
160
+ string:
161
+ '-e --regexp -f --file --color --colour --label --group-separator --binary-files -D --devices -d --directories --exclude --exclude-from --exclude-dir --include',
162
+ number: '-m --max-count -A --after-context -B --before-context -C --context',
163
+ }),
164
+ egrep: table({
165
+ none: '-F --fixed-strings -G --basic-regexp -E --extended-regexp -P --perl-regexp -i --ignore-case --no-ignore-case -v --invert-match -w --word-regexp -x --line-regexp -c --count -L --files-without-match -l --files-with-matches -o --only-matching -q --quiet --silent -s --no-messages -b --byte-offset -H --with-filename -h --no-filename -n --line-number -T --initial-tab -u --unix-byte-offsets -Z --null -z --null-data --no-group-separator -a --text -I -r --recursive -R --dereference-recursive --line-buffered -U --binary --help -V --version',
166
+ string:
167
+ '-e --regexp -f --file --color --colour --label --group-separator --binary-files -D --devices -d --directories --exclude --exclude-from --exclude-dir --include',
168
+ number: '-m --max-count -A --after-context -B --before-context -C --context',
169
+ }),
170
+ fgrep: table({
171
+ none: '-F --fixed-strings -G --basic-regexp -E --extended-regexp -P --perl-regexp -i --ignore-case --no-ignore-case -v --invert-match -w --word-regexp -x --line-regexp -c --count -L --files-without-match -l --files-with-matches -o --only-matching -q --quiet --silent -s --no-messages -b --byte-offset -H --with-filename -h --no-filename -n --line-number -T --initial-tab -u --unix-byte-offsets -Z --null -z --null-data --no-group-separator -a --text -I -r --recursive -R --dereference-recursive --line-buffered -U --binary --help -V --version',
172
+ string:
173
+ '-e --regexp -f --file --color --colour --label --group-separator --binary-files -D --devices -d --directories --exclude --exclude-from --exclude-dir --include',
174
+ number: '-m --max-count -A --after-context -B --before-context -C --context',
175
+ }),
176
+ rg: table({
177
+ none: '-i --ignore-case -S --smart-case -F --fixed-strings -w --word-regexp -v --invert-match -c --count -l --files-with-matches --files-without-match -n --line-number -o --only-matching -H -h --heading --no-heading -q --quiet --column --type-list --hidden --no-ignore -u -a --text -L --follow --json --stats --help --version --debug --',
178
+ string: '-e --regexp -f -g --glob -t --type -T --type-not --color',
179
+ number: '-A --after-context -B --before-context -C --context -m --max-count -d --max-depth',
180
+ }),
181
+ sha256sum: table({
182
+ none: '-b --binary -t --text -c --check --ignore-missing --quiet --status --strict -w --warn --tag -z --zero --help --version',
183
+ }),
184
+ sha1sum: table({
185
+ none: '-b --binary -t --text -c --check --ignore-missing --quiet --status --strict -w --warn --tag -z --zero --help --version',
186
+ }),
187
+ md5sum: table({
188
+ none: '-b --binary -t --text -c --check --ignore-missing --quiet --status --strict -w --warn --tag -z --zero --help --version',
189
+ }),
190
+ tree: table({
191
+ none: '-a -d -l -f -x --gitignore --ignore-case --matchdirs --metafirst --prune --info --noreport -q -N -Q -p -u -g -s -h --si --du -D -F --inodes --device -v -t -c -U -r --dirsfirst --filesfirst -i -A -S -n -C -X -J --nolinks --hyperlink --fromfile --fromtabfile --fflinks --help --version',
192
+ string:
193
+ '-P -I --gitfile --infofile --charset --timefmt --sort -H --hintro --houtro -T --scheme --authority',
194
+ number: '-L --filelimit',
195
+ }),
196
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Whether a command line removes something it must not, decided from its
3
+ * targets.
4
+ *
5
+ * The old floor was one regular expression, `rm\s+-rf\s+\/`. It matched a
6
+ * single spelling: `rm -fr /`, `rm -r -f /`, `rm --recursive --force /`,
7
+ * `rm -rf ~`, `rm -rf $HOME`, `rm -rf "$BUILD/"` (empty variable, so `/`) and
8
+ * `rm -rf ..` all went past it. The flags were never the point. A removal is
9
+ * catastrophic because of WHAT it removes, so this reads the targets of `rm`
10
+ * and `rmdir` whatever the flags, as the reference terminal's dangerous-removal
11
+ * check does (version 2.1.302).
12
+ *
13
+ * A removal is flagged when a target is
14
+ *
15
+ * - the file system root, a top-level directory (`/usr`, `/etc`), or the
16
+ * operator's home directory (`~`, `$HOME`, `${HOME}`);
17
+ * - the working directory or any directory above it (`.`, `..`, `../..`), or
18
+ * the same for an added directory when the target is a glob;
19
+ * - a bare glob that crosses directories (`*`, `/*`, `*\/*`);
20
+ * - a variable followed by a slash (`$DIR/`, `"$DIR/"/*`): when the variable
21
+ * is empty the path is `/`; or a variable followed by a top-level directory
22
+ * name (`$PREFIX/usr`) when the variable may be unset;
23
+ * - the output of a command substitution, for a recursive removal;
24
+ * - a path that cannot be settled before the line runs (a relative glob after
25
+ * a `cd`, `a/../*`, `~user`).
26
+ *
27
+ * The answer is a reason, which the kernel turns into an escalation: asked
28
+ * every time, in every mode, and refused when nobody can be asked. It is not a
29
+ * denial: the reference asks, because there are legitimate removals a person
30
+ * can approve.
31
+ */
32
+
33
+ import { existsSync, realpathSync } from 'node:fs'
34
+ import { homedir } from 'node:os'
35
+ import {
36
+ dirname,
37
+ isAbsolute,
38
+ join,
39
+ normalize,
40
+ basename as pathBasename,
41
+ resolve,
42
+ sep,
43
+ } from 'node:path'
44
+
45
+ import type { ShellWord } from './shell-lexer.js'
46
+ import { type ShellInvocation, type ShellReading, expandsAtRuntime } from './shell-paths.js'
47
+
48
+ /** What the check needs to know about the turn. */
49
+ export interface RemovalContext {
50
+ /** The turn's working directory. */
51
+ readonly cwd: string
52
+ /** Directories added to the session. */
53
+ readonly additionalDirectories?: readonly string[]
54
+ /** The operator's home directory; the host's own when omitted. */
55
+ readonly home?: string
56
+ /** Environment to decide whether a variable may be empty; `process.env` when omitted. */
57
+ readonly env?: Readonly<Record<string, string | undefined>>
58
+ }
59
+
60
+ /** Top-level directories a variable-prefixed target may become when the variable is empty. */
61
+ const TOP_LEVEL =
62
+ 'bin|boot|dev|etc|home|lib|lib32|lib64|libx32|media|mnt|opt|proc|root|run|sbin|srv|sys|tmp|usr|var|snap|nix|lost\\+found|private|cores|Applications|Library|System|Users|Volumes|Windows|ProgramData|cygdrive'
63
+
64
+ const VAR = '(?:\\{([A-Za-z_][A-Za-z0-9_]*)(?::?-[^}]*)?\\}|([A-Za-z_][A-Za-z0-9_]*))'
65
+ /** `$VAR/` followed by something that cannot name a child: end, a glob, another expansion, a slash. */
66
+ const EMPTY_VARIABLE_ROOT = new RegExp(`^\\$${VAR}/(?:[*?[{]|\\$|/|$)`)
67
+ const EMPTY_VARIABLE_TOP = new RegExp(`^\\$${VAR}/(${TOP_LEVEL})(?:/+\\*+)*/?$`, 'i')
68
+ const BARE_HOME = /^\$(?:\{HOME\}|HOME)(?:\/+\*?)?$/
69
+
70
+ function fold(name: string): string {
71
+ return name.toLowerCase().replace(/ı/g, 'i').replace(/ſ/g, 's')
72
+ }
73
+
74
+ /** `realpath` of the deepest existing ancestor, with the rest appended. */
75
+ function canonical(path: string): string {
76
+ let existing = path
77
+ let rest = ''
78
+ for (;;) {
79
+ if (existsSync(existing)) {
80
+ try {
81
+ const real = realpathSync(existing)
82
+ return rest === '' ? real : join(real, rest)
83
+ } catch {
84
+ return path
85
+ }
86
+ }
87
+ const parent = dirname(existing)
88
+ if (parent === existing) return path
89
+ rest = rest === '' ? pathBasename(existing) : join(pathBasename(existing), rest)
90
+ existing = parent
91
+ }
92
+ }
93
+
94
+ function slashes(path: string): string {
95
+ return path.replace(/[\\/]+/g, '/')
96
+ }
97
+
98
+ function isWithinOrEqual(parent: string, child: string): boolean {
99
+ const p = fold(slashes(parent)).replace(/\/$/, '')
100
+ const c = fold(slashes(child)).replace(/\/$/, '')
101
+ return c === p || c.startsWith(`${p}/`) || p === ''
102
+ }
103
+
104
+ /** The file system root, a top-level directory, a drive root, a bare glob, or the home directory. */
105
+ function isCritical(path: string, homes: readonly string[]): boolean {
106
+ const normalized = slashes(path)
107
+ if (normalized === '*' || normalized.endsWith('/*')) return true
108
+ const trimmed = normalized === '/' ? '/' : normalized.replace(/\/$/, '')
109
+ if (trimmed === '/') return true
110
+ if (/^[A-Za-z]:\/?$/.test(trimmed)) return true
111
+ if (homes.some((home) => fold(trimmed) === fold(slashes(home).replace(/\/$/, '')))) return true
112
+ if (dirname(trimmed) === '/') return true
113
+ return /^[A-Za-z]:\/[^/]+$/.test(trimmed)
114
+ }
115
+
116
+ function isRecursive(args: readonly ShellWord[]): boolean {
117
+ const flags: string[] = []
118
+ for (const arg of args) {
119
+ if (arg.value === '--') break
120
+ flags.push(arg.value)
121
+ }
122
+ return flags.some((flag) => /^--r/.test(flag) || /^-[a-zA-Z]*[rR]/.test(flag))
123
+ }
124
+
125
+ /** Operands of `rm`/`rmdir`: everything after `--` and everything from the first non-flag on. */
126
+ function targetsOf(args: readonly ShellWord[]): ShellWord[] {
127
+ const out: ShellWord[] = []
128
+ let afterDashes = false
129
+ let started = false
130
+ for (const arg of args) {
131
+ if (afterDashes || started) out.push(arg)
132
+ else if (arg.value === '--') afterDashes = true
133
+ else if (arg.value === '-' || !arg.value.startsWith('-')) {
134
+ out.push(arg)
135
+ started = true
136
+ }
137
+ }
138
+ return out
139
+ }
140
+
141
+ /** `~` and `~/…` against `home`. */
142
+ function expandTilde(value: string, home: string): string {
143
+ if (value === '~') return home
144
+ if (value.startsWith('~/')) return join(home, value.slice(2))
145
+ return value
146
+ }
147
+
148
+ /**
149
+ * Why the line removes something it must not, or `undefined`.
150
+ *
151
+ * `reading` is {@link readShellLine}'s result for the same line;
152
+ * `changesDirectory` says a `cd` runs in it, after which a relative target is
153
+ * not the one the working directory names.
154
+ */
155
+ export function dangerousRemoval(
156
+ reading: ShellReading,
157
+ changesDirectory: boolean,
158
+ context: RemovalContext,
159
+ ): string | undefined {
160
+ const removals = reading.invocations.filter((i) => i.program === 'rm' || i.program === 'rmdir')
161
+ if (removals.length === 0) return undefined
162
+ const home = context.home ?? homedir()
163
+ const env = context.env ?? process.env
164
+ const cwd = resolve(context.cwd)
165
+ const homes = [home, canonical(home)]
166
+ const workdirs = [cwd, canonical(cwd)]
167
+ const everyDirectory = [
168
+ ...new Set([
169
+ ...workdirs,
170
+ ...(context.additionalDirectories ?? []).flatMap((d) => [resolve(d), canonical(resolve(d))]),
171
+ ]),
172
+ ]
173
+ for (const removal of removals) {
174
+ const reason = checkRemoval(removal, {
175
+ cwd,
176
+ home,
177
+ homes,
178
+ workdirs,
179
+ everyDirectory,
180
+ env,
181
+ changesDirectory,
182
+ })
183
+ if (reason !== undefined) return reason
184
+ }
185
+ return undefined
186
+ }
187
+
188
+ interface Scope {
189
+ readonly cwd: string
190
+ readonly home: string
191
+ readonly homes: readonly string[]
192
+ readonly workdirs: readonly string[]
193
+ readonly everyDirectory: readonly string[]
194
+ readonly env: Readonly<Record<string, string | undefined>>
195
+ readonly changesDirectory: boolean
196
+ }
197
+
198
+ function checkRemoval(removal: ShellInvocation, scope: Scope): string | undefined {
199
+ const { program, args } = removal
200
+ const recursive = isRecursive(args)
201
+ for (const target of targetsOf(args)) {
202
+ const spelled = target.value
203
+ if (spelled === '') continue
204
+
205
+ // A backslash alone is the drive root in Git Bash on Windows.
206
+ if (/^\\+$/.test(spelled)) return `${program} ${spelled}: a backslash alone is the drive root`
207
+
208
+ if (BARE_HOME.test(spelled)) return `${program} ${spelled}: that is the home directory`
209
+
210
+ const root = EMPTY_VARIABLE_ROOT.exec(spelled)
211
+ if (root !== null) {
212
+ const name = root[1] ?? root[2]
213
+ return `${program} ${spelled}: when $${name} is empty this removes /`
214
+ }
215
+ const top = EMPTY_VARIABLE_TOP.exec(spelled)
216
+ if (top !== null) {
217
+ const name = top[1] ?? top[2] ?? ''
218
+ const value = scope.env[name]
219
+ if (typeof value !== 'string' || value === '') {
220
+ return `${program} ${spelled}: when $${name} is empty this removes /${top[3]}`
221
+ }
222
+ }
223
+
224
+ if (target.substitutes && recursive && /^(?:\$\(.*\)|`.*`)[/*.]*$/s.test(spelled)) {
225
+ return `${program} ${spelled}: the target is the output of a command substitution, which cannot be checked before it runs`
226
+ }
227
+ // A substitution that comes out empty leaves what is before it.
228
+ const stripped = target.substitutes
229
+ ? spelled.replace(/(.)(?:\$\([^)]*\)|`[^`]*`)(?:(?!\.\.)[/*.])*((?:\/\.\.)*)\/*$/s, '$1$2')
230
+ : spelled
231
+ const reason = checkTarget(program, args, stripped, target, scope)
232
+ if (reason !== undefined) return reason
233
+ }
234
+ return undefined
235
+ }
236
+
237
+ function checkTarget(
238
+ program: string,
239
+ args: readonly ShellWord[],
240
+ spelled: string,
241
+ word: ShellWord,
242
+ scope: Scope,
243
+ ): string | undefined {
244
+ const expanded = expandTilde(spelled, scope.home)
245
+ const relative = !isAbsolute(expanded)
246
+ if (expandsAtRuntime(word) && /[$`]/.test(expanded)) {
247
+ // A variable the check cannot read: the reference treats it as a relative
248
+ // name, which is also what the shell does when the variable is unset and
249
+ // the rest is a plain name. Nothing more to learn from it.
250
+ return undefined
251
+ }
252
+ const absolute = relative ? resolve(scope.cwd, expanded) : resolve(expanded)
253
+ let settled = absolute
254
+ for (let previous = ''; previous !== settled; ) {
255
+ previous = settled
256
+ const stripped = settled.replace(/([\\/]\*+)+[\\/]*$/, '') || sep
257
+ if (stripped !== settled) settled = /[\\/]/.test(stripped) ? normalize(stripped) : stripped
258
+ }
259
+ // A glob suffix was stripped from the target: the removal reaches what is in it.
260
+ const globbed = settled !== absolute
261
+ const original = expanded
262
+
263
+ if (scope.changesDirectory && globbed && relative && /[\\/]\*$/.test(absolute)) {
264
+ return `${program} ${spelled}: a relative glob after a cd cannot be resolved before the line runs`
265
+ }
266
+
267
+ // Where the target sits under the working directory, for the glob depth below.
268
+ let below = settled
269
+ if (relative) {
270
+ const prefix = scope.cwd.endsWith(sep) ? scope.cwd : scope.cwd + sep
271
+ if (settled.startsWith(prefix)) below = settled.slice(prefix.length)
272
+ else if (settled === scope.cwd) below = ''
273
+ }
274
+
275
+ if (
276
+ globbed &&
277
+ (hasParentAfterName(original) ||
278
+ original.startsWith('~') ||
279
+ /^[\\/]{2}/.test(original) ||
280
+ (relative && /(^|[\\/])\.\.([\\/]|$)/.test(original) && /[\\/]\*$/.test(absolute)) ||
281
+ (program === 'rmdir' &&
282
+ /[\\/]\*$/.test(absolute) &&
283
+ args.some((a) => /^--p/.test(a.value) || /^-[a-z]*p/.test(a.value))) ||
284
+ (relative && /\*[\\/]+$/.test(original) && /[\\/]\*$/.test(absolute)))
285
+ ) {
286
+ return `${program} ${spelled}: the target cannot be resolved to a directory before the line runs`
287
+ }
288
+
289
+ if (!globbed || !/[*?[]/.test(below)) {
290
+ const candidates = [settled]
291
+ const real = canonical(settled)
292
+ if (real !== settled) candidates.push(real)
293
+ for (const candidate of candidates) {
294
+ if (isCritical(candidate, scope.homes)) {
295
+ return `${program} ${spelled}: that is the file system root, a top-level directory or the home directory`
296
+ }
297
+ const directories = globbed ? scope.everyDirectory : scope.workdirs
298
+ if (directories.some((directory) => isWithinOrEqual(candidate, directory))) {
299
+ return `${program} ${spelled}: that is the working directory or one of its parents`
300
+ }
301
+ }
302
+ }
303
+
304
+ if (globbed && /[\\/]\*$/.test(absolute)) {
305
+ const segments = (value: string): number =>
306
+ value.split(/[\\/]+/).filter((s) => s !== '' && s !== '.').length
307
+ const crossed = segments(absolute) - segments(settled)
308
+ const inside = below.split(/[\\/]+/).filter((s) => /[*?[]/.test(s)).length
309
+ if (crossed + inside > 1) {
310
+ return `${program} ${spelled}: the glob crosses directories that cannot be listed before the line runs`
311
+ }
312
+ }
313
+ return undefined
314
+ }
315
+
316
+ /** `a/../b`: a parent step after a real name, which moves the target somewhere the spelling hides. */
317
+ function hasParentAfterName(path: string): boolean {
318
+ let named = false
319
+ for (const segment of path.split(/[\\/]+/)) {
320
+ if (segment === '' || segment === '.') continue
321
+ if (segment === '..') {
322
+ if (named) return true
323
+ } else named = true
324
+ }
325
+ return false
326
+ }