pi-rtk-optimizer 0.3.2 → 0.4.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 +83 -49
- package/README.md +292 -283
- package/config/config.example.json +36 -35
- package/package.json +60 -59
- package/src/additional-coverage-test.ts +197 -0
- package/src/boolean-format.ts +3 -0
- package/src/command-rewriter-test.ts +160 -0
- package/src/command-rewriter.ts +594 -349
- package/src/config-modal-test.ts +168 -0
- package/src/config-modal.ts +613 -600
- package/src/config-store.ts +224 -217
- package/src/index-test.ts +54 -0
- package/src/index.ts +320 -291
- package/src/output-compactor-test.ts +334 -120
- package/src/output-compactor.ts +418 -343
- package/src/record-utils.ts +6 -0
- package/src/rewrite-bypass.ts +332 -0
- package/src/rewrite-pipeline-safety.ts +154 -0
- package/src/rewrite-rules.ts +255 -248
- package/src/runtime-guard-test.ts +42 -0
- package/src/runtime-guard.ts +14 -0
- package/src/techniques/build.ts +155 -155
- package/src/techniques/git.ts +229 -229
- package/src/techniques/index.ts +8 -16
- package/src/techniques/linter.ts +151 -161
- package/src/techniques/path-utils.ts +67 -0
- package/src/techniques/search.ts +67 -76
- package/src/techniques/source.ts +253 -230
- package/src/techniques/test-output.ts +172 -172
- package/src/test-helpers.ts +10 -0
- package/src/types-shims.d.ts +189 -131
- package/src/types.ts +103 -114
- package/src/compat-commands.ts +0 -207
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"enabled": true,
|
|
3
|
-
"mode": "rewrite",
|
|
4
|
-
"guardWhenRtkMissing": true,
|
|
5
|
-
"showRewriteNotifications": true,
|
|
6
|
-
"rewriteGitGithub": true,
|
|
7
|
-
"rewriteFilesystem": true,
|
|
8
|
-
"rewriteRust": true,
|
|
9
|
-
"rewriteJavaScript": true,
|
|
10
|
-
"rewritePython": true,
|
|
11
|
-
"rewriteGo": true,
|
|
12
|
-
"rewriteContainers": true,
|
|
13
|
-
"rewriteNetwork": true,
|
|
14
|
-
"rewritePackageManagers": true,
|
|
15
|
-
"outputCompaction": {
|
|
16
|
-
"enabled": true,
|
|
17
|
-
"stripAnsi": true,
|
|
18
|
-
"sourceCodeFilteringEnabled": true,
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"enabled": true,
|
|
3
|
+
"mode": "rewrite",
|
|
4
|
+
"guardWhenRtkMissing": true,
|
|
5
|
+
"showRewriteNotifications": true,
|
|
6
|
+
"rewriteGitGithub": true,
|
|
7
|
+
"rewriteFilesystem": true,
|
|
8
|
+
"rewriteRust": true,
|
|
9
|
+
"rewriteJavaScript": true,
|
|
10
|
+
"rewritePython": true,
|
|
11
|
+
"rewriteGo": true,
|
|
12
|
+
"rewriteContainers": true,
|
|
13
|
+
"rewriteNetwork": true,
|
|
14
|
+
"rewritePackageManagers": true,
|
|
15
|
+
"outputCompaction": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"stripAnsi": true,
|
|
18
|
+
"sourceCodeFilteringEnabled": true,
|
|
19
|
+
"preserveExactSkillReads": false,
|
|
20
|
+
"truncate": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"maxChars": 12000
|
|
23
|
+
},
|
|
24
|
+
"sourceCodeFiltering": "minimal",
|
|
25
|
+
"smartTruncate": {
|
|
26
|
+
"enabled": true,
|
|
27
|
+
"maxLines": 220
|
|
28
|
+
},
|
|
29
|
+
"aggregateTestOutput": true,
|
|
30
|
+
"filterBuildOutput": true,
|
|
31
|
+
"compactGitOutput": true,
|
|
32
|
+
"aggregateLinterOutput": true,
|
|
33
|
+
"groupSearchOutput": true,
|
|
34
|
+
"trackSavings": true
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,59 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-rtk-optimizer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension that optimizes RTK command rewriting and tool output compaction for the coding agent.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.ts"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"index.ts",
|
|
12
|
-
"src",
|
|
13
|
-
"config/config.example.json",
|
|
14
|
-
"README.md",
|
|
15
|
-
"CHANGELOG.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
20
|
-
"lint": "npm run build",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"pi",
|
|
28
|
-
"pi
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"@mariozechner/pi-
|
|
58
|
-
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-rtk-optimizer",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Pi extension that optimizes RTK command rewriting and tool output compaction for the coding agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src",
|
|
13
|
+
"config/config.example.json",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
20
|
+
"lint": "npm run build",
|
|
21
|
+
"typecheck": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json",
|
|
22
|
+
"test": "bun ./src/output-compactor-test.ts && bun ./src/command-rewriter-test.ts && bun ./src/runtime-guard-test.ts && bun ./src/additional-coverage-test.ts && bun ./src/config-modal-test.ts && bun ./src/index-test.ts",
|
|
23
|
+
"check": "npm run lint && npm run typecheck && npm run test",
|
|
24
|
+
"build:check": "bunx esbuild ./index.ts --bundle --platform=node --format=esm --outfile=./.pi-rtk-optimizer-check.mjs --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-tui && bun -e \"import { unlinkSync } from 'node:fs'; unlinkSync('./.pi-rtk-optimizer-check.mjs');\""
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"pi-package",
|
|
28
|
+
"pi",
|
|
29
|
+
"pi-extension",
|
|
30
|
+
"rtk",
|
|
31
|
+
"token-optimization",
|
|
32
|
+
"tool-compaction",
|
|
33
|
+
"coding-agent"
|
|
34
|
+
],
|
|
35
|
+
"author": "MasuRii",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/MasuRii/pi-rtk-optimizer.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/MasuRii/pi-rtk-optimizer/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/MasuRii/pi-rtk-optimizer#readme",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"pi": {
|
|
52
|
+
"extensions": [
|
|
53
|
+
"./index.ts"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
58
|
+
"@mariozechner/pi-tui": "*"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ensureConfigExists,
|
|
6
|
+
getRtkIntegrationConfigPath,
|
|
7
|
+
loadRtkIntegrationConfig,
|
|
8
|
+
normalizeRtkIntegrationConfig,
|
|
9
|
+
saveRtkIntegrationConfig,
|
|
10
|
+
} from "./config-store.ts";
|
|
11
|
+
import { clearOutputMetrics, getOutputMetricsSummary, trackOutputSavings } from "./output-metrics.ts";
|
|
12
|
+
import { runTest } from "./test-helpers.ts";
|
|
13
|
+
import { matchesCommandPatterns, normalizeCommandForDetection } from "./techniques/command-detection.ts";
|
|
14
|
+
import { compactPath } from "./techniques/path-utils.ts";
|
|
15
|
+
import { applyWindowsBashCompatibilityFixes } from "./windows-command-helpers.ts";
|
|
16
|
+
import { applyRewrittenCommandShellSafetyFixups } from "./rewrite-pipeline-safety.ts";
|
|
17
|
+
|
|
18
|
+
function makeTempConfigPath(): string {
|
|
19
|
+
return `${getRtkIntegrationConfigPath()}.test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanupFile(path: string): void {
|
|
23
|
+
for (const candidate of [path, `${path}.tmp`]) {
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(candidate)) {
|
|
26
|
+
unlinkSync(candidate);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// Ignore cleanup failures in tests.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
runTest("config-store normalizes invalid values and clamps numeric ranges", () => {
|
|
35
|
+
const normalized = normalizeRtkIntegrationConfig({
|
|
36
|
+
enabled: "yes",
|
|
37
|
+
mode: "invalid",
|
|
38
|
+
rewriteGitGithub: false,
|
|
39
|
+
outputCompaction: {
|
|
40
|
+
stripAnsi: false,
|
|
41
|
+
sourceCodeFilteringEnabled: "sometimes",
|
|
42
|
+
sourceCodeFiltering: "extreme",
|
|
43
|
+
truncate: {
|
|
44
|
+
enabled: true,
|
|
45
|
+
maxChars: 12,
|
|
46
|
+
},
|
|
47
|
+
smartTruncate: {
|
|
48
|
+
enabled: true,
|
|
49
|
+
maxLines: 999_999,
|
|
50
|
+
},
|
|
51
|
+
trackSavings: false,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.equal(normalized.enabled, true);
|
|
56
|
+
assert.equal(normalized.mode, "rewrite");
|
|
57
|
+
assert.equal(normalized.rewriteGitGithub, false);
|
|
58
|
+
assert.equal(normalized.outputCompaction.stripAnsi, false);
|
|
59
|
+
assert.equal(normalized.outputCompaction.sourceCodeFilteringEnabled, true);
|
|
60
|
+
assert.equal(normalized.outputCompaction.sourceCodeFiltering, "minimal");
|
|
61
|
+
assert.equal(normalized.outputCompaction.truncate.maxChars, 1_000);
|
|
62
|
+
assert.equal(normalized.outputCompaction.smartTruncate.maxLines, 4_000);
|
|
63
|
+
assert.equal(normalized.outputCompaction.trackSavings, false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
runTest("config-store can ensure, save, and reload isolated config files", () => {
|
|
67
|
+
const tempPath = makeTempConfigPath();
|
|
68
|
+
cleanupFile(tempPath);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const ensured = ensureConfigExists(tempPath);
|
|
72
|
+
assert.equal(ensured.created, true);
|
|
73
|
+
assert.equal(existsSync(tempPath), true);
|
|
74
|
+
|
|
75
|
+
const defaultLoad = loadRtkIntegrationConfig(tempPath);
|
|
76
|
+
assert.equal(defaultLoad.warning, undefined);
|
|
77
|
+
assert.equal(defaultLoad.config.mode, "rewrite");
|
|
78
|
+
|
|
79
|
+
const saved = saveRtkIntegrationConfig(
|
|
80
|
+
{
|
|
81
|
+
...defaultLoad.config,
|
|
82
|
+
mode: "suggest",
|
|
83
|
+
outputCompaction: {
|
|
84
|
+
...defaultLoad.config.outputCompaction,
|
|
85
|
+
truncate: {
|
|
86
|
+
...defaultLoad.config.outputCompaction.truncate,
|
|
87
|
+
maxChars: 250_000,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
tempPath,
|
|
92
|
+
);
|
|
93
|
+
assert.equal(saved.success, true);
|
|
94
|
+
|
|
95
|
+
const reloaded = loadRtkIntegrationConfig(tempPath);
|
|
96
|
+
assert.equal(reloaded.config.mode, "suggest");
|
|
97
|
+
assert.equal(reloaded.config.outputCompaction.truncate.maxChars, 200_000);
|
|
98
|
+
assert.ok(readFileSync(tempPath, "utf-8").endsWith("\n"));
|
|
99
|
+
} finally {
|
|
100
|
+
cleanupFile(tempPath);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
runTest("config-store falls back to defaults when JSON is invalid", () => {
|
|
105
|
+
const tempPath = makeTempConfigPath();
|
|
106
|
+
cleanupFile(tempPath);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
writeFileSync(tempPath, "{not valid json", "utf-8");
|
|
110
|
+
const loaded = loadRtkIntegrationConfig(tempPath);
|
|
111
|
+
assert.equal(loaded.config.mode, "rewrite");
|
|
112
|
+
assert.ok((loaded.warning ?? "").includes(tempPath));
|
|
113
|
+
assert.ok((loaded.warning ?? "").includes("Failed to parse"));
|
|
114
|
+
} finally {
|
|
115
|
+
cleanupFile(tempPath);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
runTest("output metrics summarize tracked savings and clear state", () => {
|
|
120
|
+
clearOutputMetrics();
|
|
121
|
+
assert.equal(getOutputMetricsSummary(), "RTK output compaction metrics: no data yet.");
|
|
122
|
+
|
|
123
|
+
const first = trackOutputSavings("1234567890", "12345", "bash", ["ansi", "truncate"]);
|
|
124
|
+
assert.equal(first.tool, "bash");
|
|
125
|
+
assert.equal(first.techniques, "ansi,truncate");
|
|
126
|
+
assert.equal(first.savingsPercent, 50);
|
|
127
|
+
|
|
128
|
+
trackOutputSavings("123456", "1234", "read", []);
|
|
129
|
+
const summary = getOutputMetricsSummary();
|
|
130
|
+
assert.ok(summary.includes("calls=2, saved=7 chars (43.8%)"));
|
|
131
|
+
assert.ok(summary.includes("- bash: 1 calls, saved 5 chars (50.0%)"));
|
|
132
|
+
assert.ok(summary.includes("- read: 1 calls, saved 2 chars (33.3%)"));
|
|
133
|
+
|
|
134
|
+
clearOutputMetrics();
|
|
135
|
+
assert.equal(getOutputMetricsSummary(), "RTK output compaction metrics: no data yet.");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
runTest("command detection ignores env prefixes, blank lines, and chained suffixes", () => {
|
|
139
|
+
assert.equal(normalizeCommandForDetection("NODE_ENV=test FOO=bar npm test && echo done"), "npm test");
|
|
140
|
+
assert.equal(normalizeCommandForDetection("\n\n PYTHONPATH=src git status\n echo later"), "git status");
|
|
141
|
+
assert.equal(normalizeCommandForDetection(" "), null);
|
|
142
|
+
assert.equal(matchesCommandPatterns("CI=1 bun test | head -5", [/^bun test/]), true);
|
|
143
|
+
assert.equal(matchesCommandPatterns("echo hello", [/^bun test/]), false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
runTest("path compaction preserves the tail and handles Windows separators", () => {
|
|
147
|
+
const unixPath = "/Users/example/projects/pi-rtk-optimizer/src/techniques/path-utils.ts";
|
|
148
|
+
const compactUnixPath = compactPath(unixPath, 28);
|
|
149
|
+
assert.ok(compactUnixPath.length <= 28);
|
|
150
|
+
assert.ok(compactUnixPath.endsWith("path-utils.ts"));
|
|
151
|
+
assert.ok(compactUnixPath.includes("/"));
|
|
152
|
+
|
|
153
|
+
const windowsPath = "C:\\Users\\Administrator\\Documents\\pi-rtk-optimizer\\src\\windows-command-helpers.ts";
|
|
154
|
+
const compactWindowsPath = compactPath(windowsPath, 30);
|
|
155
|
+
assert.ok(compactWindowsPath.length <= 30);
|
|
156
|
+
assert.equal(compactWindowsPath.includes("\\"), true);
|
|
157
|
+
assert.ok(compactWindowsPath.endsWith("windows-command-helpers.ts"));
|
|
158
|
+
|
|
159
|
+
assert.equal(compactPath("src/file.ts", 40), "src/file.ts");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
runTest("windows bash compatibility rewrites only when the runtime is Windows", () => {
|
|
163
|
+
const command = "cd /d C:\\Users\\Administrator\\project && python script.py";
|
|
164
|
+
const fixed = applyWindowsBashCompatibilityFixes(command);
|
|
165
|
+
|
|
166
|
+
if (process.platform === "win32") {
|
|
167
|
+
assert.deepEqual(fixed.applied, ["cd-/d", "python-utf8"]);
|
|
168
|
+
assert.equal(
|
|
169
|
+
fixed.command,
|
|
170
|
+
'PYTHONIOENCODING=utf-8 cd "C:/Users/Administrator/project" && python script.py',
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const alreadyUtf8 = applyWindowsBashCompatibilityFixes("PYTHONIOENCODING=utf-8 python script.py");
|
|
174
|
+
assert.deepEqual(alreadyUtf8.applied, []);
|
|
175
|
+
assert.equal(alreadyUtf8.command, "PYTHONIOENCODING=utf-8 python script.py");
|
|
176
|
+
} else {
|
|
177
|
+
assert.deepEqual(fixed.applied, []);
|
|
178
|
+
assert.equal(fixed.command, command);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
runTest("rewrite pipeline safety buffers rewritten Windows producer commands", () => {
|
|
183
|
+
const rewritten = applyRewrittenCommandShellSafetyFixups("rtk git diff | grep TODO");
|
|
184
|
+
|
|
185
|
+
if (process.platform === "win32") {
|
|
186
|
+
assert.ok(rewritten.includes('mktemp'));
|
|
187
|
+
assert.ok(rewritten.includes('trap'));
|
|
188
|
+
assert.ok(rewritten.includes('rtk git diff > "$__pi_rtk_pipe_tmp"'));
|
|
189
|
+
assert.ok(rewritten.includes('(grep TODO) < "$__pi_rtk_pipe_tmp"'));
|
|
190
|
+
} else {
|
|
191
|
+
assert.equal(rewritten, "rtk git diff | grep TODO");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
assert.equal(applyRewrittenCommandShellSafetyFixups("git diff | grep TODO"), "git diff | grep TODO");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
console.log("All additional coverage tests passed.");
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import { computeRewriteDecision } from "./command-rewriter.ts";
|
|
4
|
+
import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
|
|
5
|
+
|
|
6
|
+
function expectedProxyExecutable(base: string): string {
|
|
7
|
+
const windowsExecutables = new Set(["npm", "npx", "pnpm", "yarn"]);
|
|
8
|
+
return process.platform === "win32" && windowsExecutables.has(base) ? `${base}.cmd` : base;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
runTest("pnpm dlx rewrites through RTK proxy instead of generic pnpm wrapper", () => {
|
|
12
|
+
const config = cloneDefaultConfig();
|
|
13
|
+
const decision = computeRewriteDecision("pnpm dlx create-vite@latest demo --template react-ts", config);
|
|
14
|
+
|
|
15
|
+
assert.equal(decision.changed, true);
|
|
16
|
+
assert.equal(decision.rule?.id, "pnpm-dlx-proxy");
|
|
17
|
+
assert.equal(
|
|
18
|
+
decision.rewrittenCommand,
|
|
19
|
+
`rtk proxy ${expectedProxyExecutable("pnpm")} dlx create-vite@latest demo --template react-ts`,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
runTest("docker run with shell command bypasses rewrite when interactive flags are missing", () => {
|
|
24
|
+
const config = cloneDefaultConfig();
|
|
25
|
+
const decision = computeRewriteDecision("docker run ubuntu bash", config);
|
|
26
|
+
|
|
27
|
+
assert.equal(decision.changed, false);
|
|
28
|
+
assert.equal(decision.rewrittenCommand, "docker run ubuntu bash");
|
|
29
|
+
assert.equal(decision.reason, "no_match");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
runTest("docker run with -it keeps container rewrite enabled", () => {
|
|
33
|
+
const config = cloneDefaultConfig();
|
|
34
|
+
const decision = computeRewriteDecision("docker run -it ubuntu bash", config);
|
|
35
|
+
|
|
36
|
+
assert.equal(decision.changed, true);
|
|
37
|
+
assert.equal(decision.rule?.id, "docker");
|
|
38
|
+
assert.equal(decision.rewrittenCommand, "rtk docker run -it ubuntu bash");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
runTest("docker compose exec without -it bypasses interactive shell rewrite", () => {
|
|
42
|
+
const config = cloneDefaultConfig();
|
|
43
|
+
const decision = computeRewriteDecision("docker compose exec web bash", config);
|
|
44
|
+
|
|
45
|
+
assert.equal(decision.changed, false);
|
|
46
|
+
assert.equal(decision.rewrittenCommand, "docker compose exec web bash");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
runTest("container rewrites stay enabled for scripted shells and non-shell commands", () => {
|
|
50
|
+
const config = cloneDefaultConfig();
|
|
51
|
+
|
|
52
|
+
const scriptedShell = computeRewriteDecision('docker run ubuntu bash -lc "echo hi"', config);
|
|
53
|
+
assert.equal(scriptedShell.changed, true);
|
|
54
|
+
assert.equal(scriptedShell.rule?.id, "docker");
|
|
55
|
+
assert.equal(scriptedShell.rewrittenCommand, 'rtk docker run ubuntu bash -lc "echo hi"');
|
|
56
|
+
|
|
57
|
+
const nonShell = computeRewriteDecision("docker run ubuntu python app.py", config);
|
|
58
|
+
assert.equal(nonShell.changed, true);
|
|
59
|
+
assert.equal(nonShell.rule?.id, "docker");
|
|
60
|
+
assert.equal(nonShell.rewrittenCommand, "rtk docker run ubuntu python app.py");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
runTest("kubectl exec requires interactive flags before rewriting shell sessions", () => {
|
|
64
|
+
const config = cloneDefaultConfig();
|
|
65
|
+
const missingFlagsDecision = computeRewriteDecision("kubectl exec pod-123 -- bash", config);
|
|
66
|
+
assert.equal(missingFlagsDecision.changed, false);
|
|
67
|
+
assert.equal(missingFlagsDecision.rewrittenCommand, "kubectl exec pod-123 -- bash");
|
|
68
|
+
|
|
69
|
+
const interactiveDecision = computeRewriteDecision("kubectl exec -it pod-123 -- bash", config);
|
|
70
|
+
assert.equal(interactiveDecision.changed, true);
|
|
71
|
+
assert.equal(interactiveDecision.rule?.id, "kubectl");
|
|
72
|
+
assert.equal(interactiveDecision.rewrittenCommand, "rtk kubectl exec -it pod-123 -- bash");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
runTest("sed scripts keep internal separators intact while later pipe segments still rewrite", () => {
|
|
76
|
+
const config = cloneDefaultConfig();
|
|
77
|
+
const decision = computeRewriteDecision("sed -e s/a/b/;d file.txt | git status", config);
|
|
78
|
+
|
|
79
|
+
assert.equal(decision.changed, true);
|
|
80
|
+
assert.equal(decision.rewrittenCommand, "sed -e s/a/b/;d file.txt | rtk git status");
|
|
81
|
+
assert.equal(decision.rule?.id, "git-any");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
runTest("background operators rewrite both command segments without misreading redirect ampersands", () => {
|
|
85
|
+
const config = cloneDefaultConfig();
|
|
86
|
+
const backgroundDecision = computeRewriteDecision("git status & cargo test", config);
|
|
87
|
+
assert.equal(backgroundDecision.changed, true);
|
|
88
|
+
assert.equal(backgroundDecision.rewrittenCommand, "rtk git status & rtk cargo test");
|
|
89
|
+
|
|
90
|
+
const redirectDecision = computeRewriteDecision("cargo test 2>&1 | head -5", config);
|
|
91
|
+
assert.equal(redirectDecision.changed, true);
|
|
92
|
+
assert.equal(redirectDecision.rewrittenCommand, "rtk cargo test 2>&1 | head -5");
|
|
93
|
+
assert.equal(redirectDecision.rule?.id, "cargo-any");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
runTest("gh structured output commands bypass RTK rewrites", () => {
|
|
97
|
+
const config = cloneDefaultConfig();
|
|
98
|
+
const structuredCommands = [
|
|
99
|
+
"gh pr list --json number,title",
|
|
100
|
+
"gh issue list --jq '.[].title'",
|
|
101
|
+
"gh pr view 123 --template '{{.title}}'",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const command of structuredCommands) {
|
|
105
|
+
const decision = computeRewriteDecision(command, config);
|
|
106
|
+
assert.equal(decision.changed, false);
|
|
107
|
+
assert.equal(decision.rewrittenCommand, command);
|
|
108
|
+
assert.equal(decision.reason, "no_match");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
runTest("compound find and search shell flows bypass rewrites when RTK parity is unsafe", () => {
|
|
113
|
+
const config = cloneDefaultConfig();
|
|
114
|
+
const auditedCommands = [
|
|
115
|
+
"find src -type f | xargs grep todo",
|
|
116
|
+
"find . -name '*.ts' -exec grep -n FIXME {} +",
|
|
117
|
+
"grep -R TODO src | head -n 5",
|
|
118
|
+
"rg TODO src && wc -l",
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const command of auditedCommands) {
|
|
122
|
+
const decision = computeRewriteDecision(command, config);
|
|
123
|
+
assert.equal(decision.changed, false, command);
|
|
124
|
+
assert.equal(decision.rewrittenCommand, command, command);
|
|
125
|
+
assert.equal(decision.reason, "no_match", command);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
runTest("formatting-sensitive ls flows bypass rewrites", () => {
|
|
130
|
+
const config = cloneDefaultConfig();
|
|
131
|
+
const auditedCommands = ["ls -la", "ls -l src | grep command-rewriter"];
|
|
132
|
+
|
|
133
|
+
for (const command of auditedCommands) {
|
|
134
|
+
const decision = computeRewriteDecision(command, config);
|
|
135
|
+
assert.equal(decision.changed, false, command);
|
|
136
|
+
assert.equal(decision.rewrittenCommand, command, command);
|
|
137
|
+
assert.equal(decision.reason, "no_match", command);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
runTest("native bash shell proxy flows bypass rewrites when RTK parity is unsafe", () => {
|
|
142
|
+
const config = cloneDefaultConfig();
|
|
143
|
+
const command = 'bash -lc "find src -type f | xargs grep todo"';
|
|
144
|
+
const decision = computeRewriteDecision(command, config);
|
|
145
|
+
|
|
146
|
+
assert.equal(decision.changed, false);
|
|
147
|
+
assert.equal(decision.rewrittenCommand, command);
|
|
148
|
+
assert.equal(decision.reason, "no_match");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
runTest("python proxy rewrites preserve the original executable token", () => {
|
|
152
|
+
const config = cloneDefaultConfig();
|
|
153
|
+
const decision = computeRewriteDecision('python3.11 -c "print(1)"', config);
|
|
154
|
+
|
|
155
|
+
assert.equal(decision.changed, true);
|
|
156
|
+
assert.equal(decision.rule?.id, "python-proxy");
|
|
157
|
+
assert.equal(decision.rewrittenCommand, `rtk proxy ${expectedProxyExecutable("python3.11")} -c "print(1)"`);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
console.log("All command-rewriter tests passed.");
|