pi-rtk-optimizer 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +9 -34
- package/config/config.example.json +0 -9
- package/package.json +3 -3
- package/src/additional-coverage-test.ts +1 -1
- package/src/command-rewriter-test.ts +118 -160
- package/src/command-rewriter.ts +43 -594
- package/src/config-modal-test.ts +2 -0
- package/src/config-modal.ts +1 -105
- package/src/config-store.ts +0 -24
- package/src/index.ts +6 -6
- package/src/rtk-rewrite-provider.ts +90 -0
- package/src/runtime-guard-test.ts +4 -3
- package/src/runtime-guard.ts +1 -1
- package/src/types.ts +0 -18
- package/src/rewrite-bypass.ts +0 -332
- package/src/rewrite-rules.ts +0 -255
package/src/config-modal.ts
CHANGED
|
@@ -54,25 +54,11 @@ function parseIntegerInRange(value: string, min: number, max: number): number |
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function summarizeConfig(config: RtkIntegrationConfig, runtimeStatus: RuntimeStatus): string {
|
|
57
|
-
const categories = [
|
|
58
|
-
config.rewriteGitGithub ? "git" : "",
|
|
59
|
-
config.rewriteFilesystem ? "files" : "",
|
|
60
|
-
config.rewriteRust ? "rust" : "",
|
|
61
|
-
config.rewriteJavaScript ? "js" : "",
|
|
62
|
-
config.rewritePython ? "python" : "",
|
|
63
|
-
config.rewriteGo ? "go" : "",
|
|
64
|
-
config.rewriteContainers ? "containers" : "",
|
|
65
|
-
config.rewriteNetwork ? "network" : "",
|
|
66
|
-
config.rewritePackageManagers ? "packages" : "",
|
|
67
|
-
]
|
|
68
|
-
.filter((value) => value.length > 0)
|
|
69
|
-
.join(",");
|
|
70
|
-
|
|
71
57
|
const runtime = runtimeStatus.rtkAvailable
|
|
72
58
|
? "rtk=available"
|
|
73
59
|
: `rtk=missing${runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : ""}`;
|
|
74
60
|
|
|
75
|
-
return `enabled=${config.enabled}, mode=${config.mode}, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering},
|
|
61
|
+
return `enabled=${config.enabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${runtime}`;
|
|
76
62
|
}
|
|
77
63
|
|
|
78
64
|
function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
|
|
@@ -210,69 +196,6 @@ function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
|
|
|
210
196
|
currentValue: toOnOff(config.outputCompaction.trackSavings),
|
|
211
197
|
values: ON_OFF,
|
|
212
198
|
},
|
|
213
|
-
{
|
|
214
|
-
id: "rewriteGitGithub",
|
|
215
|
-
label: "Rewrite git / gh",
|
|
216
|
-
description: "git status/log/diff and gh pr/issue/run/api/release",
|
|
217
|
-
currentValue: toOnOff(config.rewriteGitGithub),
|
|
218
|
-
values: ON_OFF,
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
id: "rewriteFilesystem",
|
|
222
|
-
label: "Rewrite filesystem commands",
|
|
223
|
-
description: "cat, head, rg/grep, ls, tree, find, diff",
|
|
224
|
-
currentValue: toOnOff(config.rewriteFilesystem),
|
|
225
|
-
values: ON_OFF,
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
id: "rewriteRust",
|
|
229
|
-
label: "Rewrite Rust commands",
|
|
230
|
-
description: "cargo test/build/clippy/check/install/nextest/fmt",
|
|
231
|
-
currentValue: toOnOff(config.rewriteRust),
|
|
232
|
-
values: ON_OFF,
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: "rewriteJavaScript",
|
|
236
|
-
label: "Rewrite JavaScript/TypeScript",
|
|
237
|
-
description: "vitest, npm/npx/next, tsc, lint, prettier, playwright, prisma",
|
|
238
|
-
currentValue: toOnOff(config.rewriteJavaScript),
|
|
239
|
-
values: ON_OFF,
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
id: "rewritePython",
|
|
243
|
-
label: "Rewrite Python",
|
|
244
|
-
description: "pytest, ruff, pip, uv pip",
|
|
245
|
-
currentValue: toOnOff(config.rewritePython),
|
|
246
|
-
values: ON_OFF,
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
id: "rewriteGo",
|
|
250
|
-
label: "Rewrite Go",
|
|
251
|
-
description: "go test/build/vet and golangci-lint",
|
|
252
|
-
currentValue: toOnOff(config.rewriteGo),
|
|
253
|
-
values: ON_OFF,
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
id: "rewriteContainers",
|
|
257
|
-
label: "Rewrite containers",
|
|
258
|
-
description: "docker compose/ps/images/logs/run/build/exec and kubectl core ops",
|
|
259
|
-
currentValue: toOnOff(config.rewriteContainers),
|
|
260
|
-
values: ON_OFF,
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
id: "rewriteNetwork",
|
|
264
|
-
label: "Rewrite network",
|
|
265
|
-
description: "curl and wget",
|
|
266
|
-
currentValue: toOnOff(config.rewriteNetwork),
|
|
267
|
-
values: ON_OFF,
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
id: "rewritePackageManagers",
|
|
271
|
-
label: "Rewrite package managers",
|
|
272
|
-
description: "pnpm list/ls/outdated/build/typecheck and npm list/outdated",
|
|
273
|
-
currentValue: toOnOff(config.rewritePackageManagers),
|
|
274
|
-
values: ON_OFF,
|
|
275
|
-
},
|
|
276
199
|
];
|
|
277
200
|
}
|
|
278
201
|
|
|
@@ -418,24 +341,6 @@ function applySetting(config: RtkIntegrationConfig, id: string, value: string):
|
|
|
418
341
|
trackSavings: value === "on",
|
|
419
342
|
},
|
|
420
343
|
};
|
|
421
|
-
case "rewriteGitGithub":
|
|
422
|
-
return { ...config, rewriteGitGithub: value === "on" };
|
|
423
|
-
case "rewriteFilesystem":
|
|
424
|
-
return { ...config, rewriteFilesystem: value === "on" };
|
|
425
|
-
case "rewriteRust":
|
|
426
|
-
return { ...config, rewriteRust: value === "on" };
|
|
427
|
-
case "rewriteJavaScript":
|
|
428
|
-
return { ...config, rewriteJavaScript: value === "on" };
|
|
429
|
-
case "rewritePython":
|
|
430
|
-
return { ...config, rewritePython: value === "on" };
|
|
431
|
-
case "rewriteGo":
|
|
432
|
-
return { ...config, rewriteGo: value === "on" };
|
|
433
|
-
case "rewriteContainers":
|
|
434
|
-
return { ...config, rewriteContainers: value === "on" };
|
|
435
|
-
case "rewriteNetwork":
|
|
436
|
-
return { ...config, rewriteNetwork: value === "on" };
|
|
437
|
-
case "rewritePackageManagers":
|
|
438
|
-
return { ...config, rewritePackageManagers: value === "on" };
|
|
439
344
|
default:
|
|
440
345
|
return config;
|
|
441
346
|
}
|
|
@@ -461,15 +366,6 @@ function syncSettingValues(settingsList: SettingValueSyncTarget, config: RtkInte
|
|
|
461
366
|
settingsList.updateValue("outputAggregateLinterOutput", toOnOff(config.outputCompaction.aggregateLinterOutput));
|
|
462
367
|
settingsList.updateValue("outputGroupSearchOutput", toOnOff(config.outputCompaction.groupSearchOutput));
|
|
463
368
|
settingsList.updateValue("outputTrackSavings", toOnOff(config.outputCompaction.trackSavings));
|
|
464
|
-
settingsList.updateValue("rewriteGitGithub", toOnOff(config.rewriteGitGithub));
|
|
465
|
-
settingsList.updateValue("rewriteFilesystem", toOnOff(config.rewriteFilesystem));
|
|
466
|
-
settingsList.updateValue("rewriteRust", toOnOff(config.rewriteRust));
|
|
467
|
-
settingsList.updateValue("rewriteJavaScript", toOnOff(config.rewriteJavaScript));
|
|
468
|
-
settingsList.updateValue("rewritePython", toOnOff(config.rewritePython));
|
|
469
|
-
settingsList.updateValue("rewriteGo", toOnOff(config.rewriteGo));
|
|
470
|
-
settingsList.updateValue("rewriteContainers", toOnOff(config.rewriteContainers));
|
|
471
|
-
settingsList.updateValue("rewriteNetwork", toOnOff(config.rewriteNetwork));
|
|
472
|
-
settingsList.updateValue("rewritePackageManagers", toOnOff(config.rewritePackageManagers));
|
|
473
369
|
}
|
|
474
370
|
|
|
475
371
|
async function openSettingsModal(ctx: ExtensionCommandContext, controller: RtkIntegrationController): Promise<void> {
|
package/src/config-store.ts
CHANGED
|
@@ -60,30 +60,6 @@ export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfi
|
|
|
60
60
|
source.showRewriteNotifications,
|
|
61
61
|
DEFAULT_RTK_INTEGRATION_CONFIG.showRewriteNotifications,
|
|
62
62
|
),
|
|
63
|
-
rewriteGitGithub: toBoolean(
|
|
64
|
-
source.rewriteGitGithub,
|
|
65
|
-
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteGitGithub,
|
|
66
|
-
),
|
|
67
|
-
rewriteFilesystem: toBoolean(
|
|
68
|
-
source.rewriteFilesystem,
|
|
69
|
-
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteFilesystem,
|
|
70
|
-
),
|
|
71
|
-
rewriteRust: toBoolean(source.rewriteRust, DEFAULT_RTK_INTEGRATION_CONFIG.rewriteRust),
|
|
72
|
-
rewriteJavaScript: toBoolean(
|
|
73
|
-
source.rewriteJavaScript,
|
|
74
|
-
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteJavaScript,
|
|
75
|
-
),
|
|
76
|
-
rewritePython: toBoolean(source.rewritePython, DEFAULT_RTK_INTEGRATION_CONFIG.rewritePython),
|
|
77
|
-
rewriteGo: toBoolean(source.rewriteGo, DEFAULT_RTK_INTEGRATION_CONFIG.rewriteGo),
|
|
78
|
-
rewriteContainers: toBoolean(
|
|
79
|
-
source.rewriteContainers,
|
|
80
|
-
DEFAULT_RTK_INTEGRATION_CONFIG.rewriteContainers,
|
|
81
|
-
),
|
|
82
|
-
rewriteNetwork: toBoolean(source.rewriteNetwork, DEFAULT_RTK_INTEGRATION_CONFIG.rewriteNetwork),
|
|
83
|
-
rewritePackageManagers: toBoolean(
|
|
84
|
-
source.rewritePackageManagers,
|
|
85
|
-
DEFAULT_RTK_INTEGRATION_CONFIG.rewritePackageManagers,
|
|
86
|
-
),
|
|
87
63
|
outputCompaction: {
|
|
88
64
|
enabled: toBoolean(
|
|
89
65
|
outputCompactionSource.enabled,
|
package/src/index.ts
CHANGED
|
@@ -123,7 +123,6 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
console.warn(`[${EXTENSION_NAME}] ${message}`);
|
|
127
126
|
if (ctx.hasUI) {
|
|
128
127
|
ctx.ui.notify(message, level);
|
|
129
128
|
}
|
|
@@ -222,7 +221,7 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
222
221
|
};
|
|
223
222
|
|
|
224
223
|
const maybeWarnRtkMissing = (ctx: ExtensionContext): void => {
|
|
225
|
-
if (!config.enabled ||
|
|
224
|
+
if (!config.enabled || !config.guardWhenRtkMissing) {
|
|
226
225
|
return;
|
|
227
226
|
}
|
|
228
227
|
|
|
@@ -237,7 +236,8 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
237
236
|
|
|
238
237
|
missingRtkWarningShown = true;
|
|
239
238
|
const reason = runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : "";
|
|
240
|
-
|
|
239
|
+
const handling = config.mode === "suggest" ? "rewrite suggestions" : "command rewrite";
|
|
240
|
+
warnOnce(ctx, `${EXTENSION_NAME}: rtk binary unavailable, ${handling} bypassed${reason}.`);
|
|
241
241
|
};
|
|
242
242
|
|
|
243
243
|
const ensureRuntimeStatusFresh = async (): Promise<void> => {
|
|
@@ -361,8 +361,8 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
361
361
|
return {};
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
const decision = computeRewriteDecision(event.input.command, config);
|
|
365
|
-
if (!decision.changed
|
|
364
|
+
const decision = await computeRewriteDecision(event.input.command, config, pi);
|
|
365
|
+
if (!decision.changed) {
|
|
366
366
|
return {};
|
|
367
367
|
}
|
|
368
368
|
|
|
@@ -376,7 +376,7 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
if (config.mode === "suggest") {
|
|
379
|
-
const suggestionKey = `${decision.
|
|
379
|
+
const suggestionKey = `${decision.originalCommand}:${decision.rewrittenCommand}`;
|
|
380
380
|
if (suggestionNotices.remember(suggestionKey) && ctx.hasUI) {
|
|
381
381
|
ctx.ui.notify(`RTK suggestion: ${decision.rewrittenCommand}`, "info");
|
|
382
382
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export interface RtkRewriteProviderResult {
|
|
4
|
+
changed: boolean;
|
|
5
|
+
originalCommand: string;
|
|
6
|
+
rewrittenCommand: string;
|
|
7
|
+
exitCode: number;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isAlreadyRtk(command: string): boolean {
|
|
12
|
+
const trimmed = command.trimStart();
|
|
13
|
+
return trimmed === "rtk" || trimmed.startsWith("rtk ");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveRtkRewrite(
|
|
17
|
+
pi: ExtensionAPI,
|
|
18
|
+
command: string,
|
|
19
|
+
timeoutMs = 3000,
|
|
20
|
+
): Promise<RtkRewriteProviderResult> {
|
|
21
|
+
if (!command || !command.trim()) {
|
|
22
|
+
return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isAlreadyRtk(command)) {
|
|
26
|
+
return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await pi.exec("rtk", ["rewrite", command], { timeout: timeoutMs });
|
|
31
|
+
|
|
32
|
+
if (result.code === 1) {
|
|
33
|
+
return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (result.code === 2) {
|
|
37
|
+
return {
|
|
38
|
+
changed: false,
|
|
39
|
+
originalCommand: command,
|
|
40
|
+
rewrittenCommand: command,
|
|
41
|
+
exitCode: 2,
|
|
42
|
+
error: result.stderr?.trim() || "rtk denied rewrite",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (result.code === 0 || result.code === 3) {
|
|
47
|
+
const rewritten = result.stdout?.trim();
|
|
48
|
+
if (!rewritten) {
|
|
49
|
+
return {
|
|
50
|
+
changed: false,
|
|
51
|
+
originalCommand: command,
|
|
52
|
+
rewrittenCommand: command,
|
|
53
|
+
exitCode: result.code,
|
|
54
|
+
error: "rtk returned empty output",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (rewritten === command) {
|
|
58
|
+
return {
|
|
59
|
+
changed: false,
|
|
60
|
+
originalCommand: command,
|
|
61
|
+
rewrittenCommand: command,
|
|
62
|
+
exitCode: result.code,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
changed: true,
|
|
67
|
+
originalCommand: command,
|
|
68
|
+
rewrittenCommand: rewritten,
|
|
69
|
+
exitCode: result.code,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
changed: false,
|
|
75
|
+
originalCommand: command,
|
|
76
|
+
rewrittenCommand: command,
|
|
77
|
+
exitCode: result.code,
|
|
78
|
+
error: `unexpected exit code ${result.code}`,
|
|
79
|
+
};
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
return {
|
|
83
|
+
changed: false,
|
|
84
|
+
originalCommand: command,
|
|
85
|
+
rewrittenCommand: command,
|
|
86
|
+
exitCode: -1,
|
|
87
|
+
error: message,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -21,13 +21,14 @@ runTest("rewrite mode still requires RTK availability when guard is enabled", ()
|
|
|
21
21
|
assert.equal(shouldSkipCommandHandlingWhenRtkMissing(config, runtimeStatus(true)), false);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
runTest("suggest mode
|
|
24
|
+
runTest("suggest mode uses RTK availability guard to avoid repeated missing-binary rewrite probes", () => {
|
|
25
25
|
const config = cloneDefaultConfig();
|
|
26
26
|
config.mode = "suggest";
|
|
27
27
|
config.guardWhenRtkMissing = true;
|
|
28
28
|
|
|
29
|
-
assert.equal(shouldRequireRtkAvailabilityForCommandHandling(config),
|
|
30
|
-
assert.equal(shouldSkipCommandHandlingWhenRtkMissing(config, runtimeStatus(false)),
|
|
29
|
+
assert.equal(shouldRequireRtkAvailabilityForCommandHandling(config), true);
|
|
30
|
+
assert.equal(shouldSkipCommandHandlingWhenRtkMissing(config, runtimeStatus(false)), true);
|
|
31
|
+
assert.equal(shouldSkipCommandHandlingWhenRtkMissing(config, runtimeStatus(true)), false);
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
runTest("guard disabled never blocks command handling", () => {
|
package/src/runtime-guard.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { RtkIntegrationConfig, RuntimeStatus } from "./types.js";
|
|
|
3
3
|
export function shouldRequireRtkAvailabilityForCommandHandling(
|
|
4
4
|
config: Pick<RtkIntegrationConfig, "mode" | "guardWhenRtkMissing">,
|
|
5
5
|
): boolean {
|
|
6
|
-
return config.
|
|
6
|
+
return config.guardWhenRtkMissing;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function shouldSkipCommandHandlingWhenRtkMissing(
|
package/src/types.ts
CHANGED
|
@@ -31,15 +31,6 @@ export interface RtkIntegrationConfig {
|
|
|
31
31
|
mode: RtkMode;
|
|
32
32
|
guardWhenRtkMissing: boolean;
|
|
33
33
|
showRewriteNotifications: boolean;
|
|
34
|
-
rewriteGitGithub: boolean;
|
|
35
|
-
rewriteFilesystem: boolean;
|
|
36
|
-
rewriteRust: boolean;
|
|
37
|
-
rewriteJavaScript: boolean;
|
|
38
|
-
rewritePython: boolean;
|
|
39
|
-
rewriteGo: boolean;
|
|
40
|
-
rewriteContainers: boolean;
|
|
41
|
-
rewriteNetwork: boolean;
|
|
42
|
-
rewritePackageManagers: boolean;
|
|
43
34
|
outputCompaction: RtkOutputCompactionConfig;
|
|
44
35
|
}
|
|
45
36
|
|
|
@@ -48,15 +39,6 @@ export const DEFAULT_RTK_INTEGRATION_CONFIG: RtkIntegrationConfig = {
|
|
|
48
39
|
mode: "rewrite",
|
|
49
40
|
guardWhenRtkMissing: true,
|
|
50
41
|
showRewriteNotifications: true,
|
|
51
|
-
rewriteGitGithub: true,
|
|
52
|
-
rewriteFilesystem: true,
|
|
53
|
-
rewriteRust: true,
|
|
54
|
-
rewriteJavaScript: true,
|
|
55
|
-
rewritePython: true,
|
|
56
|
-
rewriteGo: true,
|
|
57
|
-
rewriteContainers: true,
|
|
58
|
-
rewriteNetwork: true,
|
|
59
|
-
rewritePackageManagers: true,
|
|
60
42
|
outputCompaction: {
|
|
61
43
|
enabled: true,
|
|
62
44
|
stripAnsi: true,
|
package/src/rewrite-bypass.ts
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import type { RtkRewriteRule } from "./rewrite-rules.js";
|
|
2
|
-
|
|
3
|
-
const COMMAND_WORD_PATTERN = /"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|`(?:\\.|[^`])*`|[^\s]+/g;
|
|
4
|
-
const BYPASSED_CARGO_SUBCOMMANDS = new Set(["help", "install", "publish"]);
|
|
5
|
-
const GH_STRUCTURED_OUTPUT_FLAGS = ["--json", "--jq", "--template"] as const;
|
|
6
|
-
const UNSAFE_COMPOUND_REWRITE_COMMANDS = new Set(["find", "grep", "rg", "ls"]);
|
|
7
|
-
const BYPASSED_FIND_ACTIONS = new Set([
|
|
8
|
-
"-delete",
|
|
9
|
-
"-exec",
|
|
10
|
-
"-execdir",
|
|
11
|
-
"-fprint",
|
|
12
|
-
"-fprint0",
|
|
13
|
-
"-fprintf",
|
|
14
|
-
"-fls",
|
|
15
|
-
"-ls",
|
|
16
|
-
"-ok",
|
|
17
|
-
"-okdir",
|
|
18
|
-
"-print0",
|
|
19
|
-
"-printf",
|
|
20
|
-
"-prune",
|
|
21
|
-
"-quit",
|
|
22
|
-
]);
|
|
23
|
-
const BASH_INLINE_COMMAND_FLAGS = new Set(["-c", "-cl", "-lc", "--command"]);
|
|
24
|
-
const POWERSHELL_INLINE_COMMAND_FLAGS = new Set(["-c", "-command", "-encodedcommand"]);
|
|
25
|
-
const CMD_INLINE_COMMAND_FLAGS = new Set(["/c", "/k"]);
|
|
26
|
-
const INTERACTIVE_CONTAINER_SHELLS = new Set([
|
|
27
|
-
"ash",
|
|
28
|
-
"bash",
|
|
29
|
-
"cmd",
|
|
30
|
-
"cmd.exe",
|
|
31
|
-
"fish",
|
|
32
|
-
"powershell",
|
|
33
|
-
"powershell.exe",
|
|
34
|
-
"pwsh",
|
|
35
|
-
"pwsh.exe",
|
|
36
|
-
"sh",
|
|
37
|
-
"zsh",
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
function splitCommandWords(commandBody: string): string[] {
|
|
41
|
-
return commandBody.match(COMMAND_WORD_PATTERN) ?? [];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function splitTopLevelCompoundSegments(command: string): string[] {
|
|
45
|
-
const segments: string[] = [];
|
|
46
|
-
let segmentStart = 0;
|
|
47
|
-
let quote: "'" | '"' | "`" | null = null;
|
|
48
|
-
let escaped = false;
|
|
49
|
-
|
|
50
|
-
const pushSegment = (endIndex: number): void => {
|
|
51
|
-
const segment = command.slice(segmentStart, endIndex).trim();
|
|
52
|
-
if (segment) {
|
|
53
|
-
segments.push(segment);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
58
|
-
const char = command[index] ?? "";
|
|
59
|
-
const nextChar = command[index + 1] ?? "";
|
|
60
|
-
const prevChar = index > 0 ? command[index - 1] ?? "" : "";
|
|
61
|
-
|
|
62
|
-
if (escaped) {
|
|
63
|
-
escaped = false;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (quote !== null) {
|
|
68
|
-
if (char === "\\" && quote !== "'") {
|
|
69
|
-
escaped = true;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (char === quote) {
|
|
73
|
-
quote = null;
|
|
74
|
-
}
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (char === "\\") {
|
|
79
|
-
escaped = true;
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
84
|
-
quote = char;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let separatorLength = 0;
|
|
89
|
-
if ((char === "&" && nextChar === "&") || (char === "|" && nextChar === "|") || (char === "|" && nextChar === "&")) {
|
|
90
|
-
separatorLength = 2;
|
|
91
|
-
} else if (char === "|" && prevChar !== ">") {
|
|
92
|
-
separatorLength = 1;
|
|
93
|
-
} else if (char === "&" && nextChar !== ">" && prevChar !== ">" && prevChar !== "<") {
|
|
94
|
-
separatorLength = 1;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Intentionally ignore semicolons here so unquoted sed scripts remain eligible for
|
|
98
|
-
// segment-level rewriting instead of being treated as unsafe compound shells.
|
|
99
|
-
if (separatorLength === 0) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
pushSegment(index);
|
|
104
|
-
segmentStart = index + separatorLength;
|
|
105
|
-
if (separatorLength === 2) {
|
|
106
|
-
index += 1;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
pushSegment(command.length);
|
|
111
|
-
return segments;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function shouldBypassCargoRewrite(tokens: string[]): boolean {
|
|
115
|
-
let index = 1;
|
|
116
|
-
|
|
117
|
-
while (index < tokens.length && tokens[index].startsWith("+")) {
|
|
118
|
-
index += 1;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
while (index < tokens.length && tokens[index].startsWith("-")) {
|
|
122
|
-
index += 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const subcommand = tokens[index]?.toLowerCase();
|
|
126
|
-
if (!subcommand) {
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return BYPASSED_CARGO_SUBCOMMANDS.has(subcommand);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function normalizeCommandWord(token: string): string {
|
|
134
|
-
const unwrapped = token.replace(/^(?:["'`])|(?:["'`])$/g, "");
|
|
135
|
-
const lastPathSeparator = Math.max(unwrapped.lastIndexOf("/"), unwrapped.lastIndexOf("\\"));
|
|
136
|
-
const basename = lastPathSeparator >= 0 ? unwrapped.slice(lastPathSeparator + 1) : unwrapped;
|
|
137
|
-
return basename.toLowerCase();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function findInteractiveShellIndex(tokens: string[], startIndex: number, endIndex: number): number {
|
|
141
|
-
for (let index = startIndex; index < endIndex; index += 1) {
|
|
142
|
-
if (INTERACTIVE_CONTAINER_SHELLS.has(normalizeCommandWord(tokens[index] ?? ""))) {
|
|
143
|
-
return index;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return -1;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function hasTrailingArguments(tokens: string[], startIndex: number, endIndex: number): boolean {
|
|
151
|
-
return startIndex >= 0 && startIndex < endIndex - 1;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function hasStructuredGhOutputFlag(tokens: string[]): boolean {
|
|
155
|
-
return tokens.some((token) => {
|
|
156
|
-
const normalized = token.toLowerCase();
|
|
157
|
-
return GH_STRUCTURED_OUTPUT_FLAGS.some((flag) => normalized === flag || normalized.startsWith(`${flag}=`));
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function hasShortInteractiveFlag(token: string, flag: "i" | "t"): boolean {
|
|
162
|
-
if (!token.startsWith("-") || token.startsWith("--")) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return token.slice(1).includes(flag);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function hasInteractiveFlagPair(tokens: string[], startIndex: number, endIndex: number): boolean {
|
|
170
|
-
let interactive = false;
|
|
171
|
-
let tty = false;
|
|
172
|
-
|
|
173
|
-
for (let index = startIndex; index < endIndex; index += 1) {
|
|
174
|
-
const token = tokens[index] ?? "";
|
|
175
|
-
if (token === "--interactive") {
|
|
176
|
-
interactive = true;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (token === "--tty") {
|
|
180
|
-
tty = true;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (hasShortInteractiveFlag(token, "i")) {
|
|
184
|
-
interactive = true;
|
|
185
|
-
}
|
|
186
|
-
if (hasShortInteractiveFlag(token, "t")) {
|
|
187
|
-
tty = true;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return interactive && tty;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function shouldBypassInteractiveContainerRewrite(tokens: string[]): boolean {
|
|
195
|
-
const command = tokens[0]?.toLowerCase();
|
|
196
|
-
if (!command) {
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (command === "docker" || command === "podman") {
|
|
201
|
-
const subcommand = tokens[1]?.toLowerCase();
|
|
202
|
-
if (subcommand === "run" || subcommand === "exec") {
|
|
203
|
-
const interactiveShellIndex = findInteractiveShellIndex(tokens, 2, tokens.length);
|
|
204
|
-
return (
|
|
205
|
-
interactiveShellIndex >= 0 &&
|
|
206
|
-
!hasTrailingArguments(tokens, interactiveShellIndex, tokens.length) &&
|
|
207
|
-
!hasInteractiveFlagPair(tokens, 2, interactiveShellIndex)
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (subcommand === "compose") {
|
|
212
|
-
const composeSubcommand = tokens[2]?.toLowerCase();
|
|
213
|
-
if (composeSubcommand === "run" || composeSubcommand === "exec") {
|
|
214
|
-
const interactiveShellIndex = findInteractiveShellIndex(tokens, 3, tokens.length);
|
|
215
|
-
return (
|
|
216
|
-
interactiveShellIndex >= 0 &&
|
|
217
|
-
!hasTrailingArguments(tokens, interactiveShellIndex, tokens.length) &&
|
|
218
|
-
!hasInteractiveFlagPair(tokens, 3, interactiveShellIndex)
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (command === "kubectl" && tokens[1]?.toLowerCase() === "exec") {
|
|
225
|
-
const separatorIndex = tokens.indexOf("--");
|
|
226
|
-
if (separatorIndex === -1 || separatorIndex >= tokens.length - 1) {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const interactiveShellIndex = findInteractiveShellIndex(tokens, separatorIndex + 1, tokens.length);
|
|
231
|
-
if (interactiveShellIndex === -1) {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return !hasTrailingArguments(tokens, interactiveShellIndex, tokens.length) && !hasInteractiveFlagPair(tokens, 2, separatorIndex);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function shouldBypassFindRewrite(tokens: string[]): boolean {
|
|
242
|
-
return tokens.slice(1).some((token) => {
|
|
243
|
-
const normalized = token.toLowerCase();
|
|
244
|
-
return (
|
|
245
|
-
BYPASSED_FIND_ACTIONS.has(normalized) ||
|
|
246
|
-
normalized.startsWith("-exec") ||
|
|
247
|
-
normalized.startsWith("-ok") ||
|
|
248
|
-
normalized.startsWith("-printf") ||
|
|
249
|
-
normalized.startsWith("-fprint") ||
|
|
250
|
-
normalized.startsWith("-fls")
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function shouldBypassLsRewrite(tokens: string[]): boolean {
|
|
256
|
-
return tokens.slice(1).some((token) => token.startsWith("-"));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function shouldBypassNativeShellProxyRewrite(tokens: string[]): boolean {
|
|
260
|
-
const command = normalizeCommandWord(tokens[0] ?? "");
|
|
261
|
-
const firstArgument = tokens[1]?.toLowerCase();
|
|
262
|
-
if (!command || !firstArgument) {
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (command === "bash") {
|
|
267
|
-
return BASH_INLINE_COMMAND_FLAGS.has(firstArgument);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (command === "powershell" || command === "powershell.exe") {
|
|
271
|
-
return POWERSHELL_INLINE_COMMAND_FLAGS.has(firstArgument);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (command === "cmd" || command === "cmd.exe") {
|
|
275
|
-
return CMD_INLINE_COMMAND_FLAGS.has(firstArgument);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Skips entire compound commands when any segment depends on native shell piping or
|
|
283
|
-
* formatting-sensitive search/list output that RTK wrappers may not preserve exactly.
|
|
284
|
-
*/
|
|
285
|
-
export function shouldBypassWholeCommandRewrite(command: string): boolean {
|
|
286
|
-
const segments = splitTopLevelCompoundSegments(command.trim());
|
|
287
|
-
if (segments.length <= 1) {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return segments.some((segment) => {
|
|
292
|
-
const tokens = splitCommandWords(segment);
|
|
293
|
-
const commandName = normalizeCommandWord(tokens[0] ?? "");
|
|
294
|
-
return UNSAFE_COMPOUND_REWRITE_COMMANDS.has(commandName) || shouldBypassNativeShellProxyRewrite(tokens);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Skips RTK rewrites for command shapes that do not map cleanly to RTK wrappers.
|
|
300
|
-
*/
|
|
301
|
-
export function shouldBypassRewriteForCommand(commandBody: string, rule: RtkRewriteRule): boolean {
|
|
302
|
-
const tokens = splitCommandWords(commandBody.trim());
|
|
303
|
-
if (tokens.length === 0) {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (tokens[0]?.toLowerCase() === "gh" && hasStructuredGhOutputFlag(tokens)) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (rule.category === "rust" && tokens[0]?.toLowerCase() === "cargo") {
|
|
312
|
-
return shouldBypassCargoRewrite(tokens);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (rule.category === "containers") {
|
|
316
|
-
return shouldBypassInteractiveContainerRewrite(tokens);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (rule.id === "find") {
|
|
320
|
-
return shouldBypassFindRewrite(tokens);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (rule.id === "ls") {
|
|
324
|
-
return shouldBypassLsRewrite(tokens);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (rule.id === "bash-proxy" || rule.id === "cmd-proxy" || rule.id === "powershell-proxy") {
|
|
328
|
-
return shouldBypassNativeShellProxyRewrite(tokens);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return false;
|
|
332
|
-
}
|