@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,695 @@
1
+ /**
2
+ * Level 1 of the Action Guards hardening: READ A SCRIPT THE AGENT EXECUTES.
3
+ *
4
+ * The regex evaluator sees only the launcher command. When an agent runs a
5
+ * file it wrote — `bash cleanup.sh`, `./deploy.sh`, `python tools/x.py`,
6
+ * `npm run cleanup` — the guard never sees the dangerous commands INSIDE that
7
+ * file. This module closes that blind spot: given a raw Bash command, it
8
+ * detects that the command executes a local file/script, reads it (bounded,
9
+ * from disk — the hook runs locally), and returns the inner commands/content
10
+ * so the integrator can re-feed them to Level 0 (normalize → triage → regex).
11
+ *
12
+ * It does NOT itself scan for danger and it does NOT recurse into whatever the
13
+ * inner commands execute — the integrator feeds `shellCommands` back through
14
+ * Level 0, which may call this module again. One hop per call, bounded reads,
15
+ * never throws.
16
+ *
17
+ * ── What it detects ─────────────────────────────────────────────────────────
18
+ * - `bash|sh|zsh|dash|ksh <file>` → kind 'shell' (split into lines)
19
+ * - `./<file>` / `dir/<file>` (a path) → kind 'shell' | 'interpreter'
20
+ * (decided by shebang / extension)
21
+ * - `python|python3|node|tsx|ts-node|ruby|perl|php|Rscript|lua <f>`,
22
+ * `deno run <f>`, `bun [run] <f.ts|.js>` → kind 'interpreter' (content only)
23
+ * - `npm|pnpm|yarn|bun run <script>` → kind 'npm-script' (the script body)
24
+ * - behind `npx`, `bunx`, `pnpm exec`, `yarn exec`, `uv run`, `poetry run`,
25
+ * `pipenv run` (`uv run x.py` runs it with Python)
26
+ *
27
+ * For a 'shell' / 'npm-script' result the inner commands are in `shellCommands`
28
+ * (one per logical command line; heredoc bodies stripped, line continuations
29
+ * joined, comments and blank lines dropped). A 'shell' or 'interpreter' result
30
+ * carries the raw file text in `content` and its `language`: the script judge
31
+ * (guard-check.ts) reads it. Parsing Python/JS/Ruby into shell commands is out
32
+ * of scope.
33
+ *
34
+ * ── Documented limits (deliberately out of scope) ───────────────────────────
35
+ * - INLINE code is NOT handled here and returns `null` — `bash -c "…"`,
36
+ * `sh -c …`, `python -c …`, `node -e/-p …`, `ruby -e …`, `perl -e/-pe …`.
37
+ * De-obfuscating inline interpreter strings is the normalizer's job.
38
+ * - `python -m <module>` runs a module, not a resolvable file → `null`.
39
+ * - `git apply` / `patch` (targets live inside the patch text) are not read.
40
+ * - `source file` / `. file` (sourcing) is NOT followed.
41
+ * - No `~` / `$VAR` expansion in script paths; the path is resolved against
42
+ * `cwd` literally. A path that does not exist returns `null` — never guess.
43
+ * - At most ONE level of `npm run <other>` indirection is followed (noted).
44
+ * - Reads are capped at `maxBytes` (default 256 KB); a larger file is read up
45
+ * to the cap and `truncated` is set.
46
+ *
47
+ * Pure over its inputs except for the injected filesystem (`opts.fs`, default
48
+ * `node:fs`). Matches the tokenizer and heredoc handling of `shell-writes.ts`:
49
+ * `shell-quote` with an env function so `$VAR` survives, bodies stripped first.
50
+ */
51
+ import { parse as shellParse } from 'shell-quote';
52
+ import { isAbsolute, resolve as pathResolve, extname } from 'node:path';
53
+ import * as nodeFs from 'node:fs';
54
+ import { stripHeredocBodies } from './shell-writes.js';
55
+ const DEFAULT_MAX_BYTES = 256 * 1024;
56
+ const HARD_MAX_BYTES = 8 * 1024 * 1024;
57
+ /** Operators that end a simple command and start a new segment. */
58
+ const BOUNDARY_OPS = new Set([';', '&&', '||', '|', '|&', '&', '(', ')', '\n']);
59
+ /** Wrappers that prefix a command without changing which file it executes. */
60
+ const TRANSPARENT_PREFIXES = new Set(['sudo', 'doas', 'nohup', 'command', 'time', 'nice', 'env', 'exec']);
61
+ /** Interpreters whose script argument is a shell script. */
62
+ const SHELL_INTERPRETERS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh', 'ash']);
63
+ /** Package-manager launchers that take a `run <script>` form. */
64
+ const NPM_RUNNERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
65
+ const SHELL_SPEC = {
66
+ // `-c` = command string, `-s` = read from stdin: neither names a file to read.
67
+ inlineExact: new Set(['-c', '-s']),
68
+ valueFlags: new Set(['-o', '+o', '-O', '+O']),
69
+ };
70
+ const PYTHON_SPEC = { inlineExact: new Set(['-c']), moduleExact: new Set(['-m']), valueFlags: new Set(['-W', '-X', '-Q']) };
71
+ const NODE_SPEC = {
72
+ inlineExact: new Set(['-e', '--eval', '-p', '--print']),
73
+ valueFlags: new Set(['-r', '--require', '--loader', '--import', '--experimental-loader']),
74
+ };
75
+ const INTERPRETERS = new Map([
76
+ ['python', PYTHON_SPEC],
77
+ ['python2', PYTHON_SPEC],
78
+ ['python3', PYTHON_SPEC],
79
+ ['node', NODE_SPEC],
80
+ ['nodejs', NODE_SPEC],
81
+ ['tsx', NODE_SPEC],
82
+ ['ts-node', NODE_SPEC],
83
+ ['ruby', { inlineExact: new Set(['-e']), valueFlags: new Set(['-r', '-I', '-C']) }],
84
+ ['perl', { inlineExact: new Set(['-e', '-E']), inlineRegex: /^-[A-Za-z0-9]*[eE]$/, valueFlags: new Set(['-I']) }],
85
+ ['php', { inlineExact: new Set(['-r']), valueFlags: new Set(['-c', '-d', '-z']) }],
86
+ ['rscript', { inlineExact: new Set(['-e']), valueFlags: new Set([]) }],
87
+ ['lua', { inlineExact: new Set(['-e']), valueFlags: new Set(['-l']) }],
88
+ ]);
89
+ /** The language an interpreter runs (a JS runtime reads the file's extension). */
90
+ const INTERPRETER_LANGUAGE = {
91
+ python: 'python',
92
+ python2: 'python',
93
+ python3: 'python',
94
+ ruby: 'ruby',
95
+ perl: 'perl',
96
+ php: 'php',
97
+ rscript: 'r',
98
+ lua: 'lua',
99
+ };
100
+ const EXTENSION_LANGUAGE = {
101
+ '.py': 'python',
102
+ '.js': 'javascript',
103
+ '.mjs': 'javascript',
104
+ '.cjs': 'javascript',
105
+ '.jsx': 'javascript',
106
+ '.ts': 'typescript',
107
+ '.mts': 'typescript',
108
+ '.cts': 'typescript',
109
+ '.tsx': 'typescript',
110
+ '.rb': 'ruby',
111
+ '.pl': 'perl',
112
+ '.php': 'php',
113
+ '.r': 'r',
114
+ '.lua': 'lua',
115
+ '.sh': 'shell',
116
+ '.bash': 'shell',
117
+ '.zsh': 'shell',
118
+ '.ksh': 'shell',
119
+ };
120
+ /** Language named by a `#!` line, or null. */
121
+ function shebangLanguage(content) {
122
+ const nl = content.indexOf('\n');
123
+ const shebang = (nl === -1 ? content : content.slice(0, nl)).trim();
124
+ if (!shebang.startsWith('#!'))
125
+ return null;
126
+ if (/\bpython[23]?\b/.test(shebang))
127
+ return 'python';
128
+ if (/\b(node|nodejs|deno|bun)\b/.test(shebang))
129
+ return 'javascript';
130
+ if (/\b(tsx|ts-node)\b/.test(shebang))
131
+ return 'typescript';
132
+ if (/\bruby\b/.test(shebang))
133
+ return 'ruby';
134
+ if (/\bperl\b/.test(shebang))
135
+ return 'perl';
136
+ if (/\bphp\b/.test(shebang))
137
+ return 'php';
138
+ if (/\bRscript\b/.test(shebang))
139
+ return 'r';
140
+ if (/\blua\b/.test(shebang))
141
+ return 'lua';
142
+ if (/\b(sh|bash|zsh|dash|ksh|ash)\b/.test(shebang))
143
+ return 'shell';
144
+ return null;
145
+ }
146
+ /** Language of a file an interpreter (or the kernel, via its shebang) runs. */
147
+ function languageOf(abs, content, interpreter) {
148
+ const byInterpreter = interpreter ? INTERPRETER_LANGUAGE[interpreter] : undefined;
149
+ if (byInterpreter)
150
+ return byInterpreter;
151
+ const byExtension = EXTENSION_LANGUAGE[extname(abs).toLowerCase()];
152
+ if (interpreter)
153
+ return byExtension === 'typescript' ? 'typescript' : 'javascript';
154
+ return shebangLanguage(content) ?? byExtension ?? 'shell';
155
+ }
156
+ /** Extensions a JS runtime (`bun`, `deno`) runs as a file rather than a script name. */
157
+ const JS_RUNTIME_FILE_EXT = new Set(['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx']);
158
+ /**
159
+ * `deno run` flags that take a value as the next token. Most deno flags are
160
+ * written `--flag=value` or carry no value, so the list is short.
161
+ */
162
+ const DENO_VALUE_FLAGS = new Set(['--config', '-c', '--import-map', '--lock', '--cert', '--location', '--env-file', '--inspect']);
163
+ /** `npx` / `bunx` flags that take a value as the next token. */
164
+ const NPX_VALUE_FLAGS = new Set(['-p', '--package', '-c', '--call']);
165
+ /** `uv run` flags that take a value as the next token. */
166
+ const UV_RUN_VALUE_FLAGS = new Set([
167
+ '--with',
168
+ '--with-editable',
169
+ '--with-requirements',
170
+ '--python',
171
+ '-p',
172
+ '--project',
173
+ '--directory',
174
+ '--package',
175
+ '--extra',
176
+ '--group',
177
+ '--env-file',
178
+ '--index',
179
+ '--index-url',
180
+ ]);
181
+ function isOp(tok) {
182
+ return typeof tok === 'object' && tok !== null && 'op' in tok;
183
+ }
184
+ function tokenText(tok) {
185
+ if (typeof tok === 'string')
186
+ return tok;
187
+ if (typeof tok === 'object' && tok !== null && 'pattern' in tok)
188
+ return tok.pattern;
189
+ return null;
190
+ }
191
+ /** Lower-cased basename of an executable token, leading backslash stripped (`\rm` → `rm`). */
192
+ function canonicalName(tok) {
193
+ let t = tok;
194
+ if (t.startsWith('\\'))
195
+ t = t.slice(1);
196
+ const base = t.split('/').pop() ?? t;
197
+ return base.toLowerCase();
198
+ }
199
+ /** Does this executable token name a file path (rather than a bare PATH command)? */
200
+ function isPathToken(tok) {
201
+ return tok.includes('/');
202
+ }
203
+ function normalizeMaxBytes(v) {
204
+ if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0)
205
+ return DEFAULT_MAX_BYTES;
206
+ return Math.min(Math.floor(v), HARD_MAX_BYTES);
207
+ }
208
+ const DEFAULT_FS = {
209
+ existsSync: (p) => nodeFs.existsSync(p),
210
+ readFileSync: (p, enc) => nodeFs.readFileSync(p, enc),
211
+ statSync: (p) => {
212
+ const s = nodeFs.statSync(p);
213
+ return { size: s.size, isFile: () => s.isFile() };
214
+ },
215
+ openSync: (p, flags) => nodeFs.openSync(p, flags),
216
+ readSync: (fd, buffer, offset, length, position) => nodeFs.readSync(fd, buffer, offset, length, position),
217
+ closeSync: (fd) => nodeFs.closeSync(fd),
218
+ };
219
+ /** Resolve a (possibly relative) script path against cwd. No `~` / `$VAR` expansion. */
220
+ function resolvePath(cwd, rel) {
221
+ if (isAbsolute(rel))
222
+ return rel;
223
+ return pathResolve(cwd && cwd.length > 0 ? cwd : '.', rel);
224
+ }
225
+ /** Exists AND (when the fs can tell) is a regular file. Never throws. */
226
+ function pathExistsAsFile(fsLike, abs) {
227
+ try {
228
+ if (!fsLike.existsSync(abs))
229
+ return false;
230
+ try {
231
+ const st = fsLike.statSync(abs);
232
+ if (st && typeof st.isFile === 'function' && !st.isFile())
233
+ return false;
234
+ }
235
+ catch {
236
+ /* existsSync already said yes; stat is only a refinement */
237
+ }
238
+ return true;
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ }
244
+ function sliceToBytes(text, maxBytes) {
245
+ return Buffer.from(text, 'utf8').subarray(0, maxBytes).toString('utf8');
246
+ }
247
+ /** Bounded read. Returns null on any read failure. Never throws. */
248
+ function safeRead(fsLike, abs, maxBytes) {
249
+ try {
250
+ let size = -1;
251
+ try {
252
+ const st = fsLike.statSync(abs);
253
+ if (st && typeof st.size === 'number')
254
+ size = st.size;
255
+ }
256
+ catch {
257
+ /* fall through: read the whole thing and bound by byte length */
258
+ }
259
+ // Genuine partial read when the file is oversized and the fd API is present.
260
+ if (size > maxBytes && typeof fsLike.openSync === 'function' && typeof fsLike.readSync === 'function') {
261
+ const fd = fsLike.openSync(abs, 'r');
262
+ try {
263
+ const buf = Buffer.alloc(maxBytes);
264
+ const n = fsLike.readSync(fd, buf, 0, maxBytes, 0);
265
+ return { content: buf.subarray(0, Math.max(0, n)).toString('utf8'), truncated: true };
266
+ }
267
+ finally {
268
+ try {
269
+ fsLike.closeSync?.(fd);
270
+ }
271
+ catch {
272
+ /* best effort */
273
+ }
274
+ }
275
+ }
276
+ const raw = fsLike.readFileSync(abs, 'utf8');
277
+ const text = typeof raw === 'string' ? raw : raw == null ? '' : String(raw);
278
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) {
279
+ return { content: sliceToBytes(text, maxBytes), truncated: true };
280
+ }
281
+ return { content: text, truncated: size > maxBytes };
282
+ }
283
+ catch {
284
+ return null;
285
+ }
286
+ }
287
+ /** Trailing backslash count is odd → the line continues onto the next one. */
288
+ function endsWithOddBackslash(line) {
289
+ const m = /\\+$/.exec(line);
290
+ return m ? m[0].length % 2 === 1 : false;
291
+ }
292
+ function pushCommandLine(out, line) {
293
+ const trimmed = line.trim();
294
+ if (trimmed.length === 0)
295
+ return;
296
+ if (trimmed.startsWith('#'))
297
+ return; // full-line comment
298
+ out.push(trimmed);
299
+ }
300
+ /**
301
+ * Split shell-script text into logical command lines. Heredoc bodies are
302
+ * stripped first (their content is data, not commands), `\`-continuations are
303
+ * joined, blank and comment-only lines are dropped. Each line is left whole —
304
+ * the evaluator's own normalizer splits a line on `;`/`&&`/`||`/`|`.
305
+ */
306
+ function splitShellScriptLines(content) {
307
+ let stripped;
308
+ try {
309
+ stripped = stripHeredocBodies(content);
310
+ }
311
+ catch {
312
+ stripped = content;
313
+ }
314
+ const rawLines = stripped.split(/\r?\n/);
315
+ const out = [];
316
+ let acc = null;
317
+ for (const rawLine of rawLines) {
318
+ const current = acc === null ? rawLine : acc + rawLine;
319
+ if (endsWithOddBackslash(rawLine)) {
320
+ acc = current.replace(/\\$/, '');
321
+ continue;
322
+ }
323
+ acc = null;
324
+ pushCommandLine(out, current);
325
+ }
326
+ if (acc !== null)
327
+ pushCommandLine(out, acc);
328
+ return out;
329
+ }
330
+ /** Split a command line into simple-command argv arrays on segment boundaries. */
331
+ function segmentsOf(command) {
332
+ let tokens;
333
+ try {
334
+ tokens = shellParse(stripHeredocBodies(command), (key) => `$${key}`);
335
+ }
336
+ catch {
337
+ return [];
338
+ }
339
+ const segments = [];
340
+ let argv = [];
341
+ const flush = () => {
342
+ if (argv.length > 0)
343
+ segments.push(argv);
344
+ argv = [];
345
+ };
346
+ for (const tok of tokens) {
347
+ if (isOp(tok)) {
348
+ if (BOUNDARY_OPS.has(tok.op))
349
+ flush();
350
+ // Redirect operators and their operands are left out of argv: a redirect
351
+ // target is never the executable, which is the first non-flag token.
352
+ continue;
353
+ }
354
+ const text = tokenText(tok);
355
+ if (text !== null)
356
+ argv.push(text);
357
+ }
358
+ flush();
359
+ return segments;
360
+ }
361
+ /** Skip flags from `i`, consuming the next token after a flag of `valueFlags`. */
362
+ function skipFlags(argv, i, valueFlags) {
363
+ while (i < argv.length && argv[i].startsWith('-')) {
364
+ if (valueFlags.has(argv[i]))
365
+ i++;
366
+ i++;
367
+ }
368
+ return i;
369
+ }
370
+ /**
371
+ * Drop leading `VAR=value` assignments, transparent wrappers (`sudo`, `env …`)
372
+ * and the launchers that run a tool from a package environment (`npx`, `bunx`,
373
+ * `pnpm exec`, `yarn exec`, `uv run`, `poetry run`, `pipenv run`), so the
374
+ * interpreter they start is what the caller sees. `uv run a.py` runs the file
375
+ * with Python: it comes back as `python a.py`.
376
+ */
377
+ function peelPrefixes(argv) {
378
+ let i = 0;
379
+ while (i < argv.length) {
380
+ const tok = argv[i];
381
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok)) {
382
+ i++;
383
+ continue;
384
+ }
385
+ if (TRANSPARENT_PREFIXES.has(tok)) {
386
+ i++;
387
+ if (tok === 'env') {
388
+ while (i < argv.length && (argv[i].startsWith('-') || /^[A-Za-z_][A-Za-z0-9_]*=/.test(argv[i]))) {
389
+ if (argv[i] === '-u' || argv[i] === '--unset')
390
+ i++;
391
+ i++;
392
+ }
393
+ }
394
+ continue;
395
+ }
396
+ const name = canonicalName(tok);
397
+ const sub = argv[i + 1];
398
+ if (name === 'npx' || name === 'bunx' || name === 'pnpx') {
399
+ i = skipFlags(argv, i + 1, NPX_VALUE_FLAGS);
400
+ continue;
401
+ }
402
+ if ((name === 'pnpm' && (sub === 'exec' || sub === 'dlx')) || (name === 'yarn' && (sub === 'exec' || sub === 'dlx'))) {
403
+ i = skipFlags(argv, i + 2, NPX_VALUE_FLAGS);
404
+ continue;
405
+ }
406
+ if (name === 'uv' && sub === 'run') {
407
+ i = skipFlags(argv, i + 2, UV_RUN_VALUE_FLAGS);
408
+ const next = argv[i];
409
+ if (next !== undefined && extname(next).toLowerCase() === '.py')
410
+ return ['python', ...argv.slice(i)];
411
+ continue;
412
+ }
413
+ if ((name === 'poetry' || name === 'pipenv') && sub === 'run') {
414
+ i += 2;
415
+ continue;
416
+ }
417
+ break;
418
+ }
419
+ return argv.slice(i);
420
+ }
421
+ /** Locate the script-file argument of an interpreter, or report it is inline. */
422
+ function findInterpreterScript(args, spec) {
423
+ for (let i = 0; i < args.length; i++) {
424
+ const a = args[i];
425
+ if (a === '--')
426
+ return { inline: false, script: args[i + 1] ?? null };
427
+ if (spec.inlineExact.has(a))
428
+ return { inline: true, script: null };
429
+ if (spec.moduleExact?.has(a))
430
+ return { inline: true, script: null };
431
+ if (spec.inlineRegex && spec.inlineRegex.test(a))
432
+ return { inline: true, script: null };
433
+ if (spec.valueFlags.has(a)) {
434
+ i++; // this flag consumes the next token as its value
435
+ continue;
436
+ }
437
+ if (a.startsWith('-'))
438
+ continue; // some other flag
439
+ return { inline: false, script: a };
440
+ }
441
+ return { inline: false, script: null };
442
+ }
443
+ /**
444
+ * Classify a directly-executed file (`./x`) as a shell or interpreter script:
445
+ * the shebang first, then the extension; a file with neither is executed as a
446
+ * program, with nothing better to go on, so its lines are scanned as shell.
447
+ */
448
+ function classifyDirect(content, abs) {
449
+ return languageOf(abs, content) === 'shell' ? 'shell' : 'interpreter';
450
+ }
451
+ function readShellFile(abs, fsLike, maxBytes, notes) {
452
+ const read = safeRead(fsLike, abs, maxBytes);
453
+ if (!read) {
454
+ return { kind: 'shell', path: abs, language: 'shell', shellCommands: [], truncated: false, notes: [...notes, 'read-failed'] };
455
+ }
456
+ const n = [...notes];
457
+ if (read.truncated)
458
+ n.push('truncated');
459
+ return {
460
+ kind: 'shell',
461
+ path: abs,
462
+ language: 'shell',
463
+ shellCommands: splitShellScriptLines(read.content),
464
+ content: read.content,
465
+ truncated: read.truncated,
466
+ notes: n,
467
+ };
468
+ }
469
+ function readInterpreterFile(abs, fsLike, maxBytes, notes, interpreter) {
470
+ const read = safeRead(fsLike, abs, maxBytes);
471
+ if (!read) {
472
+ return {
473
+ kind: 'interpreter',
474
+ path: abs,
475
+ language: languageOf(abs, '', interpreter),
476
+ shellCommands: [],
477
+ truncated: false,
478
+ notes: [...notes, 'read-failed'],
479
+ };
480
+ }
481
+ const n = [...notes];
482
+ if (read.truncated)
483
+ n.push('truncated');
484
+ return {
485
+ kind: 'interpreter',
486
+ path: abs,
487
+ language: languageOf(abs, read.content, interpreter),
488
+ content: read.content,
489
+ shellCommands: [],
490
+ truncated: read.truncated,
491
+ notes: n,
492
+ };
493
+ }
494
+ function resolveShellInterpreter(args, cwd, fsLike, maxBytes) {
495
+ const { script } = findInterpreterScript(args, SHELL_SPEC);
496
+ if (!script)
497
+ return null; // inline (`-c`), stdin (`-s`) or no file
498
+ const abs = resolvePath(cwd, script);
499
+ if (!pathExistsAsFile(fsLike, abs))
500
+ return null;
501
+ return readShellFile(abs, fsLike, maxBytes, ['via:shell-interpreter']);
502
+ }
503
+ function resolveInterpreter(name, args, cwd, fsLike, maxBytes) {
504
+ const spec = INTERPRETERS.get(name);
505
+ if (!spec)
506
+ return null;
507
+ const { script } = findInterpreterScript(args, spec);
508
+ if (!script)
509
+ return null; // inline (`-c`/`-e`), module (`-m`) or no file
510
+ const abs = resolvePath(cwd, script);
511
+ if (!pathExistsAsFile(fsLike, abs))
512
+ return null;
513
+ return readInterpreterFile(abs, fsLike, maxBytes, [`via:interpreter:${name}`], name);
514
+ }
515
+ /**
516
+ * `deno run [flags] <file>`. Any other deno subcommand (`eval`, `test`, `fmt`,
517
+ * `task`…) names no script file to read here.
518
+ */
519
+ function resolveDeno(args, cwd, fsLike, maxBytes) {
520
+ if (args[0] !== 'run')
521
+ return null;
522
+ for (let i = 1; i < args.length; i++) {
523
+ const a = args[i];
524
+ if (DENO_VALUE_FLAGS.has(a)) {
525
+ i++;
526
+ continue;
527
+ }
528
+ if (a.startsWith('-'))
529
+ continue;
530
+ const abs = resolvePath(cwd, a);
531
+ if (!pathExistsAsFile(fsLike, abs))
532
+ return null;
533
+ return readInterpreterFile(abs, fsLike, maxBytes, ['via:interpreter:deno'], 'deno');
534
+ }
535
+ return null;
536
+ }
537
+ /**
538
+ * `bun <file>` / `bun run <file>`: a JS/TS file that exists is run as a file;
539
+ * anything else (`bun run build`, `bun install`) is left to the package-script
540
+ * path.
541
+ */
542
+ function resolveBunFile(args, cwd, fsLike, maxBytes) {
543
+ let i = 0;
544
+ while (i < args.length && args[i].startsWith('-'))
545
+ i++;
546
+ if (args[i] === 'run') {
547
+ i++;
548
+ while (i < args.length && args[i].startsWith('-'))
549
+ i++;
550
+ }
551
+ const candidate = args[i];
552
+ if (!candidate || !JS_RUNTIME_FILE_EXT.has(extname(candidate).toLowerCase()))
553
+ return null;
554
+ const abs = resolvePath(cwd, candidate);
555
+ if (!pathExistsAsFile(fsLike, abs))
556
+ return null;
557
+ return readInterpreterFile(abs, fsLike, maxBytes, ['via:interpreter:bun'], 'bun');
558
+ }
559
+ function resolveDirectExec(cmdTok, cwd, fsLike, maxBytes) {
560
+ const abs = resolvePath(cwd, cmdTok);
561
+ if (!pathExistsAsFile(fsLike, abs))
562
+ return null;
563
+ const read = safeRead(fsLike, abs, maxBytes);
564
+ const base = ['via:direct-exec'];
565
+ if (!read)
566
+ return { kind: 'shell', path: abs, shellCommands: [], truncated: false, notes: [...base, 'read-failed'] };
567
+ const notes = [...base];
568
+ if (read.truncated)
569
+ notes.push('truncated');
570
+ if (classifyDirect(read.content, abs) === 'interpreter') {
571
+ return {
572
+ kind: 'interpreter',
573
+ path: abs,
574
+ language: languageOf(abs, read.content),
575
+ content: read.content,
576
+ shellCommands: [],
577
+ truncated: read.truncated,
578
+ notes,
579
+ };
580
+ }
581
+ return {
582
+ kind: 'shell',
583
+ path: abs,
584
+ language: 'shell',
585
+ shellCommands: splitShellScriptLines(read.content),
586
+ content: read.content,
587
+ truncated: read.truncated,
588
+ notes,
589
+ };
590
+ }
591
+ /** The whole body of a script is exactly `npm|pnpm|yarn run <name>` → the inner name. */
592
+ function matchSinglePmRun(body) {
593
+ const m = /^\s*(?:npm|pnpm|yarn|bun)\s+(?:run|run-script)\s+([A-Za-z0-9:@._-]+)\s*$/.exec(body);
594
+ return m ? m[1] : null;
595
+ }
596
+ function resolveNpmScript(args, cwd, fsLike, maxBytes) {
597
+ let i = 0;
598
+ while (i < args.length && args[i].startsWith('-'))
599
+ i++;
600
+ const sub = args[i];
601
+ if (sub !== 'run' && sub !== 'run-script')
602
+ return null;
603
+ i++;
604
+ while (i < args.length && args[i].startsWith('-'))
605
+ i++;
606
+ const scriptName = args[i];
607
+ if (!scriptName)
608
+ return null;
609
+ const pkgPath = resolvePath(cwd, 'package.json');
610
+ if (!pathExistsAsFile(fsLike, pkgPath))
611
+ return null;
612
+ const read = safeRead(fsLike, pkgPath, maxBytes);
613
+ if (!read)
614
+ return null;
615
+ let pkg;
616
+ try {
617
+ pkg = JSON.parse(read.content);
618
+ }
619
+ catch {
620
+ return null;
621
+ }
622
+ if (typeof pkg !== 'object' || pkg === null)
623
+ return null;
624
+ const scriptsVal = pkg.scripts;
625
+ if (typeof scriptsVal !== 'object' || scriptsVal === null)
626
+ return null;
627
+ const scripts = scriptsVal;
628
+ const bodyVal = scripts[scriptName];
629
+ if (typeof bodyVal !== 'string')
630
+ return null;
631
+ const notes = ['via:npm-script'];
632
+ let body = bodyVal;
633
+ // Follow at most ONE level of `npm run <other>` indirection.
634
+ const inner = matchSinglePmRun(body);
635
+ if (inner && inner !== scriptName) {
636
+ const innerBodyVal = scripts[inner];
637
+ if (typeof innerBodyVal === 'string') {
638
+ notes.push(`followed npm run indirection: ${scriptName} -> ${inner}`);
639
+ body = innerBodyVal;
640
+ if (matchSinglePmRun(body))
641
+ notes.push('further npm run indirection not followed');
642
+ }
643
+ }
644
+ return { kind: 'npm-script', scriptName, shellCommands: [body], truncated: read.truncated, notes };
645
+ }
646
+ function detectInSegment(rawArgv, cwd, fsLike, maxBytes) {
647
+ const argv = peelPrefixes(rawArgv);
648
+ if (argv.length === 0)
649
+ return null;
650
+ const cmdTok = argv[0];
651
+ const rest = argv.slice(1);
652
+ const canonical = canonicalName(cmdTok);
653
+ if (SHELL_INTERPRETERS.has(canonical))
654
+ return resolveShellInterpreter(rest, cwd, fsLike, maxBytes);
655
+ if (INTERPRETERS.has(canonical))
656
+ return resolveInterpreter(canonical, rest, cwd, fsLike, maxBytes);
657
+ if (canonical === 'deno')
658
+ return resolveDeno(rest, cwd, fsLike, maxBytes);
659
+ if (canonical === 'bun') {
660
+ const file = resolveBunFile(rest, cwd, fsLike, maxBytes);
661
+ if (file)
662
+ return file;
663
+ }
664
+ if (NPM_RUNNERS.has(canonical))
665
+ return resolveNpmScript(rest, cwd, fsLike, maxBytes);
666
+ if (isPathToken(cmdTok))
667
+ return resolveDirectExec(cmdTok, cwd, fsLike, maxBytes);
668
+ return null;
669
+ }
670
+ /**
671
+ * Given a raw Bash command, return the local file/script it executes, read and
672
+ * ready to re-feed to the evaluator — or `null` when the command executes no
673
+ * local file (a plain command, an inline interpreter string, a missing file).
674
+ *
675
+ * Never throws. Reads are bounded by `opts.maxBytes` (default 256 KB).
676
+ */
677
+ export function resolveExecutedScript(command, opts) {
678
+ try {
679
+ if (typeof command !== 'string' || command.trim().length === 0)
680
+ return null;
681
+ const fsLike = opts?.fs ?? DEFAULT_FS;
682
+ const cwd = typeof opts?.cwd === 'string' ? opts.cwd : '';
683
+ const maxBytes = normalizeMaxBytes(opts?.maxBytes);
684
+ for (const argv of segmentsOf(command)) {
685
+ const r = detectInSegment(argv, cwd, fsLike, maxBytes);
686
+ if (r)
687
+ return r;
688
+ }
689
+ return null;
690
+ }
691
+ catch {
692
+ return null;
693
+ }
694
+ }
695
+ //# sourceMappingURL=script-resolve.js.map