@cortexkit/aft-bridge 0.39.4 → 0.40.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.
- package/dist/bash-format.d.ts +0 -1
- package/dist/bash-format.d.ts.map +1 -1
- package/dist/bash-format.js +0 -3
- package/dist/bash-format.js.map +1 -1
- package/dist/bash-hints.js +97 -9
- package/dist/bash-hints.js.map +1 -1
- package/dist/bridge.d.ts +7 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +39 -42
- package/dist/bridge.js.map +1 -1
- package/dist/coerce.d.ts +39 -0
- package/dist/coerce.d.ts.map +1 -1
- package/dist/coerce.js +82 -0
- package/dist/coerce.js.map +1 -1
- package/dist/command-timeouts.d.ts.map +1 -1
- package/dist/command-timeouts.js +1 -0
- package/dist/command-timeouts.js.map +1 -1
- package/dist/config-tiers.d.ts +19 -0
- package/dist/config-tiers.d.ts.map +1 -0
- package/dist/config-tiers.js +45 -0
- package/dist/config-tiers.js.map +1 -0
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/migration.d.ts +27 -0
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +384 -2
- package/dist/migration.js.map +1 -1
- package/dist/onnx-runtime.d.ts +4 -6
- package/dist/onnx-runtime.d.ts.map +1 -1
- package/dist/onnx-runtime.js +13 -15
- package/dist/onnx-runtime.js.map +1 -1
- package/dist/paths.d.ts +17 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +52 -1
- package/dist/paths.js.map +1 -1
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +15 -16
- package/dist/pool.js.map +1 -1
- package/dist/project-identity.d.ts +30 -0
- package/dist/project-identity.d.ts.map +1 -0
- package/dist/project-identity.js +69 -0
- package/dist/project-identity.js.map +1 -0
- package/package.json +1 -1
- package/dist/pipe-strip.d.ts +0 -7
- package/dist/pipe-strip.d.ts.map +0 -1
- package/dist/pipe-strip.js +0 -688
- package/dist/pipe-strip.js.map +0 -1
package/dist/pipe-strip.js
DELETED
|
@@ -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
|