@cortexkit/aft-bridge 0.39.4 → 0.40.1

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 (49) hide show
  1. package/dist/bash-format.d.ts +0 -1
  2. package/dist/bash-format.d.ts.map +1 -1
  3. package/dist/bash-format.js +0 -3
  4. package/dist/bash-format.js.map +1 -1
  5. package/dist/bash-hints.js +97 -9
  6. package/dist/bash-hints.js.map +1 -1
  7. package/dist/bridge.d.ts +7 -1
  8. package/dist/bridge.d.ts.map +1 -1
  9. package/dist/bridge.js +39 -42
  10. package/dist/bridge.js.map +1 -1
  11. package/dist/coerce.d.ts +39 -0
  12. package/dist/coerce.d.ts.map +1 -1
  13. package/dist/coerce.js +82 -0
  14. package/dist/coerce.js.map +1 -1
  15. package/dist/command-timeouts.d.ts.map +1 -1
  16. package/dist/command-timeouts.js +1 -0
  17. package/dist/command-timeouts.js.map +1 -1
  18. package/dist/config-tiers.d.ts +19 -0
  19. package/dist/config-tiers.d.ts.map +1 -0
  20. package/dist/config-tiers.js +45 -0
  21. package/dist/config-tiers.js.map +1 -0
  22. package/dist/index.d.ts +10 -8
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +7 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/migration.d.ts +27 -0
  27. package/dist/migration.d.ts.map +1 -1
  28. package/dist/migration.js +384 -2
  29. package/dist/migration.js.map +1 -1
  30. package/dist/onnx-runtime.d.ts +4 -6
  31. package/dist/onnx-runtime.d.ts.map +1 -1
  32. package/dist/onnx-runtime.js +13 -15
  33. package/dist/onnx-runtime.js.map +1 -1
  34. package/dist/paths.d.ts +17 -0
  35. package/dist/paths.d.ts.map +1 -1
  36. package/dist/paths.js +52 -1
  37. package/dist/paths.js.map +1 -1
  38. package/dist/pool.d.ts.map +1 -1
  39. package/dist/pool.js +15 -16
  40. package/dist/pool.js.map +1 -1
  41. package/dist/project-identity.d.ts +30 -0
  42. package/dist/project-identity.d.ts.map +1 -0
  43. package/dist/project-identity.js +69 -0
  44. package/dist/project-identity.js.map +1 -0
  45. package/package.json +1 -1
  46. package/dist/pipe-strip.d.ts +0 -7
  47. package/dist/pipe-strip.d.ts.map +0 -1
  48. package/dist/pipe-strip.js +0 -688
  49. package/dist/pipe-strip.js.map +0 -1
@@ -1,688 +0,0 @@
1
- // Filters that only *view* or *reshape* a command's output for human reading.
2
- // When a test/build runner is piped entirely through these, we can drop the
3
- // pipeline and run the bare command — the output compressor reduces the full
4
- // output while preserving failures/summaries, which these filters routinely
5
- // strip away. Two families:
6
- // - viewing: grep, rg, head, tail, cat, less, more
7
- // - transform: sed, awk, cut, sort, uniq, tr, column, fold
8
- // `wc` is deliberately excluded below: it collapses output to a scalar count
9
- // the agent explicitly asked for, so stripping it would be surprising.
10
- const NOISE_FILTERS = new Set([
11
- "grep",
12
- "rg",
13
- "head",
14
- "tail",
15
- "cat",
16
- "less",
17
- "more",
18
- "sed",
19
- "awk",
20
- "cut",
21
- "sort",
22
- "uniq",
23
- "tr",
24
- "column",
25
- "fold",
26
- ]);
27
- const GREP_GUARD_FLAGS = new Set([
28
- "c",
29
- "count",
30
- "q",
31
- "quiet",
32
- "o",
33
- "only-matching",
34
- "l",
35
- "files-with-matches",
36
- ]);
37
- export function maybeStripCompressorPipe(command, compressionEnabled) {
38
- if (!compressionEnabled)
39
- return { command, stripped: false };
40
- const chain = splitTopLevelCommandChain(command);
41
- if (chain === null)
42
- return { command, stripped: false };
43
- let stripped = false;
44
- const droppedFilterChains = [];
45
- const rebuilt = chain
46
- .map(({ segment, separator }) => {
47
- const result = stripSinglePipelineSegment(segment);
48
- if (result.stripped) {
49
- stripped = true;
50
- droppedFilterChains.push(result.filters);
51
- }
52
- return `${result.segment}${separator}`;
53
- })
54
- .join("");
55
- if (!stripped)
56
- return { command, stripped: false };
57
- return {
58
- command: rebuilt,
59
- stripped: true,
60
- note: formatStripNote(droppedFilterChains),
61
- };
62
- }
63
- function stripSinglePipelineSegment(segment) {
64
- const leading = /^\s*/.exec(segment)?.[0] ?? "";
65
- const trailing = /\s*$/.exec(segment)?.[0] ?? "";
66
- const coreStart = leading.length;
67
- const coreEnd = segment.length - trailing.length;
68
- if (coreEnd <= coreStart)
69
- return { segment, stripped: false };
70
- const core = segment.slice(coreStart, coreEnd);
71
- // Bail on shell constructs our lightweight pipe-splitter cannot reason about
72
- // safely. Command substitution / backticks / process substitution can embed
73
- // their own pipes, so naive top-level splitting would carve the segment at an
74
- // INNER pipe and rebuild a malformed runner (e.g.
75
- // `pytest $(find . | head) | grep FAIL` → `pytest $(find .`).
76
- if (containsUnsplittableConstruct(core))
77
- return { segment, stripped: false };
78
- // Standalone backgrounding changes command grouping/exit semantics. Keep this
79
- // segment verbatim, but let other top-level chain segments strip independently.
80
- if (hasUnquotedBackground(core))
81
- return { segment, stripped: false };
82
- const stages = splitTopLevelPipeline(core);
83
- if (stages.length < 2)
84
- return { segment, stripped: false };
85
- const firstStage = stages[0]?.trim() ?? "";
86
- if (!isCompressorHandledRunner(firstStage))
87
- return { segment, stripped: false };
88
- const filterStages = stages.slice(1).map((stage) => stage.trim());
89
- for (const stage of filterStages) {
90
- // A dropped filter stage must be a pure stdin→stdout view. If it writes a
91
- // file, reads a file (bypassing stdin), or backgrounds, dropping it would
92
- // silently lose data or change intent — bail and run the segment verbatim.
93
- if (!filterStageIsSafeToDrop(stage))
94
- return { segment, stripped: false };
95
- }
96
- return {
97
- segment: `${leading}${firstStage}${trailing}`,
98
- stripped: true,
99
- filters: filterStages.join(" | "),
100
- };
101
- }
102
- function formatStripNote(droppedFilterChains) {
103
- const filters = droppedFilterChains.map((filter) => `\`| ${filter}\``).join(", ");
104
- return `[AFT dropped ${filters} (compressed:false to keep)]`;
105
- }
106
- /**
107
- * Split a command into top-level chain segments, respecting quotes, escapes,
108
- * backticks, and parenthesis/substitution depth. Separators inside quotes,
109
- * `$()`, backticks, process substitution, or grouping parens are left inside the
110
- * segment. Returns `null` when the top-level structure is not parseable with
111
- * confidence (unclosed quotes/substitutions/parens, stray closing parens, or a
112
- * top-level newline separator).
113
- */
114
- function splitTopLevelCommandChain(command) {
115
- const segments = [];
116
- let start = 0;
117
- let quote = null;
118
- let escaped = false;
119
- let inBacktick = false;
120
- let parenDepth = 0;
121
- for (let index = 0; index < command.length; index++) {
122
- const char = command[index];
123
- const next = command[index + 1];
124
- if (escaped) {
125
- escaped = false;
126
- continue;
127
- }
128
- if (inBacktick) {
129
- if (char === "\\")
130
- escaped = true;
131
- else if (char === "`")
132
- inBacktick = false;
133
- continue;
134
- }
135
- if (char === "\\" && quote !== "'") {
136
- escaped = true;
137
- continue;
138
- }
139
- if (quote) {
140
- if (char === quote)
141
- quote = null;
142
- continue;
143
- }
144
- if (char === "'" || char === '"') {
145
- quote = char;
146
- continue;
147
- }
148
- if (char === "`") {
149
- inBacktick = true;
150
- continue;
151
- }
152
- if (char === "(") {
153
- parenDepth++;
154
- continue;
155
- }
156
- if (char === ")") {
157
- if (parenDepth === 0)
158
- return null;
159
- parenDepth--;
160
- continue;
161
- }
162
- if (parenDepth > 0)
163
- continue;
164
- // Top-level newline is a command separator, but not one this conservative
165
- // rewriter reassembles; keep the whole command verbatim.
166
- if (char === "\n" || char === "\r")
167
- return null;
168
- // Unquoted shell comment: everything after `#` is inert text, but it can
169
- // CONTAIN separator-looking sequences (`# && echo x`). Splitting there and
170
- // reassembling would promote comment text to executable code — the
171
- // changed-semantics failure class. Bail on the whole command.
172
- if (char === "#" && (index === 0 || command[index - 1] === " " || command[index - 1] === "\t"))
173
- return null;
174
- if (char === "&" && next === "&") {
175
- segments.push({ segment: command.slice(start, index), separator: "&&" });
176
- start = index + 2;
177
- index++;
178
- continue;
179
- }
180
- if (char === "|" && next === "|") {
181
- segments.push({ segment: command.slice(start, index), separator: "||" });
182
- start = index + 2;
183
- index++;
184
- continue;
185
- }
186
- if (char === ";") {
187
- segments.push({ segment: command.slice(start, index), separator: ";" });
188
- start = index + 1;
189
- }
190
- }
191
- if (escaped || quote || inBacktick || parenDepth !== 0)
192
- return null;
193
- segments.push({ segment: command.slice(start), separator: "" });
194
- return segments;
195
- }
196
- function splitTopLevelPipeline(command) {
197
- const stages = [];
198
- let start = 0;
199
- let quote = null;
200
- let escaped = false;
201
- for (let index = 0; index < command.length; index++) {
202
- const char = command[index];
203
- const next = command[index + 1];
204
- const previous = command[index - 1];
205
- if (escaped) {
206
- escaped = false;
207
- continue;
208
- }
209
- if (char === "\\" && quote !== "'") {
210
- escaped = true;
211
- continue;
212
- }
213
- if (quote) {
214
- if (char === quote)
215
- quote = null;
216
- continue;
217
- }
218
- if (char === "'" || char === '"') {
219
- quote = char;
220
- continue;
221
- }
222
- if (char === "|" && previous !== "|" && next !== "|") {
223
- stages.push(command.slice(start, index));
224
- start = index + 1;
225
- }
226
- }
227
- stages.push(command.slice(start));
228
- return stages;
229
- }
230
- /**
231
- * Is the first stage a test/build/lint/typecheck runner whose full output the
232
- * agent actually needs (i.e. failures)? Those are the commands where a
233
- * downstream viewing filter silently hides failures, so stripping the filter
234
- * and letting the compressor reduce the bare output is strictly better.
235
- *
236
- * IMPORTANT — this list is intentionally NARROW. It must only contain commands
237
- * you run to learn "did it pass / build / typecheck cleanly". It must NOT
238
- * include log-emitting or search tools (git, docker, kubectl, ls, find, cat,
239
- * journalctl, …) where a downstream `| grep`/`| tail` is the agent's GENUINE
240
- * intent (e.g. `git log | grep fix`, `docker logs app | tail`). Stripping those
241
- * would change behavior and surprise the agent. When in doubt, leave it out.
242
- */
243
- function isCompressorHandledRunner(stage) {
244
- const tokens = tokenizeStage(stage);
245
- if (tokens.length === 0)
246
- return false;
247
- if (tokens.some((token) => token === "&&" || token === "||" || token.includes(";"))) {
248
- return false;
249
- }
250
- // Peel leading POSIX env-var assignments (`VAR=value`, possibly several) that
251
- // prefix the runner (e.g. `CI=1 bun test`, `FOO=bar BAZ=qux npm test`).
252
- // `containsUnsplittableConstruct` already rejects `VAR=$(cmd)` (command
253
- // substitution), so values here are safe literals or simple expansions.
254
- let tokenOffset = 0;
255
- while (tokenOffset < tokens.length && isEnvAssignment(tokens[tokenOffset])) {
256
- tokenOffset++;
257
- }
258
- // Basename the launcher so `./gradlew`, `./mvnw`, `node_modules/.bin/jest`,
259
- // and `./vendor/bin/phpunit` resolve to their tool name.
260
- const first = runnerName(tokens[tokenOffset]);
261
- const runnerArgs = tokens.slice(tokenOffset + 1);
262
- const second = runnerArgs[0];
263
- const third = runnerArgs[1];
264
- const rest = runnerArgs;
265
- if (!first)
266
- return false;
267
- // --- JavaScript / TypeScript ---
268
- if (first === "bun") {
269
- // Skip `--cwd <dir>` / `--cwd=<dir>` before the subcommand.
270
- let args = rest;
271
- if (args[0] === "--cwd")
272
- args = args.slice(2);
273
- else if (args[0]?.startsWith("--cwd="))
274
- args = args.slice(1);
275
- const sub = args[0];
276
- const subNext = args[1];
277
- return sub === "test" || (sub === "run" && isJsVerificationScript(subNext));
278
- }
279
- if (first === "npm" || first === "pnpm") {
280
- return second === "test" || (second === "run" && isJsVerificationScript(third));
281
- }
282
- if (first === "yarn") {
283
- // yarn berry runs a script by name directly (`yarn test:unit`) and also
284
- // supports `yarn run <script>`.
285
- return isJsVerificationScript(second) || (second === "run" && isJsVerificationScript(third));
286
- }
287
- if (first === "deno")
288
- return ["test", "lint", "check", "bench"].includes(second ?? "");
289
- if (first === "npx") {
290
- return ["tsc", "eslint", "vitest", "jest", "playwright", "biome"].includes(second ?? "");
291
- }
292
- if (first === "playwright")
293
- return second === "test";
294
- // --- Rust ---
295
- if (first === "cargo") {
296
- return ["test", "build", "check", "clippy", "nextest"].includes(second ?? "");
297
- }
298
- // --- Go ---
299
- if (first === "go")
300
- return ["test", "build", "vet"].includes(second ?? "");
301
- // --- Java / JVM (tasks can appear anywhere: `gradle clean test`) ---
302
- // `clean` is allowed — `clean test`/`clean build` is the canonical fresh run
303
- // and only removes build output, unlike stateful goals (publish/deploy).
304
- if (first === "gradle" || first === "gradlew") {
305
- return hasBuildTask(rest, ["test", "check", "build", "assemble", "clean"]);
306
- }
307
- if (first === "mvn" || first === "mvnw") {
308
- return hasBuildTask(rest, ["test", "verify", "package", "install", "clean"]);
309
- }
310
- // --- .NET ---
311
- if (first === "dotnet")
312
- return ["test", "build"].includes(second ?? "");
313
- // --- Ruby ---
314
- if (first === "rspec")
315
- return true;
316
- // Allow multiple task words when all are plain task names (no flags/paths)
317
- // and at least one is `test` or `spec` — so `rake db:setup test` strips but
318
- // `rake test_db_reset` or `rake deploy` does not.
319
- if (first === "rake") {
320
- const positionals = rest.filter((a) => !a.startsWith("-"));
321
- if (positionals.length === 0)
322
- return false;
323
- if (positionals.some((a) => a.includes("/") || a.includes(".") || a.includes("=")))
324
- return false;
325
- return positionals.some((a) => a === "test" || a === "spec");
326
- }
327
- // --- PHP ---
328
- if (first === "phpunit" || first === "pest")
329
- return true;
330
- // --- Apple / Swift ---
331
- // Only real build/test ACTIONS — NOT query commands (`xcodebuild -list`,
332
- // `-showBuildSettings`) and NOT a scheme/target merely NAMED "test"
333
- // (`xcodebuild -showBuildSettings -scheme test`). Actions are bare positional
334
- // tokens, distinct from the values that follow `-scheme`/`-target`/etc.
335
- if (first === "xcodebuild")
336
- return xcodebuildHasBuildAction(rest);
337
- if (first === "swift")
338
- return second === "test" || second === "build";
339
- // --- Make (require an explicit test/lint target — bare `make` is a generic
340
- // build that may legitimately be grepped for errors) ---
341
- if (first === "make" || first === "gmake") {
342
- return hasBuildTask(rest, ["test", "check", "lint", "clean"]);
343
- }
344
- // --- Bare test / lint / typecheck runners ---
345
- return [
346
- "vitest",
347
- "jest",
348
- "pytest",
349
- "tsc",
350
- "eslint",
351
- "biome",
352
- "ruff",
353
- "mypy",
354
- "tox",
355
- "nox",
356
- ].includes(first);
357
- }
358
- /**
359
- * Is this token a POSIX env-var assignment (`NAME=value`)? Name must start with
360
- * a letter or underscore, followed by alphanumerics/underscores, then `=`.
361
- * Rejects `--flag=value`, `path/cmd`, and `$()` values (the latter already
362
- * caught by `containsUnsplittableConstruct` on the whole command).
363
- */
364
- function isEnvAssignment(token) {
365
- if (!token)
366
- return false;
367
- return /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(token);
368
- }
369
- /** Last path segment of a launcher token (`./gradlew` → `gradlew`, `jest` → `jest`). */
370
- function runnerName(token) {
371
- if (!token)
372
- return "";
373
- const slash = token.lastIndexOf("/");
374
- return slash === -1 ? token : token.slice(slash + 1);
375
- }
376
- /**
377
- * Should a make/gradle/mvn invocation be treated as a pure test/build run that
378
- * is safe to strip? Requires (1) at least one of the allowed tasks present, and
379
- * (2) EVERY positional (non-flag) arg to be an allowed task — so a mixed
380
- * invocation like `make deploy test` or `gradle publish test` bails, because
381
- * the stateful goal (deploy/publish) is the real intent whose output matters.
382
- *
383
- * Allowed-task match accepts the bare task (`test`) and qualified Gradle forms
384
- * (`:app:test`), but not substrings (`my-test-module` ≠ `test`). Flags
385
- * (`-x`, `--info`, `-Dkey=val`) and `key=value` make/property args are ignored.
386
- */
387
- function hasBuildTask(args, tasks) {
388
- const isAllowedTask = (arg) => tasks.some((task) => arg === task || arg.endsWith(`:${task}`));
389
- const isFlagOrProperty = (arg) => arg.startsWith("-") || arg.includes("=");
390
- let sawAllowed = false;
391
- for (const arg of args) {
392
- if (isFlagOrProperty(arg))
393
- continue;
394
- if (!isAllowedTask(arg))
395
- return false; // a non-flag positional that isn't an allowed task
396
- sawAllowed = true;
397
- }
398
- return sawAllowed;
399
- }
400
- function isJsVerificationScript(token) {
401
- if (!token)
402
- return false;
403
- return token.startsWith("test") || token === "typecheck" || token.startsWith("typecheck:");
404
- }
405
- // xcodebuild options that take a value (the following token is NOT an action).
406
- const XCODEBUILD_VALUE_FLAGS = new Set([
407
- "-scheme",
408
- "-target",
409
- "-project",
410
- "-workspace",
411
- "-configuration",
412
- "-sdk",
413
- "-destination",
414
- "-arch",
415
- "-derivedDataPath",
416
- "-resultBundlePath",
417
- "-xcconfig",
418
- "-toolchain",
419
- ]);
420
- const XCODEBUILD_BUILD_ACTIONS = new Set([
421
- "build",
422
- "test",
423
- "build-for-testing",
424
- "test-without-building",
425
- "analyze",
426
- ]);
427
- /**
428
- * True only when an actual build/test ACTION appears as a bare positional,
429
- * skipping the value token after a value-taking flag so a scheme/target named
430
- * "test" isn't mistaken for the `test` action.
431
- */
432
- function xcodebuildHasBuildAction(args) {
433
- for (let i = 0; i < args.length; i++) {
434
- const arg = args[i];
435
- if (arg.startsWith("-")) {
436
- if (XCODEBUILD_VALUE_FLAGS.has(arg))
437
- i++; // skip its value
438
- continue;
439
- }
440
- if (XCODEBUILD_BUILD_ACTIONS.has(arg))
441
- return true;
442
- }
443
- return false;
444
- }
445
- /**
446
- * Does the command contain a shell construct that can embed its own pipe and so
447
- * break naive top-level pipe-splitting? Command substitution `$(...)`,
448
- * backticks, process substitution `<(...)`/`>(...)`, and any subshell/grouping
449
- * parentheses `( ... )`. The splitter tracks neither nesting nor paren balance,
450
- * so a pipe inside (or a paren spanning) any of these would be mis-split or
451
- * leave unbalanced parens after the strip (e.g. `(cd d && bun test | tail)`
452
- * → `(cd d && bun test`). Quote-aware so a literal `(` inside quotes is fine.
453
- */
454
- function containsUnsplittableConstruct(command) {
455
- let quote = null;
456
- let escaped = false;
457
- for (let i = 0; i < command.length; i++) {
458
- const char = command[i];
459
- if (escaped) {
460
- escaped = false;
461
- continue;
462
- }
463
- if (char === "\\" && quote !== "'") {
464
- escaped = true;
465
- continue;
466
- }
467
- if (quote === "'") {
468
- // Single quotes are literal — nothing expands inside them.
469
- if (char === "'")
470
- quote = null;
471
- continue;
472
- }
473
- if (quote === '"') {
474
- // Double quotes still allow `$(...)` and backtick command substitution,
475
- // so keep scanning for those even while inside double quotes.
476
- if (char === '"')
477
- quote = null;
478
- else if (char === "`")
479
- return true;
480
- else if (char === "$" && command[i + 1] === "(")
481
- return true;
482
- continue;
483
- }
484
- if (char === "'" || char === '"') {
485
- quote = char;
486
- continue;
487
- }
488
- if (char === "`")
489
- return true;
490
- // Any unquoted paren — command/process substitution, subshell, or grouping.
491
- if (char === "(" || char === ")")
492
- return true;
493
- }
494
- return false;
495
- }
496
- // Filters that, given ANY bare (non-flag) operand, read that operand as a FILE
497
- // instead of stdin — so a stage like `cat saved.log` or `head file` ignores the
498
- // piped output entirely. Dropping them would replace the runner's output with
499
- // the file's contents.
500
- const READS_FILE_OPERAND = new Set(["cat", "tac", "nl", "less", "more"]);
501
- /**
502
- * Is a (to-be-dropped) filter stage a pure stdin→stdout view that can be safely
503
- * removed? It must be a recognized viewing/transform filter invoked with no file
504
- * write, no file read that bypasses stdin, and no backgrounding. Conservative:
505
- * anything ambiguous bails (we just don't optimize — never lose data).
506
- */
507
- function filterStageIsSafeToDrop(stage) {
508
- const head = tokenizeStage(stage)[0];
509
- if (!head)
510
- return false;
511
- if (head === "wc")
512
- return false; // collapses to a scalar the agent asked for
513
- if (!NOISE_FILTERS.has(head))
514
- return false;
515
- // Any unquoted redirect / process-sub / backgrounding metacharacter, OR a
516
- // redirect hidden INSIDE quotes (awk `'{ print > "f" }'`, sed `'w file'`).
517
- // We scan the raw stage for `>`/`<` ANYWHERE (quote-blind) because a redirect
518
- // inside a filter's program is still a write; over-bailing on a literal `>`
519
- // search pattern is safe (we just skip the optimization).
520
- if (/[<>]/.test(stage))
521
- return false;
522
- if (hasUnquotedBackground(stage))
523
- return false;
524
- const args = tokenizeStage(stage).slice(1);
525
- const hasFlag = (...names) => args.some((a) => names.some((n) => a === n || a.startsWith(`${n}=`)));
526
- // grep/rg: a count/list/quiet flag changes the output the agent wanted, AND a
527
- // second bare operand means it reads a FILE not stdin (`grep PAT file`).
528
- // (`-i` here is case-insensitive, NOT in-place — must not bail.)
529
- if (head === "grep" || head === "rg") {
530
- if (hasIntentChangingGrepFlag(args))
531
- return false;
532
- // pattern is one bare operand; more than one means a file argument.
533
- if (countBareOperands(args) > 1)
534
- return false;
535
- return true;
536
- }
537
- // head/tail: bare operands are files unless consumed by -n/-c. Any bare
538
- // non-numeric operand is a filename → reads the file, bypassing stdin.
539
- if (head === "head" || head === "tail") {
540
- if (bareOperands(args).some((op) => !/^\d+$/.test(op)))
541
- return false;
542
- return true;
543
- }
544
- // cat/tac/nl/less/more: any bare operand is a file to read instead of stdin.
545
- if (READS_FILE_OPERAND.has(head)) {
546
- if (countBareOperands(args) > 0)
547
- return false;
548
- return true;
549
- }
550
- // sed: `-i`/`--in-place` writes the file in place; the first bare operand is
551
- // the script, a SECOND is an input file. (Internal `w`/`>` caught by `[<>]`.)
552
- if (head === "sed") {
553
- if (hasFlag("-i", "--in-place"))
554
- return false;
555
- if (countBareOperands(args) > 1)
556
- return false;
557
- return true;
558
- }
559
- // awk: first bare operand is the program, a SECOND is an input file.
560
- if (head === "awk") {
561
- if (countBareOperands(args) > 1)
562
- return false;
563
- return true;
564
- }
565
- // sort: `-o`/`--output` writes a file.
566
- if (head === "sort" && hasFlag("-o", "--output"))
567
- return false;
568
- // sort/uniq/cut/tr/column/fold: a bare path-like operand reads a file.
569
- // (`-o` already caught.) tr's operands are sets, not files; cut/sort can take
570
- // a trailing file. Conservatively bail on any operand that looks like a path.
571
- if (bareOperands(args).some((op) => op.includes("/") || op.includes(".")))
572
- return false;
573
- return true;
574
- }
575
- /** Bare (non-flag, non-flag-value) operands of a tokenized arg list. */
576
- function bareOperands(args) {
577
- const out = [];
578
- let afterDoubleDash = false;
579
- for (const arg of args) {
580
- if (!afterDoubleDash && arg === "--") {
581
- afterDoubleDash = true;
582
- continue;
583
- }
584
- if (!afterDoubleDash && arg.startsWith("-") && arg !== "-")
585
- continue;
586
- out.push(arg);
587
- }
588
- return out;
589
- }
590
- function countBareOperands(args) {
591
- return bareOperands(args).length;
592
- }
593
- /** A standalone background `&` (not `&&`, not the `&` in a `>&`/`&>` fd dup). */
594
- function hasUnquotedBackground(stage) {
595
- let quote = null;
596
- let escaped = false;
597
- for (let i = 0; i < stage.length; i++) {
598
- const char = stage[i];
599
- if (escaped) {
600
- escaped = false;
601
- continue;
602
- }
603
- if (char === "\\" && quote !== "'") {
604
- escaped = true;
605
- continue;
606
- }
607
- if (quote) {
608
- if (char === quote)
609
- quote = null;
610
- continue;
611
- }
612
- if (char === "'" || char === '"') {
613
- quote = char;
614
- continue;
615
- }
616
- if (char === "&") {
617
- const prev = stage[i - 1];
618
- const next = stage[i + 1];
619
- // `&&` (handled elsewhere), `>&`/`&>`/`2>&1` fd dup → not a background.
620
- if (prev === "&" || next === "&" || prev === ">" || next === ">")
621
- continue;
622
- return true;
623
- }
624
- }
625
- return false;
626
- }
627
- function hasIntentChangingGrepFlag(args) {
628
- for (const arg of args) {
629
- if (arg === "--")
630
- return false;
631
- if (!arg.startsWith("-") || arg === "-")
632
- continue;
633
- if (arg.startsWith("--")) {
634
- const flag = arg.slice(2).split("=", 1)[0];
635
- if (GREP_GUARD_FLAGS.has(flag))
636
- return true;
637
- continue;
638
- }
639
- for (const flag of arg.slice(1)) {
640
- if (GREP_GUARD_FLAGS.has(flag))
641
- return true;
642
- }
643
- }
644
- return false;
645
- }
646
- function tokenizeStage(stage) {
647
- const tokens = [];
648
- let current = "";
649
- let quote = null;
650
- let escaped = false;
651
- for (let index = 0; index < stage.length; index++) {
652
- const char = stage[index];
653
- if (escaped) {
654
- current += char;
655
- escaped = false;
656
- continue;
657
- }
658
- if (char === "\\" && quote !== "'") {
659
- escaped = true;
660
- continue;
661
- }
662
- if (quote) {
663
- if (char === quote) {
664
- quote = null;
665
- }
666
- else {
667
- current += char;
668
- }
669
- continue;
670
- }
671
- if (char === "'" || char === '"') {
672
- quote = char;
673
- continue;
674
- }
675
- if (/\s/.test(char)) {
676
- if (current.length > 0) {
677
- tokens.push(current);
678
- current = "";
679
- }
680
- continue;
681
- }
682
- current += char;
683
- }
684
- if (current.length > 0)
685
- tokens.push(current);
686
- return tokens;
687
- }
688
- //# sourceMappingURL=pipe-strip.js.map