@vibe-agent-toolkit/utils 0.1.41 → 0.1.42

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 (116) hide show
  1. package/README.md +146 -31
  2. package/dist/asset.d.ts +9 -0
  3. package/dist/asset.d.ts.map +1 -0
  4. package/dist/asset.js +9 -0
  5. package/dist/asset.js.map +1 -0
  6. package/dist/crawl.d.ts +19 -0
  7. package/dist/crawl.d.ts.map +1 -0
  8. package/dist/crawl.js +19 -0
  9. package/dist/crawl.js.map +1 -0
  10. package/dist/file-crawler.d.ts +19 -2
  11. package/dist/file-crawler.d.ts.map +1 -1
  12. package/dist/file-crawler.js +19 -1
  13. package/dist/file-crawler.js.map +1 -1
  14. package/dist/fs-utils.d.ts +60 -3
  15. package/dist/fs-utils.d.ts.map +1 -1
  16. package/dist/fs-utils.js +88 -10
  17. package/dist/fs-utils.js.map +1 -1
  18. package/dist/fs.d.ts +7 -6
  19. package/dist/fs.d.ts.map +1 -1
  20. package/dist/fs.js +7 -6
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git-utils.d.ts +2 -0
  23. package/dist/git-utils.d.ts.map +1 -1
  24. package/dist/git-utils.js +16 -0
  25. package/dist/git-utils.js.map +1 -1
  26. package/dist/git.d.ts +23 -0
  27. package/dist/git.d.ts.map +1 -0
  28. package/dist/git.js +23 -0
  29. package/dist/git.js.map +1 -0
  30. package/dist/gitignore-checker.d.ts +0 -9
  31. package/dist/gitignore-checker.d.ts.map +1 -1
  32. package/dist/gitignore-checker.js +0 -12
  33. package/dist/gitignore-checker.js.map +1 -1
  34. package/dist/glob/glob-pattern.js +1 -1
  35. package/dist/glob/glob-pattern.js.map +1 -1
  36. package/dist/glob.d.ts +8 -0
  37. package/dist/glob.d.ts.map +1 -0
  38. package/dist/glob.js +8 -0
  39. package/dist/glob.js.map +1 -0
  40. package/dist/path-core.d.ts +196 -0
  41. package/dist/path-core.d.ts.map +1 -0
  42. package/dist/path-core.js +251 -0
  43. package/dist/path-core.js.map +1 -0
  44. package/dist/path-utils.d.ts +12 -183
  45. package/dist/path-utils.d.ts.map +1 -1
  46. package/dist/path-utils.js +13 -237
  47. package/dist/path-utils.js.map +1 -1
  48. package/dist/path.d.ts +12 -0
  49. package/dist/path.d.ts.map +1 -0
  50. package/dist/path.js +12 -0
  51. package/dist/path.js.map +1 -0
  52. package/dist/process.d.ts +7 -4
  53. package/dist/process.d.ts.map +1 -1
  54. package/dist/process.js +7 -4
  55. package/dist/process.js.map +1 -1
  56. package/dist/project.d.ts +31 -0
  57. package/dist/project.d.ts.map +1 -0
  58. package/dist/project.js +31 -0
  59. package/dist/project.js.map +1 -0
  60. package/dist/safe-exec.d.ts.map +1 -1
  61. package/dist/safe-exec.js +8 -4
  62. package/dist/safe-exec.js.map +1 -1
  63. package/dist/spawn-hardened.d.ts.map +1 -1
  64. package/dist/spawn-hardened.js +4 -17
  65. package/dist/spawn-hardened.js.map +1 -1
  66. package/dist/template-entry.d.ts +10 -0
  67. package/dist/template-entry.d.ts.map +1 -0
  68. package/dist/template-entry.js +10 -0
  69. package/dist/template-entry.js.map +1 -0
  70. package/dist/testing.d.ts +8 -0
  71. package/dist/testing.d.ts.map +1 -0
  72. package/dist/testing.js +8 -0
  73. package/dist/testing.js.map +1 -0
  74. package/dist/windows-shell.d.ts +140 -8
  75. package/dist/windows-shell.d.ts.map +1 -1
  76. package/dist/windows-shell.js +218 -11
  77. package/dist/windows-shell.js.map +1 -1
  78. package/dist/yaml.d.ts +9 -0
  79. package/dist/yaml.d.ts.map +1 -0
  80. package/dist/yaml.js +9 -0
  81. package/dist/yaml.js.map +1 -0
  82. package/dist/zod.d.ts +10 -0
  83. package/dist/zod.d.ts.map +1 -0
  84. package/dist/zod.js +10 -0
  85. package/dist/zod.js.map +1 -0
  86. package/eslint/README.md +217 -0
  87. package/eslint/index.cjs +161 -0
  88. package/eslint/index.d.cts +60 -0
  89. package/eslint/rules/dead-import.cjs +201 -0
  90. package/eslint/rules/eslint-rule-factory.cjs +410 -0
  91. package/eslint/rules/exempt-path-matcher.cjs +265 -0
  92. package/eslint/rules/no-bare-dynamic-import-path.cjs +142 -0
  93. package/eslint/rules/no-child-process-execSync.cjs +23 -0
  94. package/eslint/rules/no-command-direct-factory.cjs +246 -0
  95. package/eslint/rules/no-file-url-string-concat.cjs +77 -0
  96. package/eslint/rules/no-fs-mkdirSync.cjs +23 -0
  97. package/eslint/rules/no-fs-promises-cp.cjs +36 -0
  98. package/eslint/rules/no-fs-realpathSync.cjs +23 -0
  99. package/eslint/rules/no-hardcoded-path-split.cjs +141 -0
  100. package/eslint/rules/no-manual-path-normalize.cjs +161 -0
  101. package/eslint/rules/no-os-tmpdir.cjs +24 -0
  102. package/eslint/rules/no-path-join.cjs +14 -0
  103. package/eslint/rules/no-path-operations-in-comparisons.cjs +148 -0
  104. package/eslint/rules/no-path-relative.cjs +14 -0
  105. package/eslint/rules/no-path-resolve.cjs +14 -0
  106. package/eslint/rules/no-path-sep-in-strings.cjs +130 -0
  107. package/eslint/rules/no-path-startswith.cjs +139 -0
  108. package/eslint/rules/no-test-scoped-functions.cjs +134 -0
  109. package/eslint/rules/no-unix-shell-commands.cjs +152 -0
  110. package/eslint/rules/no-unsafe-root-join.cjs +85 -0
  111. package/eslint/rules/no-url-pathname-for-fs.cjs +107 -0
  112. package/eslint/rules/path-function-rule-factory.cjs +417 -0
  113. package/eslint/rules/prefer-startswith-over-regex.cjs +257 -0
  114. package/eslint/rules/require-justified-skip.cjs +348 -0
  115. package/eslint/rules/safe-import.cjs +164 -0
  116. package/package.json +65 -7
@@ -10,6 +10,15 @@
10
10
  * npm-installed CLIs (`claude`, `npm`, `git` shims, …) resolve to exactly such a
11
11
  * wrapper on Windows, so any spawn that omits this handling crashes on Windows only.
12
12
  */
13
+ /**
14
+ * A `command` is treated as an explicit path (used verbatim, never PATH-resolved)
15
+ * when it is absolute or contains a path separator. A bare name (`claude`, `npm`,
16
+ * `git`) is resolved on PATH via `which.sync`. This mirrors — and preserves — the
17
+ * behaviour callers relied on before hardening: an explicit `binPath` is spawned
18
+ * as-given (so a nonexistent path still surfaces as an async `'error'` event, not a
19
+ * synchronous `which` throw), while a bare command is looked up.
20
+ */
21
+ export declare function isPathLike(command: string): boolean;
13
22
  /**
14
23
  * Determine if shell should be used for command execution on Windows.
15
24
  *
@@ -32,13 +41,28 @@
32
41
  * **Root Cause of Previous ENOENT Issues:** Likely resolved in newer Node.js versions.
33
42
  * Current testing (Node 20+) shows shell:false works correctly with absolute path.
34
43
  *
35
- * ## Why This Is Secure
44
+ * ## Why This Is Secure — and where it is only *quoted*, not secure
45
+ *
46
+ * There are two modes, and they have genuinely different properties. Describing them as
47
+ * one ("arguments are always an array", "we never interpolate") would be false for this
48
+ * module's own Windows branch, which does exactly that interpolation.
36
49
  *
37
- * 1. **Minimal Shell Usage:** Shell only used for .cmd/.bat/.ps1 files (required)
38
- * 2. **Path Validation:** Command paths resolved via `which.sync()` before execution
39
- * 3. **Array-Based Arguments:** Arguments passed as array, preventing injection
40
- * 4. **Controlled Environment:** Commands from trusted configuration, not user input
41
- * 5. **No String Interpolation:** Never concatenate user input into command strings
50
+ * 1. **Minimal Shell Usage:** the shell is used ONLY when this function returns true —
51
+ * `.cmd` / `.bat` / `.ps1`, where a shell interpreter is required by design.
52
+ * 2. **Path Validation:** command paths are resolved via `which.sync()` before execution.
53
+ * 3. **Two modes:**
54
+ * - **`shell: false` (every other command the default):** arguments are handed to
55
+ * `spawn`/`spawnSync` as a real argv array. Nothing re-parses them, so there is no
56
+ * metacharacter surface at all. This is the mode that is *secure*.
57
+ * - **`shell: true` (this function returning true):** Node's DEP0190 forbids an args
58
+ * array in shell mode, so {@link buildWindowsShellLine} MUST concatenate the command
59
+ * and its arguments into a single string. Safety there rests entirely on
60
+ * {@link windowsShellQuote}, which *quotes* — it does not neutralize. `%VAR%` still
61
+ * expands inside double quotes, and an argument containing a `"` unavoidably desyncs
62
+ * `cmd.exe`'s quote tracking (that function documents the two-parser trade in full).
63
+ * 4. **Controlled Environment:** commands and arguments come from trusted configuration.
64
+ * On the `shell: true` branch that is a *requirement*, not a nicety — do not route
65
+ * untrusted input through it; spawn with `shell: false` and an args array instead.
42
66
  *
43
67
  * ## References
44
68
  *
@@ -53,10 +77,86 @@ export declare function shouldUseShell(commandPath: string): boolean;
53
77
  /**
54
78
  * Quote a single argument for a Windows `cmd.exe` command line.
55
79
  *
56
- * Rules:
80
+ * Rules (the canonical `CommandLineToArgvW` algorithm — the same one used by .NET's
81
+ * `PasteArguments`, Python's `subprocess.list2cmdline`, and Rust's `Command`):
57
82
  * - An arg that's empty, or contains whitespace, quotes, or any of `& | < > ^ ( ) % !`,
58
83
  * gets wrapped in double quotes.
59
- * - Embedded double quotes become `""` (cmd.exe's escape form inside quoted strings).
84
+ * - Every run of backslashes that immediately precedes a `"` an embedded one OR the
85
+ * closing one — is doubled.
86
+ * - Embedded double quotes are escaped as `\"`.
87
+ *
88
+ * ## Why backslash runs must be doubled before EVERY quote, not just the last one
89
+ *
90
+ * `CommandLineToArgvW` — what the launched program uses to split the line back into argv —
91
+ * reads a backslash run only in relation to the character that follows it: `2n`
92
+ * backslashes before a `"` mean `n` literal backslashes plus a *delimiter* quote; `2n+1`
93
+ * mean `n` literal backslashes plus a *literal* quote; a run followed by anything else is
94
+ * literal. So a backslash sitting in front of a quote silently changes what that quote
95
+ * means, and the parse desynchronizes from there — the argument swallows the rest of the
96
+ * line, or a backslash vanishes.
97
+ *
98
+ * The everyday cases are not exotic: `C:\Program Files\` (a directory path with a trailing
99
+ * separator and a space) and `{"dir":"C:\\tools\\"}` (JSON carrying a Windows path). A
100
+ * previous version of this function handled only the *trailing* run and escaped quotes as
101
+ * `""`. Replaying it through the reference parser in `test/windows-shell.test.ts` over every
102
+ * string in `{a, \, ", space, %}` of length ≤ 4 corrupted 85 of 781 inputs, 74 of which
103
+ * swallowed the following argument; every failure contained a backslash-quote sequence. That
104
+ * harness now runs on every commit, and asserts 0 of 781.
105
+ *
106
+ * ## TWO PARSERS IN SERIES: why `\"` and not `""` (the deliberate trade)
107
+ *
108
+ * The string this function emits is consumed by two different parsers, in order:
109
+ *
110
+ * 1. **`cmd.exe`**, which has NO backslash escape at all. It toggles a quote flag on
111
+ * every `"` it sees, and only text inside that flag is protected from `& | < > ^ ( )`.
112
+ * 2. **`CommandLineToArgvW`** (or the CRT's equivalent) in the process the `.cmd`/`.bat`
113
+ * shim ultimately launches. The dominant real case is an npm shim forwarding `%*` to
114
+ * a real `.exe`, and `%*` is the *verbatim* remaining text — so whatever we emit here
115
+ * is exactly what that second parser sees.
116
+ *
117
+ * These two want opposite things, and no single byte sequence satisfies both:
118
+ *
119
+ * - `""` keeps `cmd.exe`'s quote count even, so metacharacters stay protected — but its
120
+ * meaning to the second parser is version- and implementation-dependent. It is absent
121
+ * from `CommandLineToArgvW`'s documented ruleset (which specifies only the backslash
122
+ * rules), and under the widely-mirrored `CommandLineToArgvW` implementation — where a
123
+ * run of three quotes yields one literal quote and *resets* the quote state — the token
124
+ * `"a""b"` leaves the parser back inside quotes, so the following space stops
125
+ * separating arguments and the next argument is swallowed.
126
+ * - `\"` is understood identically by every implementation there is (CommandLineToArgvW,
127
+ * old and new CRT, .NET, Python, Rust, Wine). But `cmd.exe` does not honour the
128
+ * backslash, so each embedded quote flips its quote flag: within an argument that
129
+ * contains a `"`, the span following that quote is *outside* cmd's quotes and a
130
+ * metacharacter there would be re-interpreted.
131
+ *
132
+ * **The trade made here: `\"`.** Delivering the argument to the child process byte-exact is
133
+ * the job of this function, and `\"` is the only form with no parser-variant risk. The cost
134
+ * is bounded and stated plainly: for an argument containing BOTH a `"` and one of
135
+ * `& | < > ^ ( )`, `cmd.exe` may act on that metacharacter. Arguments with no embedded
136
+ * quote — which is every path, and the trailing-backslash case that motivated all of this —
137
+ * emit exactly two quotes and keep cmd's tracking balanced, so they are unaffected.
138
+ *
139
+ * (Rust makes the opposite trade in `append_bat_arg`, emitting `""`. Its target is a batch
140
+ * file reading `%~1` *itself*, where cmd is the only parser and injection is the whole
141
+ * threat model. Ours is a shim forwarding `%*` to an `.exe`, where the second parser is the
142
+ * one that decides what the program actually receives. Same two parsers, different consumer,
143
+ * different answer.)
144
+ *
145
+ * ## These characters are QUOTED, NOT NEUTRALIZED
146
+ *
147
+ * Wrapping in double quotes stops `cmd.exe` from re-interpreting whitespace and the
148
+ * redirection/pipe/grouping metacharacters — but it does **not** stop variable
149
+ * expansion. `cmd.exe` expands `%VAR%` *inside* double quotes, so:
150
+ *
151
+ * - an arg containing `%PATH%` is still substituted before the program sees it, and
152
+ * - a literal `%` in a filename (legal on Windows) can be corrupted by that pass.
153
+ *
154
+ * `%` is in the trigger class only so such args get quoted at all; treat the value as
155
+ * still shell-visible. `!` has the same shape under delayed expansion (`!VAR!`) — that
156
+ * is off by default for `cmd /c`, so it is a caveat rather than a live bug, but a
157
+ * caller that enables `ENABLEDELAYEDEXPANSION` inherits the problem. If an argument
158
+ * must survive verbatim, do not route it through a shell at all: spawn with
159
+ * `shell: false` and a real args array (see {@link shouldUseShell}).
60
160
  *
61
161
  * This is narrow on purpose: it's only used when we're forced to assemble a shell string
62
162
  * for `.cmd` / `.bat` wrappers on Windows (DEP0190 path). Callers still control what's
@@ -64,12 +164,44 @@ export declare function shouldUseShell(commandPath: string): boolean;
64
164
  * re-interpreted by the shell.
65
165
  */
66
166
  export declare function windowsShellQuote(arg: string): string;
167
+ /**
168
+ * Choose the `commandToken` to hand {@link buildWindowsShellLine} on the Windows shell
169
+ * path. This is the ONE definition of that choice — both spawn wrappers
170
+ * ({@link ./safe-exec.ts} and {@link ./spawn-hardened.ts}) call it, so the two paths are
171
+ * provably identical rather than merely similar. They previously disagreed: the sync path
172
+ * emitted a raw, unquoted command, which silently broke on any explicit path containing a
173
+ * space (and now throws, thanks to the assertion in `buildWindowsShellLine`).
174
+ *
175
+ * Two cases, and they need opposite treatment:
176
+ *
177
+ * - **Bare PATH name** (`claude`, `npm`) — emitted verbatim, NOT quoted and NOT replaced
178
+ * by the resolved path, because `cmd.exe` must be left to re-resolve it through
179
+ * `PATHEXT`. A bare name has no whitespace, so it never trips the assertion.
180
+ * - **Explicit path** (absolute, or containing a separator) — the RESOLVED path, run
181
+ * through {@link windowsShellQuote} so a directory with a space (`C:\Program Files\…`)
182
+ * stays one token, and a trailing backslash cannot escape the closing quote and
183
+ * swallow the next argument.
184
+ *
185
+ * @param command - The command as the caller supplied it (decides bare vs. explicit).
186
+ * @param resolvedPath - The path actually being launched (what gets quoted, when quoted).
187
+ */
188
+ export declare function resolveShellCommandToken(command: string, resolvedPath: string): string;
67
189
  /**
68
190
  * Build the single `cmd.exe` command line used when a Windows shim forces shell mode
69
191
  * (Node's DEP0190 rejects `shell: true` with a separate args array, so command + args
70
192
  * must be one string). `commandToken` is the already-resolved command — a bare PATH
71
193
  * name that `cmd.exe` re-resolves via PATHEXT, or a pre-quoted explicit path — and each
72
194
  * arg is quoted via {@link windowsShellQuote} so shell metacharacters can't re-interpret.
195
+ *
196
+ * `commandToken` is emitted verbatim (quoting it here would defeat the PATHEXT lookup a
197
+ * bare name relies on), so quoting it is the caller's job — see
198
+ * {@link resolveShellCommandToken}, which is how every caller should produce it. An
199
+ * unquoted path containing a space — `C:\Program Files\tool.cmd` — would otherwise produce
200
+ * a silently broken line where `cmd.exe` tries to run `C:\Program` with `Files\tool.cmd` as
201
+ * its first argument. Those cases throw rather than emitting the broken line.
202
+ *
203
+ * @throws {Error} if `commandToken` is not a single `cmd.exe` token (see
204
+ * {@link isSingleShellToken}). Pass it through {@link windowsShellQuote} first.
73
205
  */
74
206
  export declare function buildWindowsShellLine(commandToken: string, args: string[]): string;
75
207
  //# sourceMappingURL=windows-shell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"windows-shell.d.ts","sourceRoot":"","sources":["../src/windows-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAiB3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKrD;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAElF"}
1
+ {"version":3,"file":"windows-shell.d.ts","sourceRoot":"","sources":["../src/windows-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAiB3D;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2BrD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtF;AAsCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAWlF"}
@@ -10,6 +10,18 @@
10
10
  * npm-installed CLIs (`claude`, `npm`, `git` shims, …) resolve to exactly such a
11
11
  * wrapper on Windows, so any spawn that omits this handling crashes on Windows only.
12
12
  */
13
+ import { isAbsoluteAnyPlatform } from './path-core.js';
14
+ /**
15
+ * A `command` is treated as an explicit path (used verbatim, never PATH-resolved)
16
+ * when it is absolute or contains a path separator. A bare name (`claude`, `npm`,
17
+ * `git`) is resolved on PATH via `which.sync`. This mirrors — and preserves — the
18
+ * behaviour callers relied on before hardening: an explicit `binPath` is spawned
19
+ * as-given (so a nonexistent path still surfaces as an async `'error'` event, not a
20
+ * synchronous `which` throw), while a bare command is looked up.
21
+ */
22
+ export function isPathLike(command) {
23
+ return isAbsoluteAnyPlatform(command) || command.includes('/') || command.includes('\\');
24
+ }
13
25
  /**
14
26
  * Determine if shell should be used for command execution on Windows.
15
27
  *
@@ -32,13 +44,28 @@
32
44
  * **Root Cause of Previous ENOENT Issues:** Likely resolved in newer Node.js versions.
33
45
  * Current testing (Node 20+) shows shell:false works correctly with absolute path.
34
46
  *
35
- * ## Why This Is Secure
47
+ * ## Why This Is Secure — and where it is only *quoted*, not secure
36
48
  *
37
- * 1. **Minimal Shell Usage:** Shell only used for .cmd/.bat/.ps1 files (required)
38
- * 2. **Path Validation:** Command paths resolved via `which.sync()` before execution
39
- * 3. **Array-Based Arguments:** Arguments passed as array, preventing injection
40
- * 4. **Controlled Environment:** Commands from trusted configuration, not user input
41
- * 5. **No String Interpolation:** Never concatenate user input into command strings
49
+ * There are two modes, and they have genuinely different properties. Describing them as
50
+ * one ("arguments are always an array", "we never interpolate") would be false for this
51
+ * module's own Windows branch, which does exactly that interpolation.
52
+ *
53
+ * 1. **Minimal Shell Usage:** the shell is used ONLY when this function returns true —
54
+ * `.cmd` / `.bat` / `.ps1`, where a shell interpreter is required by design.
55
+ * 2. **Path Validation:** command paths are resolved via `which.sync()` before execution.
56
+ * 3. **Two modes:**
57
+ * - **`shell: false` (every other command — the default):** arguments are handed to
58
+ * `spawn`/`spawnSync` as a real argv array. Nothing re-parses them, so there is no
59
+ * metacharacter surface at all. This is the mode that is *secure*.
60
+ * - **`shell: true` (this function returning true):** Node's DEP0190 forbids an args
61
+ * array in shell mode, so {@link buildWindowsShellLine} MUST concatenate the command
62
+ * and its arguments into a single string. Safety there rests entirely on
63
+ * {@link windowsShellQuote}, which *quotes* — it does not neutralize. `%VAR%` still
64
+ * expands inside double quotes, and an argument containing a `"` unavoidably desyncs
65
+ * `cmd.exe`'s quote tracking (that function documents the two-parser trade in full).
66
+ * 4. **Controlled Environment:** commands and arguments come from trusted configuration.
67
+ * On the `shell: true` branch that is a *requirement*, not a nicety — do not route
68
+ * untrusted input through it; spawn with `shell: false` and an args array instead.
42
69
  *
43
70
  * ## References
44
71
  *
@@ -65,13 +92,96 @@ export function shouldUseShell(commandPath) {
65
92
  const lowerPath = commandPath.toLowerCase();
66
93
  return lowerPath.endsWith('.cmd') || lowerPath.endsWith('.bat') || lowerPath.endsWith('.ps1');
67
94
  }
95
+ /**
96
+ * Characters that force an argument to be wrapped in double quotes: whitespace, a quote,
97
+ * and `cmd.exe`'s metacharacters. Also the predicate {@link buildWindowsShellLine} uses to
98
+ * decide whether an *unquoted* command token is safe — "a token {@link windowsShellQuote}
99
+ * would have left alone" is exactly the token cmd.exe cannot split or re-interpret.
100
+ */
101
+ const SHELL_QUOTE_TRIGGER = /["\s&|<>^()%!]/;
68
102
  /**
69
103
  * Quote a single argument for a Windows `cmd.exe` command line.
70
104
  *
71
- * Rules:
105
+ * Rules (the canonical `CommandLineToArgvW` algorithm — the same one used by .NET's
106
+ * `PasteArguments`, Python's `subprocess.list2cmdline`, and Rust's `Command`):
72
107
  * - An arg that's empty, or contains whitespace, quotes, or any of `& | < > ^ ( ) % !`,
73
108
  * gets wrapped in double quotes.
74
- * - Embedded double quotes become `""` (cmd.exe's escape form inside quoted strings).
109
+ * - Every run of backslashes that immediately precedes a `"` an embedded one OR the
110
+ * closing one — is doubled.
111
+ * - Embedded double quotes are escaped as `\"`.
112
+ *
113
+ * ## Why backslash runs must be doubled before EVERY quote, not just the last one
114
+ *
115
+ * `CommandLineToArgvW` — what the launched program uses to split the line back into argv —
116
+ * reads a backslash run only in relation to the character that follows it: `2n`
117
+ * backslashes before a `"` mean `n` literal backslashes plus a *delimiter* quote; `2n+1`
118
+ * mean `n` literal backslashes plus a *literal* quote; a run followed by anything else is
119
+ * literal. So a backslash sitting in front of a quote silently changes what that quote
120
+ * means, and the parse desynchronizes from there — the argument swallows the rest of the
121
+ * line, or a backslash vanishes.
122
+ *
123
+ * The everyday cases are not exotic: `C:\Program Files\` (a directory path with a trailing
124
+ * separator and a space) and `{"dir":"C:\\tools\\"}` (JSON carrying a Windows path). A
125
+ * previous version of this function handled only the *trailing* run and escaped quotes as
126
+ * `""`. Replaying it through the reference parser in `test/windows-shell.test.ts` over every
127
+ * string in `{a, \, ", space, %}` of length ≤ 4 corrupted 85 of 781 inputs, 74 of which
128
+ * swallowed the following argument; every failure contained a backslash-quote sequence. That
129
+ * harness now runs on every commit, and asserts 0 of 781.
130
+ *
131
+ * ## TWO PARSERS IN SERIES: why `\"` and not `""` (the deliberate trade)
132
+ *
133
+ * The string this function emits is consumed by two different parsers, in order:
134
+ *
135
+ * 1. **`cmd.exe`**, which has NO backslash escape at all. It toggles a quote flag on
136
+ * every `"` it sees, and only text inside that flag is protected from `& | < > ^ ( )`.
137
+ * 2. **`CommandLineToArgvW`** (or the CRT's equivalent) in the process the `.cmd`/`.bat`
138
+ * shim ultimately launches. The dominant real case is an npm shim forwarding `%*` to
139
+ * a real `.exe`, and `%*` is the *verbatim* remaining text — so whatever we emit here
140
+ * is exactly what that second parser sees.
141
+ *
142
+ * These two want opposite things, and no single byte sequence satisfies both:
143
+ *
144
+ * - `""` keeps `cmd.exe`'s quote count even, so metacharacters stay protected — but its
145
+ * meaning to the second parser is version- and implementation-dependent. It is absent
146
+ * from `CommandLineToArgvW`'s documented ruleset (which specifies only the backslash
147
+ * rules), and under the widely-mirrored `CommandLineToArgvW` implementation — where a
148
+ * run of three quotes yields one literal quote and *resets* the quote state — the token
149
+ * `"a""b"` leaves the parser back inside quotes, so the following space stops
150
+ * separating arguments and the next argument is swallowed.
151
+ * - `\"` is understood identically by every implementation there is (CommandLineToArgvW,
152
+ * old and new CRT, .NET, Python, Rust, Wine). But `cmd.exe` does not honour the
153
+ * backslash, so each embedded quote flips its quote flag: within an argument that
154
+ * contains a `"`, the span following that quote is *outside* cmd's quotes and a
155
+ * metacharacter there would be re-interpreted.
156
+ *
157
+ * **The trade made here: `\"`.** Delivering the argument to the child process byte-exact is
158
+ * the job of this function, and `\"` is the only form with no parser-variant risk. The cost
159
+ * is bounded and stated plainly: for an argument containing BOTH a `"` and one of
160
+ * `& | < > ^ ( )`, `cmd.exe` may act on that metacharacter. Arguments with no embedded
161
+ * quote — which is every path, and the trailing-backslash case that motivated all of this —
162
+ * emit exactly two quotes and keep cmd's tracking balanced, so they are unaffected.
163
+ *
164
+ * (Rust makes the opposite trade in `append_bat_arg`, emitting `""`. Its target is a batch
165
+ * file reading `%~1` *itself*, where cmd is the only parser and injection is the whole
166
+ * threat model. Ours is a shim forwarding `%*` to an `.exe`, where the second parser is the
167
+ * one that decides what the program actually receives. Same two parsers, different consumer,
168
+ * different answer.)
169
+ *
170
+ * ## These characters are QUOTED, NOT NEUTRALIZED
171
+ *
172
+ * Wrapping in double quotes stops `cmd.exe` from re-interpreting whitespace and the
173
+ * redirection/pipe/grouping metacharacters — but it does **not** stop variable
174
+ * expansion. `cmd.exe` expands `%VAR%` *inside* double quotes, so:
175
+ *
176
+ * - an arg containing `%PATH%` is still substituted before the program sees it, and
177
+ * - a literal `%` in a filename (legal on Windows) can be corrupted by that pass.
178
+ *
179
+ * `%` is in the trigger class only so such args get quoted at all; treat the value as
180
+ * still shell-visible. `!` has the same shape under delayed expansion (`!VAR!`) — that
181
+ * is off by default for `cmd /c`, so it is a caveat rather than a live bug, but a
182
+ * caller that enables `ENABLEDELAYEDEXPANSION` inherits the problem. If an argument
183
+ * must survive verbatim, do not route it through a shell at all: spawn with
184
+ * `shell: false` and a real args array (see {@link shouldUseShell}).
75
185
  *
76
186
  * This is narrow on purpose: it's only used when we're forced to assemble a shell string
77
187
  * for `.cmd` / `.bat` wrappers on Windows (DEP0190 path). Callers still control what's
@@ -79,10 +189,90 @@ export function shouldUseShell(commandPath) {
79
189
  * re-interpreted by the shell.
80
190
  */
81
191
  export function windowsShellQuote(arg) {
82
- if (arg === '' || /["\s&|<>^()%!]/.test(arg)) {
83
- return `"${arg.replaceAll('"', '""')}"`;
192
+ if (arg !== '' && !SHELL_QUOTE_TRIGGER.test(arg)) {
193
+ return arg;
84
194
  }
85
- return arg;
195
+ // Walk the argument, holding each backslash run until we see what follows it: a run
196
+ // before a quote (embedded, or the closing one) is doubled so the quote keeps its own
197
+ // meaning; a run before anything else is literal and emitted as-is.
198
+ let quoted = '"';
199
+ let backslashes = 0;
200
+ for (const char of arg) {
201
+ if (char === '\\') {
202
+ backslashes += 1;
203
+ continue;
204
+ }
205
+ if (char === '"') {
206
+ // 2n backslashes (now literal) + one more to escape the quote => 2n+1.
207
+ quoted += '\\'.repeat(backslashes * 2 + 1) + '"';
208
+ backslashes = 0;
209
+ continue;
210
+ }
211
+ quoted += '\\'.repeat(backslashes) + char;
212
+ backslashes = 0;
213
+ }
214
+ // Trailing run: doubled, so the closing quote terminates the argument instead of being
215
+ // escaped by it (`"C:\Program Files\"` would otherwise swallow the next argument).
216
+ return `${quoted}${'\\'.repeat(backslashes * 2)}"`;
217
+ }
218
+ /**
219
+ * Choose the `commandToken` to hand {@link buildWindowsShellLine} on the Windows shell
220
+ * path. This is the ONE definition of that choice — both spawn wrappers
221
+ * ({@link ./safe-exec.ts} and {@link ./spawn-hardened.ts}) call it, so the two paths are
222
+ * provably identical rather than merely similar. They previously disagreed: the sync path
223
+ * emitted a raw, unquoted command, which silently broke on any explicit path containing a
224
+ * space (and now throws, thanks to the assertion in `buildWindowsShellLine`).
225
+ *
226
+ * Two cases, and they need opposite treatment:
227
+ *
228
+ * - **Bare PATH name** (`claude`, `npm`) — emitted verbatim, NOT quoted and NOT replaced
229
+ * by the resolved path, because `cmd.exe` must be left to re-resolve it through
230
+ * `PATHEXT`. A bare name has no whitespace, so it never trips the assertion.
231
+ * - **Explicit path** (absolute, or containing a separator) — the RESOLVED path, run
232
+ * through {@link windowsShellQuote} so a directory with a space (`C:\Program Files\…`)
233
+ * stays one token, and a trailing backslash cannot escape the closing quote and
234
+ * swallow the next argument.
235
+ *
236
+ * @param command - The command as the caller supplied it (decides bare vs. explicit).
237
+ * @param resolvedPath - The path actually being launched (what gets quoted, when quoted).
238
+ */
239
+ export function resolveShellCommandToken(command, resolvedPath) {
240
+ return isPathLike(command) ? windowsShellQuote(resolvedPath) : command;
241
+ }
242
+ /**
243
+ * Is `token` a single `cmd.exe` token — one thing cmd will treat as the command, with no
244
+ * way for it to split or re-interpret the rest?
245
+ *
246
+ * Two admissible shapes, and nothing else:
247
+ *
248
+ * - **Unquoted:** a token {@link windowsShellQuote} would have left alone — no
249
+ * whitespace, no quote, no metacharacter. A bare PATH name (`claude`, `npm`) or a
250
+ * space-free path.
251
+ * - **Quoted:** exactly ONE balanced outer pair with no further quote inside it, e.g.
252
+ * `"C:\Program Files\tool.cmd"`. Stripping that pair must leave a body cmd cannot
253
+ * escape from.
254
+ *
255
+ * The interior-quote check is the whole point. The previous predicate was "starts with `"`
256
+ * and ends with `"`", which is true of an entire crafted command line —
257
+ * `"a b" && calc "x"` — so a caller could hand over something that runs `calc` and this
258
+ * function would wave it through as "already quoted".
259
+ *
260
+ * The empty string is neither shape, and needs saying because it slipped between both
261
+ * branches: `''` does not start with `"`, and `SHELL_QUOTE_TRIGGER` cannot match a string
262
+ * with no characters, so the metacharacter branch returned true. `buildWindowsShellLine('',
263
+ * ['calc', 'b'])` was accepted and emitted `" calc b"` — a line whose first cmd.exe token
264
+ * is `calc`, i.e. the caller's first *argument* silently promoted into the command
265
+ * position. The explicit length check is what makes "one token" mean *one*, not *at most
266
+ * one*. (Whitespace-only tokens were already rejected — `\s` is in the trigger class.)
267
+ */
268
+ function isSingleShellToken(token) {
269
+ if (token.length === 0) {
270
+ return false;
271
+ }
272
+ if (token.startsWith('"')) {
273
+ return token.length >= 2 && token.endsWith('"') && !token.slice(1, -1).includes('"');
274
+ }
275
+ return !SHELL_QUOTE_TRIGGER.test(token);
86
276
  }
87
277
  /**
88
278
  * Build the single `cmd.exe` command line used when a Windows shim forces shell mode
@@ -90,8 +280,25 @@ export function windowsShellQuote(arg) {
90
280
  * must be one string). `commandToken` is the already-resolved command — a bare PATH
91
281
  * name that `cmd.exe` re-resolves via PATHEXT, or a pre-quoted explicit path — and each
92
282
  * arg is quoted via {@link windowsShellQuote} so shell metacharacters can't re-interpret.
283
+ *
284
+ * `commandToken` is emitted verbatim (quoting it here would defeat the PATHEXT lookup a
285
+ * bare name relies on), so quoting it is the caller's job — see
286
+ * {@link resolveShellCommandToken}, which is how every caller should produce it. An
287
+ * unquoted path containing a space — `C:\Program Files\tool.cmd` — would otherwise produce
288
+ * a silently broken line where `cmd.exe` tries to run `C:\Program` with `Files\tool.cmd` as
289
+ * its first argument. Those cases throw rather than emitting the broken line.
290
+ *
291
+ * @throws {Error} if `commandToken` is not a single `cmd.exe` token (see
292
+ * {@link isSingleShellToken}). Pass it through {@link windowsShellQuote} first.
93
293
  */
94
294
  export function buildWindowsShellLine(commandToken, args) {
295
+ if (!isSingleShellToken(commandToken)) {
296
+ throw new Error(`buildWindowsShellLine: commandToken is not a single cmd.exe token: ${commandToken}. ` +
297
+ 'It must be either a bare name with no whitespace/quote/metacharacter, or one ' +
298
+ 'balanced double-quoted token with no quote inside it. Wrap it with ' +
299
+ 'windowsShellQuote() first — cmd.exe would otherwise split it or run part of it ' +
300
+ 'as a separate command.');
301
+ }
95
302
  return `${commandToken} ${args.map(windowsShellQuote).join(' ')}`;
96
303
  }
97
304
  //# sourceMappingURL=windows-shell.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"windows-shell.js","sourceRoot":"","sources":["../src/windows-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,UAAU,cAAc,CAAC,WAAmB;IAChD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,yFAAyF;IACzF,4FAA4F;IAC5F,yDAAyD;IACzD,2EAA2E;IAC3E,EAAE;IACF,gDAAgD;IAChD,wEAAwE;IACxE,4DAA4D;IAE5D,yEAAyE;IACzE,MAAM,SAAS,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IAC5C,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChG,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,GAAG,KAAK,EAAE,IAAI,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,OAAO,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;IAC1C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,YAAoB,EAAE,IAAc;IACxE,OAAO,GAAG,YAAY,IAAI,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACpE,CAAC"}
1
+ {"version":3,"file":"windows-shell.js","sourceRoot":"","sources":["../src/windows-shell.ts"],"names":[],"mappings":"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"}
package/dist/yaml.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @vibe-agent-toolkit/utils/yaml
3
+ *
4
+ * Byte-surgical YAML value updates that replace or insert without reflowing
5
+ * the surrounding document. Pure with respect to Node builtins, but it pulls
6
+ * in `yaml` — roughly 271KB bundled. Quarantined behind its own subpath.
7
+ */
8
+ export * from './yaml/surgical-yaml.js';
9
+ //# sourceMappingURL=yaml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,yBAAyB,CAAC"}
package/dist/yaml.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @vibe-agent-toolkit/utils/yaml
3
+ *
4
+ * Byte-surgical YAML value updates that replace or insert without reflowing
5
+ * the surrounding document. Pure with respect to Node builtins, but it pulls
6
+ * in `yaml` — roughly 271KB bundled. Quarantined behind its own subpath.
7
+ */
8
+ export * from './yaml/surgical-yaml.js';
9
+ //# sourceMappingURL=yaml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yaml.js","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,yBAAyB,CAAC"}
package/dist/zod.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @vibe-agent-toolkit/utils/zod
3
+ *
4
+ * Version-agnostic Zod type introspection via duck typing on `_def.typeName`,
5
+ * so it works across Zod v3 and v4 even when library and consumer versions
6
+ * differ. Pure — reaches no Node builtin. Isolating it here means consumers
7
+ * who never touch Zod are not coupled to the `zod` peer range.
8
+ */
9
+ export * from './zod-introspection.js';
10
+ //# sourceMappingURL=zod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod.d.ts","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,wBAAwB,CAAC"}
package/dist/zod.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @vibe-agent-toolkit/utils/zod
3
+ *
4
+ * Version-agnostic Zod type introspection via duck typing on `_def.typeName`,
5
+ * so it works across Zod v3 and v4 even when library and consumer versions
6
+ * differ. Pure — reaches no Node builtin. Isolating it here means consumers
7
+ * who never touch Zod are not coupled to the `zod` peer range.
8
+ */
9
+ export * from './zod-introspection.js';
10
+ //# sourceMappingURL=zod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod.js","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,wBAAwB,CAAC"}