@cybedefend/vibedefend 1.5.0 → 1.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 (246) hide show
  1. package/README.md +67 -2
  2. package/dist/api/gateway-request.js +49 -0
  3. package/dist/api/gateway-request.js.map +1 -0
  4. package/dist/api/projects.js +67 -0
  5. package/dist/api/projects.js.map +1 -1
  6. package/dist/api/repository-link.js +157 -0
  7. package/dist/api/repository-link.js.map +1 -0
  8. package/dist/auth/auth-store.js +107 -25
  9. package/dist/auth/auth-store.js.map +1 -1
  10. package/dist/auth/auth.js +21 -0
  11. package/dist/auth/auth.js.map +1 -1
  12. package/dist/clients/cursor.js +5 -1
  13. package/dist/clients/cursor.js.map +1 -1
  14. package/dist/clients/vscode.js +38 -29
  15. package/dist/clients/vscode.js.map +1 -1
  16. package/dist/commands/mcp/allow.js +219 -0
  17. package/dist/commands/mcp/allow.js.map +1 -0
  18. package/dist/commands/mcp/connect.js +337 -0
  19. package/dist/commands/mcp/connect.js.map +1 -0
  20. package/dist/commands/mcp/index.js +170 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/mcp/proxy.js +264 -0
  23. package/dist/commands/mcp/proxy.js.map +1 -0
  24. package/dist/commands/mcp/scan.js +349 -0
  25. package/dist/commands/mcp/scan.js.map +1 -0
  26. package/dist/commands/mcp/sync.js +215 -0
  27. package/dist/commands/mcp/sync.js.map +1 -0
  28. package/dist/config.js +6 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/diagnostics.js +78 -12
  31. package/dist/diagnostics.js.map +1 -1
  32. package/dist/doctor.js +19 -0
  33. package/dist/doctor.js.map +1 -1
  34. package/dist/guards-evaluator/analysis-types.js +27 -0
  35. package/dist/guards-evaluator/analysis-types.js.map +1 -0
  36. package/dist/guards-evaluator/command-analysis.js +572 -0
  37. package/dist/guards-evaluator/command-analysis.js.map +1 -0
  38. package/dist/guards-evaluator/command-view.js +586 -0
  39. package/dist/guards-evaluator/command-view.js.map +1 -0
  40. package/dist/guards-evaluator/env.js +11 -0
  41. package/dist/guards-evaluator/env.js.map +1 -0
  42. package/dist/guards-evaluator/evaluate.js +91 -41
  43. package/dist/guards-evaluator/evaluate.js.map +1 -1
  44. package/dist/guards-evaluator/index.js +2 -1
  45. package/dist/guards-evaluator/index.js.map +1 -1
  46. package/dist/guards-evaluator/inline-code.js +259 -0
  47. package/dist/guards-evaluator/inline-code.js.map +1 -0
  48. package/dist/guards-evaluator/normalize.js +716 -0
  49. package/dist/guards-evaluator/normalize.js.map +1 -0
  50. package/dist/guards-evaluator/preprocess.js +13 -5
  51. package/dist/guards-evaluator/preprocess.js.map +1 -1
  52. package/dist/guards-evaluator/redact.js +19 -0
  53. package/dist/guards-evaluator/redact.js.map +1 -1
  54. package/dist/guards-evaluator/regex.js +25 -3
  55. package/dist/guards-evaluator/regex.js.map +1 -1
  56. package/dist/guards-evaluator/safe-commands.js +155 -0
  57. package/dist/guards-evaluator/safe-commands.js.map +1 -0
  58. package/dist/guards-evaluator/triage.js +315 -0
  59. package/dist/guards-evaluator/triage.js.map +1 -0
  60. package/dist/hook-runner.js +24679 -5079
  61. package/dist/hooks/install.js +38 -0
  62. package/dist/hooks/install.js.map +1 -1
  63. package/dist/hooks/runtime/config.js +38 -0
  64. package/dist/hooks/runtime/config.js.map +1 -1
  65. package/dist/hooks/runtime/emit.js +94 -61
  66. package/dist/hooks/runtime/emit.js.map +1 -1
  67. package/dist/hooks/runtime/fetch-rules.js +22 -7
  68. package/dist/hooks/runtime/fetch-rules.js.map +1 -1
  69. package/dist/hooks/runtime/gateway-http-error.js +34 -0
  70. package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
  71. package/dist/hooks/runtime/guard-rules-cache.js +139 -17
  72. package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
  73. package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
  74. package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
  75. package/dist/hooks/runtime/judge-cache.js +6 -0
  76. package/dist/hooks/runtime/judge-cache.js.map +1 -1
  77. package/dist/hooks/runtime/judge-triage.js +193 -0
  78. package/dist/hooks/runtime/judge-triage.js.map +1 -0
  79. package/dist/hooks/runtime/judge.js +7 -1
  80. package/dist/hooks/runtime/judge.js.map +1 -1
  81. package/dist/hooks/runtime/pre-compact.js +12 -6
  82. package/dist/hooks/runtime/pre-compact.js.map +1 -1
  83. package/dist/hooks/runtime/resolve.js +90 -8
  84. package/dist/hooks/runtime/resolve.js.map +1 -1
  85. package/dist/hooks/runtime/retry-policy.js +57 -0
  86. package/dist/hooks/runtime/retry-policy.js.map +1 -0
  87. package/dist/hooks/runtime/review-sessions.js +74 -0
  88. package/dist/hooks/runtime/review-sessions.js.map +1 -0
  89. package/dist/hooks/runtime/script-resolve.js +695 -0
  90. package/dist/hooks/runtime/script-resolve.js.map +1 -0
  91. package/dist/hooks/runtime/session-activation.js +5 -1
  92. package/dist/hooks/runtime/session-activation.js.map +1 -1
  93. package/dist/hooks/runtime/session-review.js +163 -45
  94. package/dist/hooks/runtime/session-review.js.map +1 -1
  95. package/dist/hooks/runtime/session-start.js +2 -1
  96. package/dist/hooks/runtime/session-start.js.map +1 -1
  97. package/dist/hooks/runtime/session-writes.js +232 -0
  98. package/dist/hooks/runtime/session-writes.js.map +1 -0
  99. package/dist/hooks/runtime/sniff.js +26 -7
  100. package/dist/hooks/runtime/sniff.js.map +1 -1
  101. package/dist/hooks/runtime/target-resolve.js +336 -0
  102. package/dist/hooks/runtime/target-resolve.js.map +1 -0
  103. package/dist/hooks/runtime/unmapped-tools.js +93 -0
  104. package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
  105. package/dist/hooks/runtime/usage-block.js +4 -0
  106. package/dist/hooks/runtime/usage-block.js.map +1 -1
  107. package/dist/hooks/runtime/user-prompt-submit.js +2 -1
  108. package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
  109. package/dist/hooks/runtime/wire-text.js +41 -0
  110. package/dist/hooks/runtime/wire-text.js.map +1 -0
  111. package/dist/hooks/runtime/work-dir.js +84 -0
  112. package/dist/hooks/runtime/work-dir.js.map +1 -0
  113. package/dist/index.js +45 -12
  114. package/dist/index.js.map +1 -1
  115. package/dist/install.js +174 -11
  116. package/dist/install.js.map +1 -1
  117. package/dist/link.js +74 -48
  118. package/dist/link.js.map +1 -1
  119. package/dist/login.js +77 -11
  120. package/dist/login.js.map +1 -1
  121. package/dist/mcp/auth/agent-sign-ins.js +174 -0
  122. package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
  123. package/dist/mcp/auth/callback.js +200 -0
  124. package/dist/mcp/auth/callback.js.map +1 -0
  125. package/dist/mcp/auth/connect.js +392 -0
  126. package/dist/mcp/auth/connect.js.map +1 -0
  127. package/dist/mcp/auth/discovery.js +399 -0
  128. package/dist/mcp/auth/discovery.js.map +1 -0
  129. package/dist/mcp/auth/oauth.js +326 -0
  130. package/dist/mcp/auth/oauth.js.map +1 -0
  131. package/dist/mcp/auth/probe.js +128 -0
  132. package/dist/mcp/auth/probe.js.map +1 -0
  133. package/dist/mcp/auth/secrets.js +446 -0
  134. package/dist/mcp/auth/secrets.js.map +1 -0
  135. package/dist/mcp/auth/store.js +260 -0
  136. package/dist/mcp/auth/store.js.map +1 -0
  137. package/dist/mcp/auth/tokenSource.js +55 -0
  138. package/dist/mcp/auth/tokenSource.js.map +1 -0
  139. package/dist/mcp/config/atomicWrite.js +98 -0
  140. package/dist/mcp/config/atomicWrite.js.map +1 -0
  141. package/dist/mcp/config/clients.js +171 -0
  142. package/dist/mcp/config/clients.js.map +1 -0
  143. package/dist/mcp/config/cybedefend-servers.js +48 -0
  144. package/dist/mcp/config/cybedefend-servers.js.map +1 -0
  145. package/dist/mcp/config/discover.js +114 -0
  146. package/dist/mcp/config/discover.js.map +1 -0
  147. package/dist/mcp/config/hook.js +123 -0
  148. package/dist/mcp/config/hook.js.map +1 -0
  149. package/dist/mcp/config/index.js +11 -0
  150. package/dist/mcp/config/index.js.map +1 -0
  151. package/dist/mcp/config/install.js +132 -0
  152. package/dist/mcp/config/install.js.map +1 -0
  153. package/dist/mcp/config/installed.js +37 -0
  154. package/dist/mcp/config/installed.js.map +1 -0
  155. package/dist/mcp/config/jsonEdit.js +119 -0
  156. package/dist/mcp/config/jsonEdit.js.map +1 -0
  157. package/dist/mcp/config/lock.js +102 -0
  158. package/dist/mcp/config/lock.js.map +1 -0
  159. package/dist/mcp/config/proxyBinary.js +211 -0
  160. package/dist/mcp/config/proxyBinary.js.map +1 -0
  161. package/dist/mcp/config/rewrite.js +388 -0
  162. package/dist/mcp/config/rewrite.js.map +1 -0
  163. package/dist/mcp/config/types.js +34 -0
  164. package/dist/mcp/config/types.js.map +1 -0
  165. package/dist/mcp/proxy/audit.js +83 -0
  166. package/dist/mcp/proxy/audit.js.map +1 -0
  167. package/dist/mcp/proxy/authInterceptor.js +119 -0
  168. package/dist/mcp/proxy/authInterceptor.js.map +1 -0
  169. package/dist/mcp/proxy/expand.js +93 -0
  170. package/dist/mcp/proxy/expand.js.map +1 -0
  171. package/dist/mcp/proxy/inject.js +397 -0
  172. package/dist/mcp/proxy/inject.js.map +1 -0
  173. package/dist/mcp/proxy/intercept.js +37 -0
  174. package/dist/mcp/proxy/intercept.js.map +1 -0
  175. package/dist/mcp/proxy/jsonrpc.js +153 -0
  176. package/dist/mcp/proxy/jsonrpc.js.map +1 -0
  177. package/dist/mcp/proxy/log.js +109 -0
  178. package/dist/mcp/proxy/log.js.map +1 -0
  179. package/dist/mcp/proxy/paths.js +53 -0
  180. package/dist/mcp/proxy/paths.js.map +1 -0
  181. package/dist/mcp/proxy/proxy.js +510 -0
  182. package/dist/mcp/proxy/proxy.js.map +1 -0
  183. package/dist/mcp/proxy/spec.js +203 -0
  184. package/dist/mcp/proxy/spec.js.map +1 -0
  185. package/dist/mcp/proxy/sse.js +77 -0
  186. package/dist/mcp/proxy/sse.js.map +1 -0
  187. package/dist/mcp/proxy/upstream.js +2 -0
  188. package/dist/mcp/proxy/upstream.js.map +1 -0
  189. package/dist/mcp/proxy/upstreamHttp.js +499 -0
  190. package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
  191. package/dist/mcp/proxy/upstreamStdio.js +188 -0
  192. package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
  193. package/dist/mcp/proxy/upstreams.js +143 -0
  194. package/dist/mcp/proxy/upstreams.js.map +1 -0
  195. package/dist/mcp/proxy/verifyInterceptor.js +559 -0
  196. package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
  197. package/dist/mcp/proxy/windowsCommand.js +99 -0
  198. package/dist/mcp/proxy/windowsCommand.js.map +1 -0
  199. package/dist/mcp/scan/args.js +228 -0
  200. package/dist/mcp/scan/args.js.map +1 -0
  201. package/dist/mcp/scan/results.js +253 -0
  202. package/dist/mcp/scan/results.js.map +1 -0
  203. package/dist/mcp/scan/types.js +40 -0
  204. package/dist/mcp/scan/types.js.map +1 -0
  205. package/dist/mcp/verify/api.js +475 -0
  206. package/dist/mcp/verify/api.js.map +1 -0
  207. package/dist/mcp/verify/backend.js +143 -0
  208. package/dist/mcp/verify/backend.js.map +1 -0
  209. package/dist/mcp/verify/cache.js +182 -0
  210. package/dist/mcp/verify/cache.js.map +1 -0
  211. package/dist/mcp/verify/contract-errors.js +198 -0
  212. package/dist/mcp/verify/contract-errors.js.map +1 -0
  213. package/dist/mcp/verify/digest.js +91 -0
  214. package/dist/mcp/verify/digest.js.map +1 -0
  215. package/dist/mcp/verify/factory.js +56 -0
  216. package/dist/mcp/verify/factory.js.map +1 -0
  217. package/dist/mcp/verify/identity.js +173 -0
  218. package/dist/mcp/verify/identity.js.map +1 -0
  219. package/dist/mcp/verify/policy-store.js +63 -0
  220. package/dist/mcp/verify/policy-store.js.map +1 -0
  221. package/dist/mcp/verify/policy.js +158 -0
  222. package/dist/mcp/verify/policy.js.map +1 -0
  223. package/dist/mcp/verify/pool.js +27 -0
  224. package/dist/mcp/verify/pool.js.map +1 -0
  225. package/dist/mcp/verify/token-refresh.js +44 -0
  226. package/dist/mcp/verify/token-refresh.js.map +1 -0
  227. package/dist/mcp/verify/token.js +67 -0
  228. package/dist/mcp/verify/token.js.map +1 -0
  229. package/dist/mcp/verify/verifier.js +1009 -0
  230. package/dist/mcp/verify/verifier.js.map +1 -0
  231. package/dist/org-gate.js +1 -1
  232. package/dist/org-gate.js.map +1 -1
  233. package/dist/org.js +146 -0
  234. package/dist/org.js.map +1 -0
  235. package/dist/project-create.js +129 -0
  236. package/dist/project-create.js.map +1 -0
  237. package/dist/prompts.js +21 -0
  238. package/dist/prompts.js.map +1 -1
  239. package/dist/re2.wasm +0 -0
  240. package/dist/status.js +61 -0
  241. package/dist/status.js.map +1 -1
  242. package/dist/uninstall.js +49 -4
  243. package/dist/uninstall.js.map +1 -1
  244. package/dist/utils.js +62 -6
  245. package/dist/utils.js.map +1 -1
  246. package/package.json +10 -3
@@ -0,0 +1,716 @@
1
+ /**
2
+ * Level 0 core — de-obfuscate + normalize a raw Bash command STRING into the
3
+ * list of EFFECTIVE simple commands it will actually run.
4
+ *
5
+ * The current evaluator matches its rule regexes against the RAW command, so
6
+ * trivial rewrites (`/bin/rm`, `\rm`, `--recursive --force`, `.e""nv`,
7
+ * `bash -c "<here>"`, `eval "<here>"`, `$( … )`, a heredoc body, `xargs`,
8
+ * `find … -exec`) slip through as `allow`. `normalizeCommand` unfolds those
9
+ * rewrites so a downstream triage pass (triage.ts) can reason about what runs,
10
+ * not about how it was spelled.
11
+ *
12
+ * Design contract (see analysis-types.ts):
13
+ * - PURE: no I/O, no network, no clock. Disk resolution lives elsewhere.
14
+ * - NEVER THROWS: any blindness (unbalanced quotes, an indirect executable,
15
+ * nesting past the cap) sets `incomplete: true` with a stable note and
16
+ * returns what was recovered. Blindness fails toward "look closer".
17
+ * - ALLOCATION-LIGHT: this runs on every Bash tool call on the hot path.
18
+ *
19
+ * The tokenizer is `shell-quote` (`parse`), the same one `preprocess.ts` and
20
+ * `shell-writes.ts` use, driven with an env function `(k) => '$' + k` so a
21
+ * `$VAR` reference is preserved verbatim instead of being substituted to the
22
+ * empty string (which would delete the literal every rule matches on).
23
+ */
24
+ import { parse as shellParse } from 'shell-quote';
25
+ // ---------------------------------------------------------------------------
26
+ // Tunables (bounds keep a pathological input from blowing up the hot path)
27
+ // ---------------------------------------------------------------------------
28
+ /** Deepest nesting level we expand; past it we stop and note 'depth-cap'. */
29
+ const MAX_DEPTH = 3;
30
+ /** Hard ceiling on emitted commands, so a fork-bomb of segments can't blow up. */
31
+ const MAX_COMMANDS = 256;
32
+ /** Hard ceiling on substitutions pulled from one command. */
33
+ const MAX_SUBST = 64;
34
+ /** The stored `raw` field is bounded; the tokenizer still sees the full text. */
35
+ const RAW_CLIP = 2000;
36
+ /** env function for shell-quote: keep `$VAR` as the literal `$VAR`. */
37
+ const SQ_ENV = (key) => '$' + key;
38
+ // ---------------------------------------------------------------------------
39
+ // Wrapper / prefix / interpreter vocabulary
40
+ // ---------------------------------------------------------------------------
41
+ /** Interpreters whose `-c` argument is itself a shell command. */
42
+ const SHELL_INTERPRETERS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh']);
43
+ /** Interpreters whose heredoc body is code worth re-reading. */
44
+ const HEREDOC_INTERPRETERS = new Set([
45
+ 'bash',
46
+ 'sh',
47
+ 'zsh',
48
+ 'dash',
49
+ 'ksh',
50
+ 'python',
51
+ 'python2',
52
+ 'python3',
53
+ 'node',
54
+ 'nodejs',
55
+ 'ruby',
56
+ 'perl',
57
+ 'php',
58
+ ]);
59
+ /** Transparent leading wrappers that don't change what ultimately runs. */
60
+ const TRANSPARENT_PREFIXES = new Set([
61
+ 'sudo',
62
+ 'doas',
63
+ 'nohup',
64
+ 'command',
65
+ 'time',
66
+ 'nice',
67
+ 'stdbuf',
68
+ 'setsid',
69
+ 'env',
70
+ ]);
71
+ /**
72
+ * Per-prefix options that CONSUME the following token as their value, so the
73
+ * peeler does not mistake that value for the wrapped command. e.g. in
74
+ * `sudo -u root rm …`, `-u` eats `root`; `rm` is the real command.
75
+ */
76
+ const PREFIX_VALUE_OPTS = {
77
+ sudo: new Set([
78
+ '-u', '--user', '-g', '--group', '-p', '--prompt', '-C', '--close-from',
79
+ '-h', '--host', '-r', '--role', '-t', '--type', '-U', '--other-user',
80
+ '-R', '--chroot', '-D', '--chdir',
81
+ ]),
82
+ doas: new Set(['-u', '-C']),
83
+ env: new Set(['-u', '--unset', '-S', '--split-string', '-C', '--chdir']),
84
+ nice: new Set(['-n', '--adjustment']),
85
+ stdbuf: new Set(['-i', '--input', '-o', '--output', '-e', '--error']),
86
+ time: new Set(['-o', '--output', '-f', '--format']),
87
+ command: new Set(),
88
+ nohup: new Set(),
89
+ setsid: new Set(),
90
+ };
91
+ /** `xargs` options that consume the following token as their value. */
92
+ const XARGS_VALUE_OPTS = new Set([
93
+ '-I', '--replace', '-n', '--max-args', '-L', '--max-lines', '-P',
94
+ '--max-procs', '-s', '--max-chars', '-d', '--delimiter', '-E', '--eof',
95
+ '-a', '--arg-file', '--process-slot-var',
96
+ ]);
97
+ /** Operators that end a simple command (segment boundary). */
98
+ const SEPARATOR_OPS = new Set([';', '&&', '||', '|', '|&']);
99
+ /** Operators whose following token is a redirect target, not an argument. */
100
+ const REDIRECT_OPS = new Set([
101
+ '>', '>>', '<', '>|', '&>', '&>>', '<>', '>&', '<&', '<<<', '<<', '<<-',
102
+ ]);
103
+ /**
104
+ * A small, documented, extensible per-executable long→short flag map, so
105
+ * `rm --recursive --force` contributes the same `{'r','f'}` as `rm -rf`.
106
+ * Keyed by canonical (lower-cased) executable; letters carry the real case
107
+ * the tool uses (chmod/chown recursive is `-R`, grep recursive is `-r`).
108
+ * Unknown long flags add no letter — they still appear in `longFlags`.
109
+ */
110
+ const LONG_TO_SHORT = {
111
+ rm: { recursive: 'r', force: 'f', verbose: 'v', interactive: 'i', dir: 'd' },
112
+ rmdir: { verbose: 'v', parents: 'p' },
113
+ cp: {
114
+ recursive: 'r', force: 'f', verbose: 'v', interactive: 'i',
115
+ archive: 'a', link: 'l', 'symbolic-link': 's', 'no-clobber': 'n',
116
+ },
117
+ mv: { force: 'f', verbose: 'v', interactive: 'i', 'no-clobber': 'n' },
118
+ chmod: { recursive: 'R', verbose: 'v', changes: 'c', silent: 'f', quiet: 'f' },
119
+ chown: { recursive: 'R', verbose: 'v', changes: 'c', silent: 'f', quiet: 'f' },
120
+ grep: {
121
+ recursive: 'r', 'ignore-case': 'i', 'invert-match': 'v',
122
+ 'line-number': 'n', 'word-regexp': 'w', count: 'c',
123
+ 'files-with-matches': 'l', 'extended-regexp': 'E', 'fixed-strings': 'F',
124
+ },
125
+ ls: {
126
+ all: 'a', 'almost-all': 'A', recursive: 'R', reverse: 'r',
127
+ 'human-readable': 'h', directory: 'd', inode: 'i', size: 's',
128
+ },
129
+ };
130
+ /** Opening token of a heredoc redirect: `<<EOF`, `<<-EOF`, `<<'EOF'`, `<<"EOF"`. */
131
+ const HEREDOC_OPEN = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/;
132
+ function isGlob(t) {
133
+ return typeof t === 'object' && t !== null && 'pattern' in t;
134
+ }
135
+ function isOperator(t) {
136
+ return typeof t === 'object' && t !== null && 'op' in t && !('pattern' in t);
137
+ }
138
+ /** A glob keeps its pattern text; an operator/comment has no argv text. */
139
+ function tokenText(t) {
140
+ if (typeof t === 'string')
141
+ return t;
142
+ if (isGlob(t))
143
+ return t.pattern;
144
+ return null;
145
+ }
146
+ function clip(s, n) {
147
+ return s.length > n ? s.slice(0, n) : s;
148
+ }
149
+ function emit(acc, cmd) {
150
+ if (acc.commands.length >= MAX_COMMANDS) {
151
+ acc.incomplete = true;
152
+ acc.notes.add('command-cap');
153
+ return false;
154
+ }
155
+ acc.commands.push(cmd);
156
+ return true;
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Executable canonicalization & prefix peeling
160
+ // ---------------------------------------------------------------------------
161
+ /** True when the executable is supplied indirectly (`$X`, `${X}`, `$(…)`, `` `…` ``). */
162
+ function isIndirect(tok) {
163
+ return /^\$/.test(tok) || tok.includes('`');
164
+ }
165
+ /**
166
+ * Canonical executable name: strip the directory, strip a single leading
167
+ * backslash, lower-case the basename. `/usr/bin/rm` → `rm`, `\rm` → `rm`,
168
+ * `RM` → `rm`.
169
+ */
170
+ function canonicalExec(raw) {
171
+ let base = raw.split('/').pop() ?? raw;
172
+ if (base.startsWith('\\'))
173
+ base = base.slice(1);
174
+ return base.toLowerCase();
175
+ }
176
+ /**
177
+ * Peel leading `VAR=value` assignments and transparent wrappers (`sudo`,
178
+ * `env FOO=bar`, `nice -n 10`, …) so the first remaining token is the real
179
+ * command. Returns the remainder; an empty remainder means the segment was
180
+ * all assignments/wrappers (the caller then keeps the original argv).
181
+ */
182
+ function peelPrefixes(argv) {
183
+ let i = 0;
184
+ while (i < argv.length) {
185
+ const tok = argv[i];
186
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok)) {
187
+ i++; // leading VAR=value assignment
188
+ continue;
189
+ }
190
+ if (TRANSPARENT_PREFIXES.has(tok)) {
191
+ const prefix = tok;
192
+ i++;
193
+ const valueOpts = PREFIX_VALUE_OPTS[prefix] ?? new Set();
194
+ while (i < argv.length) {
195
+ const a = argv[i];
196
+ // `env VAR=val cmd` — drop env's own assignments too.
197
+ if (prefix === 'env' && /^[A-Za-z_][A-Za-z0-9_]*=/.test(a)) {
198
+ i++;
199
+ continue;
200
+ }
201
+ if (a.startsWith('-') && a.length > 1) {
202
+ if (!a.includes('=') && valueOpts.has(a))
203
+ i += 2;
204
+ else
205
+ i++;
206
+ continue;
207
+ }
208
+ break;
209
+ }
210
+ continue;
211
+ }
212
+ break;
213
+ }
214
+ return argv.slice(i);
215
+ }
216
+ // ---------------------------------------------------------------------------
217
+ // Segment splitting & argv collection (token level)
218
+ // ---------------------------------------------------------------------------
219
+ /**
220
+ * Split a line's tokens into segments on `;`, `&&`, `||`, `|`, `|&`, `&`.
221
+ * `&` is a separator UNLESS it is the `&>` redirect spelled as two ops.
222
+ */
223
+ function splitSegments(tokens) {
224
+ const segs = [];
225
+ let cur = [];
226
+ for (let i = 0; i < tokens.length; i++) {
227
+ const tok = tokens[i];
228
+ if (isOperator(tok)) {
229
+ const op = tok.op;
230
+ if (op === '&') {
231
+ const nxt = tokens[i + 1];
232
+ if (isOperator(nxt) && (nxt.op === '>' || nxt.op === '>>')) {
233
+ cur.push(tok); // part of `&>` / `&>>`
234
+ continue;
235
+ }
236
+ segs.push(cur);
237
+ cur = [];
238
+ continue;
239
+ }
240
+ if (SEPARATOR_OPS.has(op)) {
241
+ segs.push(cur);
242
+ cur = [];
243
+ continue;
244
+ }
245
+ cur.push(tok);
246
+ continue;
247
+ }
248
+ cur.push(tok);
249
+ }
250
+ segs.push(cur);
251
+ return segs.filter((s) => s.length > 0);
252
+ }
253
+ /**
254
+ * Collect the argv of one segment: the string/glob tokens that are the
255
+ * executable, flags and arguments. Command-substitution content (`$( … )`)
256
+ * is dropped (it is recovered separately via `findSubstitutions`), a
257
+ * substitution standing in the executable position leaves a `$(` marker so
258
+ * the command is flagged indirect, and redirect targets are dropped.
259
+ */
260
+ function collectArgv(tokens) {
261
+ const argv = [];
262
+ let substDepth = 0;
263
+ let skipTarget = false;
264
+ for (let i = 0; i < tokens.length; i++) {
265
+ const tok = tokens[i];
266
+ if (isOperator(tok)) {
267
+ const op = tok.op;
268
+ if (op === '(') {
269
+ // A `$(` command substitution only when preceded by the `$` marker.
270
+ if (tokens[i - 1] === '$') {
271
+ if (substDepth === 0 && argv.length === 0)
272
+ argv.push('$(');
273
+ substDepth++;
274
+ }
275
+ continue;
276
+ }
277
+ if (op === ')') {
278
+ if (substDepth > 0)
279
+ substDepth--;
280
+ continue;
281
+ }
282
+ if (substDepth > 0)
283
+ continue;
284
+ if (REDIRECT_OPS.has(op)) {
285
+ // `2> err` — drop the fd number that precedes the redirect.
286
+ if (argv.length > 0 && /^\d+$/.test(argv[argv.length - 1]))
287
+ argv.pop();
288
+ skipTarget = true;
289
+ continue;
290
+ }
291
+ continue; // any other op inside a segment is structural, ignore
292
+ }
293
+ if (substDepth > 0)
294
+ continue;
295
+ const text = tokenText(tok);
296
+ if (text === null)
297
+ continue; // a comment
298
+ // The `$` marker just before a `$(` substitution is not an argument.
299
+ const next = tokens[i + 1];
300
+ if (text === '$' && isOperator(next) && next.op === '(')
301
+ continue;
302
+ if (skipTarget) {
303
+ skipTarget = false;
304
+ continue;
305
+ }
306
+ argv.push(text);
307
+ }
308
+ return argv;
309
+ }
310
+ // ---------------------------------------------------------------------------
311
+ // Building one EffectiveCommand
312
+ // ---------------------------------------------------------------------------
313
+ function buildEffectiveCommand(eff, via, depth, rawText, acc) {
314
+ const rawExecutable = eff[0] ?? '';
315
+ let executable;
316
+ if (isIndirect(rawExecutable)) {
317
+ executable = '';
318
+ acc.incomplete = true;
319
+ acc.notes.add('indirect-exec');
320
+ }
321
+ else {
322
+ executable = canonicalExec(rawExecutable);
323
+ }
324
+ const flagLetters = new Set();
325
+ const longFlags = [];
326
+ const args = [];
327
+ let endOfOpts = false;
328
+ for (let i = 1; i < eff.length; i++) {
329
+ const t = eff[i];
330
+ if (endOfOpts) {
331
+ args.push(t);
332
+ continue;
333
+ }
334
+ if (t === '--') {
335
+ endOfOpts = true;
336
+ continue;
337
+ }
338
+ if (t.startsWith('--') && t.length > 2) {
339
+ const m = /^--([A-Za-z][A-Za-z0-9-]*)/.exec(t);
340
+ if (m) {
341
+ longFlags.push(m[1]);
342
+ const letter = LONG_TO_SHORT[executable]?.[m[1]];
343
+ if (letter)
344
+ flagLetters.add(letter);
345
+ }
346
+ continue;
347
+ }
348
+ if (t.startsWith('-') && t.length > 1) {
349
+ const m = /^-([A-Za-z]+)/.exec(t);
350
+ if (m)
351
+ for (const ch of m[1])
352
+ flagLetters.add(ch);
353
+ continue;
354
+ }
355
+ args.push(t);
356
+ }
357
+ return {
358
+ executable,
359
+ rawExecutable,
360
+ args,
361
+ flagLetters,
362
+ longFlags,
363
+ raw: clip(rawText, RAW_CLIP),
364
+ via,
365
+ depth,
366
+ };
367
+ }
368
+ // ---------------------------------------------------------------------------
369
+ // Per-segment nested-construct extraction
370
+ // ---------------------------------------------------------------------------
371
+ /** The command a shell interpreter runs via `-c` (`-c`, `-lc`, `-xc`, …). */
372
+ function shellCArg(eff) {
373
+ for (let i = 1; i < eff.length; i++) {
374
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(eff[i])) {
375
+ return i + 1 < eff.length ? eff[i + 1] : null;
376
+ }
377
+ }
378
+ return null;
379
+ }
380
+ /** The command `xargs` will run: skip xargs' own options (and their values). */
381
+ function xargsCommand(eff) {
382
+ let i = 1; // skip `xargs`
383
+ while (i < eff.length) {
384
+ const a = eff[i];
385
+ if (a.startsWith('-') && a.length > 1) {
386
+ if (!a.includes('=') && XARGS_VALUE_OPTS.has(a))
387
+ i += 2;
388
+ else
389
+ i++;
390
+ continue;
391
+ }
392
+ break;
393
+ }
394
+ if (i >= eff.length)
395
+ return null;
396
+ return eff.slice(i).join(' ');
397
+ }
398
+ /** The command `find … -exec`/`-execdir` will run, up to its `;`/`+` terminator. */
399
+ function findExecCommand(eff) {
400
+ let idx = -1;
401
+ for (let i = 1; i < eff.length; i++) {
402
+ const t = eff[i];
403
+ if (t === '-exec' || t === '-execdir' || t === '-ok' || t === '-okdir') {
404
+ idx = i;
405
+ break;
406
+ }
407
+ }
408
+ if (idx < 0 || idx + 1 >= eff.length)
409
+ return null;
410
+ const rest = eff.slice(idx + 1);
411
+ const term = rest.findIndex((t) => t === ';' || t === '+' || t === '\\;');
412
+ const cmdTokens = term >= 0 ? rest.slice(0, term) : rest;
413
+ if (cmdTokens.length === 0)
414
+ return null;
415
+ return cmdTokens.join(' ');
416
+ }
417
+ // ---------------------------------------------------------------------------
418
+ // Heredoc extraction
419
+ // ---------------------------------------------------------------------------
420
+ /** First command word of a line, with prefixes peeled and canonicalized. */
421
+ function firstCommandWord(line) {
422
+ const parts = line.trim().split(/\s+/).filter(Boolean);
423
+ const peeled = peelPrefixes(parts);
424
+ const eff = peeled.length > 0 ? peeled : parts;
425
+ return eff.length > 0 && !isIndirect(eff[0]) ? canonicalExec(eff[0]) : '';
426
+ }
427
+ /**
428
+ * Pull heredoc BODIES out of the command so their content (which may contain
429
+ * shell operators) never corrupts segmentation, and so an interpreter-fed
430
+ * body can be re-normalized. The opener line is kept with its `<<DELIM`
431
+ * redirect removed; `cat <<EOF` (data, not code) is still stripped but its
432
+ * `cat` interpreter is not in HEREDOC_INTERPRETERS, so it is never recursed.
433
+ */
434
+ function extractHeredocs(raw) {
435
+ if (raw.indexOf('<<') === -1)
436
+ return { scrubbed: raw, heredocs: [] };
437
+ const lines = raw.split('\n');
438
+ const out = [];
439
+ const heredocs = [];
440
+ let i = 0;
441
+ while (i < lines.length) {
442
+ const line = lines[i];
443
+ const m = HEREDOC_OPEN.exec(line);
444
+ if (!m) {
445
+ out.push(line);
446
+ i++;
447
+ continue;
448
+ }
449
+ const delim = m[2];
450
+ const openerNoHeredoc = line.slice(0, m.index) + line.slice(m.index + m[0].length);
451
+ out.push(openerNoHeredoc);
452
+ const interpreter = firstCommandWord(openerNoHeredoc);
453
+ i++;
454
+ const body = [];
455
+ while (i < lines.length && lines[i].trim() !== delim) {
456
+ body.push(lines[i]);
457
+ i++;
458
+ }
459
+ if (i < lines.length)
460
+ i++; // consume the terminator line
461
+ heredocs.push({ interpreter, body: body.join('\n') });
462
+ }
463
+ return { scrubbed: out.join('\n'), heredocs };
464
+ }
465
+ // ---------------------------------------------------------------------------
466
+ // Command-substitution extraction (`$( … )` and backticks)
467
+ // ---------------------------------------------------------------------------
468
+ function captureParen(text, start) {
469
+ let depth = 1;
470
+ let i = start;
471
+ while (i < text.length) {
472
+ const c = text[i];
473
+ if (c === '\\') {
474
+ i += 2;
475
+ continue;
476
+ }
477
+ if (c === '(')
478
+ depth++;
479
+ else if (c === ')') {
480
+ depth--;
481
+ if (depth === 0)
482
+ return { inner: text.slice(start, i), end: i + 1 };
483
+ }
484
+ i++;
485
+ }
486
+ return { inner: text.slice(start), end: text.length }; // unterminated
487
+ }
488
+ /** Skip an arithmetic expansion `$(( … ))` (not a command). */
489
+ function skipArithmetic(text, at) {
490
+ let depth = 0;
491
+ let j = at + 1; // at the first '('
492
+ while (j < text.length) {
493
+ const c = text[j];
494
+ if (c === '(')
495
+ depth++;
496
+ else if (c === ')') {
497
+ depth--;
498
+ if (depth === 0)
499
+ return j + 1;
500
+ }
501
+ j++;
502
+ }
503
+ return text.length;
504
+ }
505
+ function findUnescapedBacktick(text, from) {
506
+ let i = from;
507
+ while (i < text.length) {
508
+ if (text[i] === '\\') {
509
+ i += 2;
510
+ continue;
511
+ }
512
+ if (text[i] === '`')
513
+ return i;
514
+ i++;
515
+ }
516
+ return -1;
517
+ }
518
+ /**
519
+ * Every command-substitution body in `text` (`$( … )` and backticks),
520
+ * honouring shell quoting: a substitution inside SINGLE quotes does not run
521
+ * (`echo '$(rm -rf ~)'`), so it is not extracted — avoiding a false positive;
522
+ * one inside DOUBLE quotes does run and is extracted. Arithmetic `$(( … ))`
523
+ * is skipped. Pure string scan, no tokenizer.
524
+ */
525
+ function findSubstitutions(text) {
526
+ const out = [];
527
+ let i = 0;
528
+ let single = false;
529
+ let double = false;
530
+ while (i < text.length && out.length < MAX_SUBST) {
531
+ const c = text[i];
532
+ if (c === '\\' && !single) {
533
+ i += 2;
534
+ continue;
535
+ }
536
+ if (c === "'" && !double) {
537
+ single = !single;
538
+ i++;
539
+ continue;
540
+ }
541
+ if (c === '"' && !single) {
542
+ double = !double;
543
+ i++;
544
+ continue;
545
+ }
546
+ if (single) {
547
+ i++;
548
+ continue;
549
+ }
550
+ if (c === '$' && text[i + 1] === '(') {
551
+ if (text[i + 2] === '(') {
552
+ i = skipArithmetic(text, i + 1);
553
+ continue;
554
+ }
555
+ const { inner, end } = captureParen(text, i + 2);
556
+ if (inner.trim().length > 0)
557
+ out.push(inner);
558
+ i = end;
559
+ continue;
560
+ }
561
+ if (c === '`') {
562
+ const close = findUnescapedBacktick(text, i + 1);
563
+ if (close > i) {
564
+ const inner = text.slice(i + 1, close);
565
+ if (inner.trim().length > 0)
566
+ out.push(inner);
567
+ i = close + 1;
568
+ }
569
+ else {
570
+ i = text.length; // unterminated
571
+ }
572
+ continue;
573
+ }
574
+ i++;
575
+ }
576
+ return out;
577
+ }
578
+ // ---------------------------------------------------------------------------
579
+ // Recursion driver
580
+ // ---------------------------------------------------------------------------
581
+ function recurse(inner, via, depth, acc) {
582
+ if (depth > MAX_DEPTH) {
583
+ acc.incomplete = true;
584
+ acc.notes.add('depth-cap');
585
+ return;
586
+ }
587
+ if (acc.commands.length >= MAX_COMMANDS) {
588
+ acc.incomplete = true;
589
+ acc.notes.add('command-cap');
590
+ return;
591
+ }
592
+ analyze(inner, via, depth, acc);
593
+ }
594
+ /** Emit one best-effort command from a naive whitespace split (parse fallback). */
595
+ function pushNaive(text, via, depth, acc) {
596
+ const parts = text.trim().split(/\s+/).filter(Boolean);
597
+ if (parts.length === 0)
598
+ return;
599
+ const peeled = peelPrefixes(parts);
600
+ const eff = peeled.length > 0 ? peeled : parts;
601
+ emit(acc, buildEffectiveCommand(eff, via, depth, text, acc));
602
+ }
603
+ function processSegment(segTokens, via, depth, acc) {
604
+ const argvFull = collectArgv(segTokens);
605
+ if (argvFull.length === 0)
606
+ return;
607
+ const peeled = peelPrefixes(argvFull);
608
+ const eff = peeled.length > 0 ? peeled : argvFull;
609
+ const rawText = argvFull.join(' ');
610
+ const cmd = buildEffectiveCommand(eff, via, depth, rawText, acc);
611
+ if (!emit(acc, cmd))
612
+ return;
613
+ const exe = cmd.executable;
614
+ if (exe === '')
615
+ return; // indirect executable: nothing concrete to recurse
616
+ const nextDepth = depth + 1;
617
+ if (SHELL_INTERPRETERS.has(exe)) {
618
+ const body = shellCArg(eff);
619
+ if (body !== null && body.trim().length > 0)
620
+ recurse(body, 'bash-c', nextDepth, acc);
621
+ }
622
+ else if (exe === 'eval') {
623
+ const body = eff.slice(1).join(' ');
624
+ if (body.trim().length > 0)
625
+ recurse(body, 'eval', nextDepth, acc);
626
+ }
627
+ else if (exe === 'xargs') {
628
+ const body = xargsCommand(eff);
629
+ if (body !== null && body.trim().length > 0)
630
+ recurse(body, 'xargs', nextDepth, acc);
631
+ }
632
+ else if (exe === 'find') {
633
+ const body = findExecCommand(eff);
634
+ if (body !== null && body.trim().length > 0)
635
+ recurse(body, 'exec', nextDepth, acc);
636
+ }
637
+ }
638
+ function analyze(raw, via, depth, acc) {
639
+ if (acc.commands.length >= MAX_COMMANDS) {
640
+ acc.incomplete = true;
641
+ acc.notes.add('command-cap');
642
+ return;
643
+ }
644
+ const { scrubbed, heredocs } = extractHeredocs(raw);
645
+ // Top-level segments, one tokenize per logical line (newline is a boundary
646
+ // that shell-quote otherwise flattens away).
647
+ for (const line of scrubbed.split('\n')) {
648
+ if (acc.commands.length >= MAX_COMMANDS)
649
+ break;
650
+ if (line.trim().length === 0)
651
+ continue;
652
+ let tokens;
653
+ try {
654
+ tokens = shellParse(line, SQ_ENV);
655
+ }
656
+ catch {
657
+ acc.incomplete = true;
658
+ acc.notes.add('parse-failed');
659
+ pushNaive(line, via, depth, acc);
660
+ continue;
661
+ }
662
+ for (const seg of splitSegments(tokens))
663
+ processSegment(seg, via, depth, acc);
664
+ }
665
+ // Command substitutions anywhere in this (heredoc-stripped) command.
666
+ for (const inner of findSubstitutions(scrubbed))
667
+ recurse(inner, 'subst', depth + 1, acc);
668
+ // Interpreter-fed heredoc bodies are code; re-normalize them.
669
+ for (const hd of heredocs) {
670
+ if (HEREDOC_INTERPRETERS.has(hd.interpreter) && hd.body.trim().length > 0) {
671
+ recurse(hd.body, 'heredoc', depth + 1, acc);
672
+ }
673
+ }
674
+ }
675
+ // ---------------------------------------------------------------------------
676
+ // Public entry point
677
+ // ---------------------------------------------------------------------------
678
+ /**
679
+ * Normalize one raw Bash command string into the effective simple commands it
680
+ * will run. Pure and never throws: on any failure it returns what it recovered
681
+ * with `incomplete: true` and a stable note.
682
+ */
683
+ export function normalizeCommand(raw) {
684
+ const acc = { commands: [], incomplete: false, notes: new Set() };
685
+ try {
686
+ if (typeof raw !== 'string') {
687
+ return { commands: [], incomplete: true, notes: ['invalid-input'] };
688
+ }
689
+ if (raw.trim().length === 0) {
690
+ return { commands: [], incomplete: false, notes: [] };
691
+ }
692
+ analyze(raw, 'top', 0, acc);
693
+ if (acc.commands.length === 0) {
694
+ // A non-empty input always yields at least one command.
695
+ pushNaive(raw, 'top', 0, acc);
696
+ }
697
+ }
698
+ catch {
699
+ acc.incomplete = true;
700
+ acc.notes.add('internal-error');
701
+ if (acc.commands.length === 0) {
702
+ try {
703
+ pushNaive(raw, 'top', 0, acc);
704
+ }
705
+ catch {
706
+ /* give up: return whatever we have, never throw */
707
+ }
708
+ }
709
+ }
710
+ return {
711
+ commands: acc.commands,
712
+ incomplete: acc.incomplete,
713
+ notes: [...acc.notes],
714
+ };
715
+ }
716
+ //# sourceMappingURL=normalize.js.map