pi-rtk-optimizer 0.3.3 → 0.5.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 +102 -67
- package/README.md +292 -290
- package/config/config.example.json +36 -35
- package/package.json +4 -4
- package/src/additional-coverage-test.ts +278 -0
- package/src/boolean-format.ts +3 -0
- package/src/command-rewriter-test.ts +160 -120
- package/src/command-rewriter.ts +594 -585
- 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 +410 -289
- package/src/output-compactor-test.ts +500 -158
- package/src/output-compactor.ts +432 -349
- package/src/record-utils.ts +6 -0
- package/src/rewrite-bypass.ts +332 -173
- package/src/rewrite-pipeline-safety.ts +154 -0
- package/src/rewrite-rules.ts +255 -255
- package/src/rtk-command-environment.ts +64 -0
- package/src/runtime-guard-test.ts +42 -50
- package/src/runtime-guard.ts +14 -14
- package/src/techniques/build.ts +155 -155
- package/src/techniques/emoji.ts +91 -0
- package/src/techniques/git.ts +231 -229
- package/src/techniques/index.ts +10 -16
- package/src/techniques/linter.ts +151 -161
- package/src/techniques/path-utils.ts +67 -0
- package/src/techniques/rtk.ts +136 -0
- package/src/techniques/search.ts +67 -76
- package/src/techniques/source.ts +253 -253
- package/src/techniques/test-output.ts +172 -172
- package/src/test-helpers.ts +10 -0
- package/src/tool-execution-sanitizer.ts +69 -0
- package/src/types-shims.d.ts +192 -183
- package/src/types.ts +103 -114
- package/src/zellij-modal.ts +1001 -1001
- package/src/compat-commands.ts +0 -207
package/src/command-rewriter.ts
CHANGED
|
@@ -1,585 +1,594 @@
|
|
|
1
|
-
import { shouldBypassRewriteForCommand } from "./rewrite-bypass.js";
|
|
2
|
-
import { RTK_REWRITE_RULES, type RtkRewriteCategory, type RtkRewriteRule } from "./rewrite-rules.js";
|
|
3
|
-
import type { RtkIntegrationConfig } from "./types.js";
|
|
4
|
-
|
|
5
|
-
const ENV_PREFIX_PATTERN = /^((?:[A-Za-z_][A-Za-z0-9_]*=[^\s]*\s+)*)/;
|
|
6
|
-
const SHELL_ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
|
|
7
|
-
|
|
8
|
-
type CommandToken =
|
|
9
|
-
| {
|
|
10
|
-
type: "segment";
|
|
11
|
-
value: string;
|
|
12
|
-
}
|
|
13
|
-
| {
|
|
14
|
-
type: "separator";
|
|
15
|
-
value: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type SedNextTokenMode = "none" | "defaultScript" | "expressionScript" | "fileArgument" | "inPlaceArgument";
|
|
19
|
-
|
|
20
|
-
interface SegmentParseState {
|
|
21
|
-
commandName?: string;
|
|
22
|
-
sedNextTokenMode: SedNextTokenMode;
|
|
23
|
-
sedScriptSeen: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RewriteDecision {
|
|
27
|
-
changed: boolean;
|
|
28
|
-
originalCommand: string;
|
|
29
|
-
rewrittenCommand: string;
|
|
30
|
-
rule?: RtkRewriteRule;
|
|
31
|
-
reason:
|
|
32
|
-
| "ok"
|
|
33
|
-
| "empty"
|
|
34
|
-
| "already_rtk"
|
|
35
|
-
| "heredoc"
|
|
36
|
-
| "disabled_category"
|
|
37
|
-
| "no_match";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface SegmentRewriteResult {
|
|
41
|
-
value: string;
|
|
42
|
-
changed: boolean;
|
|
43
|
-
rule?: RtkRewriteRule;
|
|
44
|
-
skippedByDisabledCategory: boolean;
|
|
45
|
-
considered: boolean;
|
|
46
|
-
alreadyRtk: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface SingleSegmentRewriteResult {
|
|
50
|
-
changed: boolean;
|
|
51
|
-
rule?: RtkRewriteRule;
|
|
52
|
-
rewrittenBody?: string;
|
|
53
|
-
skippedByDisabledCategory: boolean;
|
|
54
|
-
alreadyRtk: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function categoryEnabled(config: RtkIntegrationConfig, category: RtkRewriteCategory): boolean {
|
|
58
|
-
switch (category) {
|
|
59
|
-
case "gitGithub":
|
|
60
|
-
return config.rewriteGitGithub;
|
|
61
|
-
case "filesystem":
|
|
62
|
-
return config.rewriteFilesystem;
|
|
63
|
-
case "rust":
|
|
64
|
-
return config.rewriteRust;
|
|
65
|
-
case "javascript":
|
|
66
|
-
return config.rewriteJavaScript;
|
|
67
|
-
case "python":
|
|
68
|
-
return config.rewritePython;
|
|
69
|
-
case "go":
|
|
70
|
-
return config.rewriteGo;
|
|
71
|
-
case "containers":
|
|
72
|
-
return config.rewriteContainers;
|
|
73
|
-
case "network":
|
|
74
|
-
return config.rewriteNetwork;
|
|
75
|
-
case "packageManagers":
|
|
76
|
-
return config.rewritePackageManagers;
|
|
77
|
-
default:
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function createSegmentParseState(): SegmentParseState {
|
|
83
|
-
return {
|
|
84
|
-
sedNextTokenMode: "none",
|
|
85
|
-
sedScriptSeen: false,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function normalizeShellWord(word: string): string {
|
|
90
|
-
const unwrapped = word.replace(/^(?:["'`])|(?:["'`])$/g, "");
|
|
91
|
-
const lastPathSeparator = Math.max(unwrapped.lastIndexOf("/"), unwrapped.lastIndexOf("\\"));
|
|
92
|
-
const basename = lastPathSeparator >= 0 ? unwrapped.slice(lastPathSeparator + 1) : unwrapped;
|
|
93
|
-
return basename.toLowerCase();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function shouldProtectSedWord(state: SegmentParseState): boolean {
|
|
97
|
-
return (
|
|
98
|
-
state.commandName === "sed" &&
|
|
99
|
-
!state.sedScriptSeen &&
|
|
100
|
-
(state.sedNextTokenMode === "defaultScript" ||
|
|
101
|
-
state.sedNextTokenMode === "expressionScript" ||
|
|
102
|
-
state.sedNextTokenMode === "inPlaceArgument")
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isQuotedEmptyToken(word: string): boolean {
|
|
107
|
-
return word === "''" || word === '""';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function looksLikeSedBackupExtension(word: string): boolean {
|
|
111
|
-
const normalized = word.replace(/^(?:["'`])|(?:["'`])$/g, "");
|
|
112
|
-
return normalized.startsWith(".") || normalized === "*";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function updateSegmentParseState(state: SegmentParseState, word: string): SegmentParseState {
|
|
116
|
-
if (!word) {
|
|
117
|
-
return state;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!state.commandName) {
|
|
121
|
-
if (SHELL_ENV_ASSIGNMENT_PATTERN.test(word)) {
|
|
122
|
-
return state;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const commandName = normalizeShellWord(word);
|
|
126
|
-
return {
|
|
127
|
-
commandName,
|
|
128
|
-
sedNextTokenMode: "none",
|
|
129
|
-
sedScriptSeen: false,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (state.commandName !== "sed" || state.sedScriptSeen) {
|
|
134
|
-
return state;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (state.sedNextTokenMode === "expressionScript") {
|
|
138
|
-
return {
|
|
139
|
-
...state,
|
|
140
|
-
sedNextTokenMode: "none",
|
|
141
|
-
sedScriptSeen: true,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (state.sedNextTokenMode === "fileArgument") {
|
|
146
|
-
return {
|
|
147
|
-
...state,
|
|
148
|
-
sedNextTokenMode: "none",
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (state.sedNextTokenMode === "inPlaceArgument") {
|
|
153
|
-
if (isQuotedEmptyToken(word) || looksLikeSedBackupExtension(word)) {
|
|
154
|
-
return {
|
|
155
|
-
...state,
|
|
156
|
-
sedNextTokenMode: "defaultScript",
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
...state,
|
|
162
|
-
sedNextTokenMode: "none",
|
|
163
|
-
sedScriptSeen: true,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (state.sedNextTokenMode === "defaultScript") {
|
|
168
|
-
return {
|
|
169
|
-
...state,
|
|
170
|
-
sedNextTokenMode: "none",
|
|
171
|
-
sedScriptSeen: true,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (word === "--") {
|
|
176
|
-
return {
|
|
177
|
-
...state,
|
|
178
|
-
sedNextTokenMode: "defaultScript",
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (word === "-e" || word === "--expression") {
|
|
183
|
-
return {
|
|
184
|
-
...state,
|
|
185
|
-
sedNextTokenMode: "expressionScript",
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (word.startsWith("--expression=")) {
|
|
190
|
-
return {
|
|
191
|
-
...state,
|
|
192
|
-
sedScriptSeen: true,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (/^-[A-Za-z]*e[A-Za-z]*$/.test(word)) {
|
|
197
|
-
return {
|
|
198
|
-
...state,
|
|
199
|
-
sedNextTokenMode: "expressionScript",
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (word === "-f" || word === "--file") {
|
|
204
|
-
return {
|
|
205
|
-
...state,
|
|
206
|
-
sedNextTokenMode: "fileArgument",
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (word.startsWith("--file=") || /^-f.+$/.test(word)) {
|
|
211
|
-
return state;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (word === "-i" || word === "--in-place") {
|
|
215
|
-
return {
|
|
216
|
-
...state,
|
|
217
|
-
sedNextTokenMode: "inPlaceArgument",
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (word.startsWith("--in-place=") || /^-i.+$/.test(word)) {
|
|
222
|
-
return {
|
|
223
|
-
...state,
|
|
224
|
-
sedNextTokenMode: "defaultScript",
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (word.startsWith("-")) {
|
|
229
|
-
return state;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
...state,
|
|
234
|
-
sedScriptSeen: true,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function tokenizeCommand(command: string): CommandToken[] {
|
|
239
|
-
if (!command) {
|
|
240
|
-
return [];
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const tokens: CommandToken[] = [];
|
|
244
|
-
let segmentStart = 0;
|
|
245
|
-
let segmentState = createSegmentParseState();
|
|
246
|
-
let currentWordStart: number | null = null;
|
|
247
|
-
let currentWordProtected = false;
|
|
248
|
-
let quote: "'" | '"' | "`" | null = null;
|
|
249
|
-
let escaped = false;
|
|
250
|
-
|
|
251
|
-
const finalizeWord = (endIndexExclusive: number): void => {
|
|
252
|
-
if (currentWordStart === null) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const word = command.slice(currentWordStart, endIndexExclusive);
|
|
257
|
-
segmentState = updateSegmentParseState(segmentState, word);
|
|
258
|
-
currentWordStart = null;
|
|
259
|
-
currentWordProtected = false;
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const pushSeparator = (index: number, length: number): void => {
|
|
263
|
-
finalizeWord(index);
|
|
264
|
-
const segment = command.slice(segmentStart, index);
|
|
265
|
-
if (segment.length > 0) {
|
|
266
|
-
tokens.push({ type: "segment", value: segment });
|
|
267
|
-
}
|
|
268
|
-
tokens.push({ type: "separator", value: command.slice(index, index + length) });
|
|
269
|
-
segmentStart = index + length;
|
|
270
|
-
segmentState = createSegmentParseState();
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const beginWord = (index: number): void => {
|
|
274
|
-
if (currentWordStart !== null) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
currentWordStart = index;
|
|
279
|
-
currentWordProtected = shouldProtectSedWord(segmentState);
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
283
|
-
const char = command[index];
|
|
284
|
-
const nextChar = command[index + 1] ?? "";
|
|
285
|
-
const prevChar = index > 0 ? command[index - 1] ?? "" : "";
|
|
286
|
-
|
|
287
|
-
if (escaped) {
|
|
288
|
-
escaped = false;
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (quote !== null) {
|
|
293
|
-
if (char === "\\" && quote !== "'") {
|
|
294
|
-
escaped = true;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (char === quote) {
|
|
298
|
-
quote = null;
|
|
299
|
-
}
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (char === "\\") {
|
|
304
|
-
beginWord(index);
|
|
305
|
-
escaped = true;
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (/\s/.test(char)) {
|
|
310
|
-
finalizeWord(index);
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (!currentWordProtected) {
|
|
315
|
-
if (char === "&" && nextChar === "&") {
|
|
316
|
-
pushSeparator(index, 2);
|
|
317
|
-
index += 1;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (char === "|" && nextChar === "|") {
|
|
322
|
-
pushSeparator(index, 2);
|
|
323
|
-
index += 1;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (char === "|" && nextChar === "&") {
|
|
328
|
-
pushSeparator(index, 2);
|
|
329
|
-
index += 1;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (char === "|" && prevChar !== ">") {
|
|
334
|
-
pushSeparator(index, 1);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (char === "&" && nextChar !== ">" && prevChar !== ">" && prevChar !== "<") {
|
|
339
|
-
pushSeparator(index, 1);
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (char === ";") {
|
|
344
|
-
pushSeparator(index, 1);
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
350
|
-
beginWord(index);
|
|
351
|
-
quote = char;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
beginWord(index);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
finalizeWord(command.length);
|
|
359
|
-
|
|
360
|
-
const tail = command.slice(segmentStart);
|
|
361
|
-
if (tail.length > 0 || tokens.length === 0) {
|
|
362
|
-
tokens.push({ type: "segment", value: tail });
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return tokens;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export function isAlreadyRtkCommand(command: string): boolean {
|
|
369
|
-
const trimmed = command.trimStart();
|
|
370
|
-
return /^rtk\s+/.test(trimmed) || /(?:^|\s)[^\s]*\/rtk\s+/.test(trimmed);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function applyPlatformProxyCommandFixups(command: string): string {
|
|
374
|
-
if (process.platform !== "win32") {
|
|
375
|
-
return command;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const windowsProxyExecutables: Array<[string, string]> = [
|
|
379
|
-
["npm", "npm.cmd"],
|
|
380
|
-
["npx", "npx.cmd"],
|
|
381
|
-
["pnpm", "pnpm.cmd"],
|
|
382
|
-
["yarn", "yarn.cmd"],
|
|
383
|
-
];
|
|
384
|
-
|
|
385
|
-
let next = command;
|
|
386
|
-
for (const [base, windowsExecutable] of windowsProxyExecutables) {
|
|
387
|
-
next = next.replace(
|
|
388
|
-
new RegExp(`^(rtk\\s+proxy\\s+)${base}(\\b)`, "i"),
|
|
389
|
-
`$1${windowsExecutable}$2`,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return next;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function rewriteSingleSegmentCommand(
|
|
397
|
-
segmentCommand: string,
|
|
398
|
-
config: RtkIntegrationConfig,
|
|
399
|
-
): SingleSegmentRewriteResult {
|
|
400
|
-
const envMatch = segmentCommand.match(ENV_PREFIX_PATTERN);
|
|
401
|
-
const envPrefix = envMatch?.[1] ?? "";
|
|
402
|
-
const commandBody = segmentCommand.slice(envPrefix.length);
|
|
403
|
-
|
|
404
|
-
if (isAlreadyRtkCommand(segmentCommand) || isAlreadyRtkCommand(commandBody)) {
|
|
405
|
-
return {
|
|
406
|
-
changed: false,
|
|
407
|
-
alreadyRtk: true,
|
|
408
|
-
skippedByDisabledCategory: false,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
let skippedByDisabledCategory = false;
|
|
413
|
-
|
|
414
|
-
for (const rule of RTK_REWRITE_RULES) {
|
|
415
|
-
if (!categoryEnabled(config, rule.category)) {
|
|
416
|
-
skippedByDisabledCategory = true;
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
rule.matcher.lastIndex = 0;
|
|
421
|
-
if (!rule.matcher.test(commandBody)) {
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
rule.matcher.lastIndex = 0;
|
|
426
|
-
if (shouldBypassRewriteForCommand(commandBody, rule)) {
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const rewrittenBody = commandBody.replace(rule.matcher, rule.replacement);
|
|
431
|
-
const finalizedRewrittenBody = applyPlatformProxyCommandFixups(rewrittenBody);
|
|
432
|
-
if (finalizedRewrittenBody === commandBody) {
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
changed: true,
|
|
438
|
-
rule,
|
|
439
|
-
rewrittenBody: finalizedRewrittenBody,
|
|
440
|
-
alreadyRtk: false,
|
|
441
|
-
skippedByDisabledCategory,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
changed: false,
|
|
447
|
-
alreadyRtk: false,
|
|
448
|
-
skippedByDisabledCategory,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function rewriteSegment(segment: string, config: RtkIntegrationConfig): SegmentRewriteResult {
|
|
453
|
-
const leadingWhitespace = segment.match(/^\s*/)?.[0] ?? "";
|
|
454
|
-
const trailingWhitespace = segment.match(/\s*$/)?.[0] ?? "";
|
|
455
|
-
const core = segment.trim();
|
|
456
|
-
|
|
457
|
-
if (!core) {
|
|
458
|
-
return {
|
|
459
|
-
value: segment,
|
|
460
|
-
changed: false,
|
|
461
|
-
skippedByDisabledCategory: false,
|
|
462
|
-
considered: false,
|
|
463
|
-
alreadyRtk: false,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const rewrite = rewriteSingleSegmentCommand(core, config);
|
|
468
|
-
if (!rewrite.changed || !rewrite.rule) {
|
|
469
|
-
return {
|
|
470
|
-
value: segment,
|
|
471
|
-
changed: false,
|
|
472
|
-
rule: undefined,
|
|
473
|
-
skippedByDisabledCategory: rewrite.skippedByDisabledCategory,
|
|
474
|
-
considered: true,
|
|
475
|
-
alreadyRtk: rewrite.alreadyRtk,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const envMatch = core.match(ENV_PREFIX_PATTERN);
|
|
480
|
-
const envPrefix = envMatch?.[1] ?? "";
|
|
481
|
-
const commandBody = core.slice(envPrefix.length);
|
|
482
|
-
rewrite.rule.matcher.lastIndex = 0;
|
|
483
|
-
const rewrittenBody = rewrite.rewrittenBody ?? commandBody.replace(rewrite.rule.matcher, rewrite.rule.replacement);
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
value: `${leadingWhitespace}${envPrefix}${rewrittenBody}${trailingWhitespace}`,
|
|
487
|
-
changed: true,
|
|
488
|
-
rule: rewrite.rule,
|
|
489
|
-
skippedByDisabledCategory: rewrite.skippedByDisabledCategory,
|
|
490
|
-
considered: true,
|
|
491
|
-
alreadyRtk: false,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
export function computeRewriteDecision(command: string, config: RtkIntegrationConfig): RewriteDecision {
|
|
496
|
-
const original = command;
|
|
497
|
-
const trimmed = command.trim();
|
|
498
|
-
if (!trimmed) {
|
|
499
|
-
return {
|
|
500
|
-
changed: false,
|
|
501
|
-
originalCommand: original,
|
|
502
|
-
rewrittenCommand: original,
|
|
503
|
-
reason: "empty",
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (trimmed.includes("<<")) {
|
|
508
|
-
return {
|
|
509
|
-
changed: false,
|
|
510
|
-
originalCommand: original,
|
|
511
|
-
rewrittenCommand: original,
|
|
512
|
-
reason: "heredoc",
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (result.
|
|
548
|
-
|
|
549
|
-
if (
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
reason: "
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
1
|
+
import { shouldBypassRewriteForCommand, shouldBypassWholeCommandRewrite } from "./rewrite-bypass.js";
|
|
2
|
+
import { RTK_REWRITE_RULES, type RtkRewriteCategory, type RtkRewriteRule } from "./rewrite-rules.js";
|
|
3
|
+
import type { RtkIntegrationConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const ENV_PREFIX_PATTERN = /^((?:[A-Za-z_][A-Za-z0-9_]*=[^\s]*\s+)*)/;
|
|
6
|
+
const SHELL_ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
|
|
7
|
+
|
|
8
|
+
type CommandToken =
|
|
9
|
+
| {
|
|
10
|
+
type: "segment";
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
type: "separator";
|
|
15
|
+
value: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type SedNextTokenMode = "none" | "defaultScript" | "expressionScript" | "fileArgument" | "inPlaceArgument";
|
|
19
|
+
|
|
20
|
+
interface SegmentParseState {
|
|
21
|
+
commandName?: string;
|
|
22
|
+
sedNextTokenMode: SedNextTokenMode;
|
|
23
|
+
sedScriptSeen: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RewriteDecision {
|
|
27
|
+
changed: boolean;
|
|
28
|
+
originalCommand: string;
|
|
29
|
+
rewrittenCommand: string;
|
|
30
|
+
rule?: RtkRewriteRule;
|
|
31
|
+
reason:
|
|
32
|
+
| "ok"
|
|
33
|
+
| "empty"
|
|
34
|
+
| "already_rtk"
|
|
35
|
+
| "heredoc"
|
|
36
|
+
| "disabled_category"
|
|
37
|
+
| "no_match";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SegmentRewriteResult {
|
|
41
|
+
value: string;
|
|
42
|
+
changed: boolean;
|
|
43
|
+
rule?: RtkRewriteRule;
|
|
44
|
+
skippedByDisabledCategory: boolean;
|
|
45
|
+
considered: boolean;
|
|
46
|
+
alreadyRtk: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SingleSegmentRewriteResult {
|
|
50
|
+
changed: boolean;
|
|
51
|
+
rule?: RtkRewriteRule;
|
|
52
|
+
rewrittenBody?: string;
|
|
53
|
+
skippedByDisabledCategory: boolean;
|
|
54
|
+
alreadyRtk: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function categoryEnabled(config: RtkIntegrationConfig, category: RtkRewriteCategory): boolean {
|
|
58
|
+
switch (category) {
|
|
59
|
+
case "gitGithub":
|
|
60
|
+
return config.rewriteGitGithub;
|
|
61
|
+
case "filesystem":
|
|
62
|
+
return config.rewriteFilesystem;
|
|
63
|
+
case "rust":
|
|
64
|
+
return config.rewriteRust;
|
|
65
|
+
case "javascript":
|
|
66
|
+
return config.rewriteJavaScript;
|
|
67
|
+
case "python":
|
|
68
|
+
return config.rewritePython;
|
|
69
|
+
case "go":
|
|
70
|
+
return config.rewriteGo;
|
|
71
|
+
case "containers":
|
|
72
|
+
return config.rewriteContainers;
|
|
73
|
+
case "network":
|
|
74
|
+
return config.rewriteNetwork;
|
|
75
|
+
case "packageManagers":
|
|
76
|
+
return config.rewritePackageManagers;
|
|
77
|
+
default:
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createSegmentParseState(): SegmentParseState {
|
|
83
|
+
return {
|
|
84
|
+
sedNextTokenMode: "none",
|
|
85
|
+
sedScriptSeen: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeShellWord(word: string): string {
|
|
90
|
+
const unwrapped = word.replace(/^(?:["'`])|(?:["'`])$/g, "");
|
|
91
|
+
const lastPathSeparator = Math.max(unwrapped.lastIndexOf("/"), unwrapped.lastIndexOf("\\"));
|
|
92
|
+
const basename = lastPathSeparator >= 0 ? unwrapped.slice(lastPathSeparator + 1) : unwrapped;
|
|
93
|
+
return basename.toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function shouldProtectSedWord(state: SegmentParseState): boolean {
|
|
97
|
+
return (
|
|
98
|
+
state.commandName === "sed" &&
|
|
99
|
+
!state.sedScriptSeen &&
|
|
100
|
+
(state.sedNextTokenMode === "defaultScript" ||
|
|
101
|
+
state.sedNextTokenMode === "expressionScript" ||
|
|
102
|
+
state.sedNextTokenMode === "inPlaceArgument")
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isQuotedEmptyToken(word: string): boolean {
|
|
107
|
+
return word === "''" || word === '""';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function looksLikeSedBackupExtension(word: string): boolean {
|
|
111
|
+
const normalized = word.replace(/^(?:["'`])|(?:["'`])$/g, "");
|
|
112
|
+
return normalized.startsWith(".") || normalized === "*";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function updateSegmentParseState(state: SegmentParseState, word: string): SegmentParseState {
|
|
116
|
+
if (!word) {
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!state.commandName) {
|
|
121
|
+
if (SHELL_ENV_ASSIGNMENT_PATTERN.test(word)) {
|
|
122
|
+
return state;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const commandName = normalizeShellWord(word);
|
|
126
|
+
return {
|
|
127
|
+
commandName,
|
|
128
|
+
sedNextTokenMode: "none",
|
|
129
|
+
sedScriptSeen: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (state.commandName !== "sed" || state.sedScriptSeen) {
|
|
134
|
+
return state;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (state.sedNextTokenMode === "expressionScript") {
|
|
138
|
+
return {
|
|
139
|
+
...state,
|
|
140
|
+
sedNextTokenMode: "none",
|
|
141
|
+
sedScriptSeen: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (state.sedNextTokenMode === "fileArgument") {
|
|
146
|
+
return {
|
|
147
|
+
...state,
|
|
148
|
+
sedNextTokenMode: "none",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (state.sedNextTokenMode === "inPlaceArgument") {
|
|
153
|
+
if (isQuotedEmptyToken(word) || looksLikeSedBackupExtension(word)) {
|
|
154
|
+
return {
|
|
155
|
+
...state,
|
|
156
|
+
sedNextTokenMode: "defaultScript",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...state,
|
|
162
|
+
sedNextTokenMode: "none",
|
|
163
|
+
sedScriptSeen: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (state.sedNextTokenMode === "defaultScript") {
|
|
168
|
+
return {
|
|
169
|
+
...state,
|
|
170
|
+
sedNextTokenMode: "none",
|
|
171
|
+
sedScriptSeen: true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (word === "--") {
|
|
176
|
+
return {
|
|
177
|
+
...state,
|
|
178
|
+
sedNextTokenMode: "defaultScript",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (word === "-e" || word === "--expression") {
|
|
183
|
+
return {
|
|
184
|
+
...state,
|
|
185
|
+
sedNextTokenMode: "expressionScript",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (word.startsWith("--expression=")) {
|
|
190
|
+
return {
|
|
191
|
+
...state,
|
|
192
|
+
sedScriptSeen: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (/^-[A-Za-z]*e[A-Za-z]*$/.test(word)) {
|
|
197
|
+
return {
|
|
198
|
+
...state,
|
|
199
|
+
sedNextTokenMode: "expressionScript",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (word === "-f" || word === "--file") {
|
|
204
|
+
return {
|
|
205
|
+
...state,
|
|
206
|
+
sedNextTokenMode: "fileArgument",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (word.startsWith("--file=") || /^-f.+$/.test(word)) {
|
|
211
|
+
return state;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (word === "-i" || word === "--in-place") {
|
|
215
|
+
return {
|
|
216
|
+
...state,
|
|
217
|
+
sedNextTokenMode: "inPlaceArgument",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (word.startsWith("--in-place=") || /^-i.+$/.test(word)) {
|
|
222
|
+
return {
|
|
223
|
+
...state,
|
|
224
|
+
sedNextTokenMode: "defaultScript",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (word.startsWith("-")) {
|
|
229
|
+
return state;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...state,
|
|
234
|
+
sedScriptSeen: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function tokenizeCommand(command: string): CommandToken[] {
|
|
239
|
+
if (!command) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const tokens: CommandToken[] = [];
|
|
244
|
+
let segmentStart = 0;
|
|
245
|
+
let segmentState = createSegmentParseState();
|
|
246
|
+
let currentWordStart: number | null = null;
|
|
247
|
+
let currentWordProtected = false;
|
|
248
|
+
let quote: "'" | '"' | "`" | null = null;
|
|
249
|
+
let escaped = false;
|
|
250
|
+
|
|
251
|
+
const finalizeWord = (endIndexExclusive: number): void => {
|
|
252
|
+
if (currentWordStart === null) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const word = command.slice(currentWordStart, endIndexExclusive);
|
|
257
|
+
segmentState = updateSegmentParseState(segmentState, word);
|
|
258
|
+
currentWordStart = null;
|
|
259
|
+
currentWordProtected = false;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const pushSeparator = (index: number, length: number): void => {
|
|
263
|
+
finalizeWord(index);
|
|
264
|
+
const segment = command.slice(segmentStart, index);
|
|
265
|
+
if (segment.length > 0) {
|
|
266
|
+
tokens.push({ type: "segment", value: segment });
|
|
267
|
+
}
|
|
268
|
+
tokens.push({ type: "separator", value: command.slice(index, index + length) });
|
|
269
|
+
segmentStart = index + length;
|
|
270
|
+
segmentState = createSegmentParseState();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const beginWord = (index: number): void => {
|
|
274
|
+
if (currentWordStart !== null) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
currentWordStart = index;
|
|
279
|
+
currentWordProtected = shouldProtectSedWord(segmentState);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
283
|
+
const char = command[index];
|
|
284
|
+
const nextChar = command[index + 1] ?? "";
|
|
285
|
+
const prevChar = index > 0 ? command[index - 1] ?? "" : "";
|
|
286
|
+
|
|
287
|
+
if (escaped) {
|
|
288
|
+
escaped = false;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (quote !== null) {
|
|
293
|
+
if (char === "\\" && quote !== "'") {
|
|
294
|
+
escaped = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (char === quote) {
|
|
298
|
+
quote = null;
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (char === "\\") {
|
|
304
|
+
beginWord(index);
|
|
305
|
+
escaped = true;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (/\s/.test(char)) {
|
|
310
|
+
finalizeWord(index);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!currentWordProtected) {
|
|
315
|
+
if (char === "&" && nextChar === "&") {
|
|
316
|
+
pushSeparator(index, 2);
|
|
317
|
+
index += 1;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (char === "|" && nextChar === "|") {
|
|
322
|
+
pushSeparator(index, 2);
|
|
323
|
+
index += 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (char === "|" && nextChar === "&") {
|
|
328
|
+
pushSeparator(index, 2);
|
|
329
|
+
index += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (char === "|" && prevChar !== ">") {
|
|
334
|
+
pushSeparator(index, 1);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (char === "&" && nextChar !== ">" && prevChar !== ">" && prevChar !== "<") {
|
|
339
|
+
pushSeparator(index, 1);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (char === ";") {
|
|
344
|
+
pushSeparator(index, 1);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
350
|
+
beginWord(index);
|
|
351
|
+
quote = char;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
beginWord(index);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
finalizeWord(command.length);
|
|
359
|
+
|
|
360
|
+
const tail = command.slice(segmentStart);
|
|
361
|
+
if (tail.length > 0 || tokens.length === 0) {
|
|
362
|
+
tokens.push({ type: "segment", value: tail });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return tokens;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function isAlreadyRtkCommand(command: string): boolean {
|
|
369
|
+
const trimmed = command.trimStart();
|
|
370
|
+
return /^rtk\s+/.test(trimmed) || /(?:^|\s)[^\s]*\/rtk\s+/.test(trimmed);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function applyPlatformProxyCommandFixups(command: string): string {
|
|
374
|
+
if (process.platform !== "win32") {
|
|
375
|
+
return command;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const windowsProxyExecutables: Array<[string, string]> = [
|
|
379
|
+
["npm", "npm.cmd"],
|
|
380
|
+
["npx", "npx.cmd"],
|
|
381
|
+
["pnpm", "pnpm.cmd"],
|
|
382
|
+
["yarn", "yarn.cmd"],
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
let next = command;
|
|
386
|
+
for (const [base, windowsExecutable] of windowsProxyExecutables) {
|
|
387
|
+
next = next.replace(
|
|
388
|
+
new RegExp(`^(rtk\\s+proxy\\s+)${base}(\\b)`, "i"),
|
|
389
|
+
`$1${windowsExecutable}$2`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return next;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function rewriteSingleSegmentCommand(
|
|
397
|
+
segmentCommand: string,
|
|
398
|
+
config: RtkIntegrationConfig,
|
|
399
|
+
): SingleSegmentRewriteResult {
|
|
400
|
+
const envMatch = segmentCommand.match(ENV_PREFIX_PATTERN);
|
|
401
|
+
const envPrefix = envMatch?.[1] ?? "";
|
|
402
|
+
const commandBody = segmentCommand.slice(envPrefix.length);
|
|
403
|
+
|
|
404
|
+
if (isAlreadyRtkCommand(segmentCommand) || isAlreadyRtkCommand(commandBody)) {
|
|
405
|
+
return {
|
|
406
|
+
changed: false,
|
|
407
|
+
alreadyRtk: true,
|
|
408
|
+
skippedByDisabledCategory: false,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let skippedByDisabledCategory = false;
|
|
413
|
+
|
|
414
|
+
for (const rule of RTK_REWRITE_RULES) {
|
|
415
|
+
if (!categoryEnabled(config, rule.category)) {
|
|
416
|
+
skippedByDisabledCategory = true;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
rule.matcher.lastIndex = 0;
|
|
421
|
+
if (!rule.matcher.test(commandBody)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
rule.matcher.lastIndex = 0;
|
|
426
|
+
if (shouldBypassRewriteForCommand(commandBody, rule)) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const rewrittenBody = commandBody.replace(rule.matcher, rule.replacement);
|
|
431
|
+
const finalizedRewrittenBody = applyPlatformProxyCommandFixups(rewrittenBody);
|
|
432
|
+
if (finalizedRewrittenBody === commandBody) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
changed: true,
|
|
438
|
+
rule,
|
|
439
|
+
rewrittenBody: finalizedRewrittenBody,
|
|
440
|
+
alreadyRtk: false,
|
|
441
|
+
skippedByDisabledCategory,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
changed: false,
|
|
447
|
+
alreadyRtk: false,
|
|
448
|
+
skippedByDisabledCategory,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function rewriteSegment(segment: string, config: RtkIntegrationConfig): SegmentRewriteResult {
|
|
453
|
+
const leadingWhitespace = segment.match(/^\s*/)?.[0] ?? "";
|
|
454
|
+
const trailingWhitespace = segment.match(/\s*$/)?.[0] ?? "";
|
|
455
|
+
const core = segment.trim();
|
|
456
|
+
|
|
457
|
+
if (!core) {
|
|
458
|
+
return {
|
|
459
|
+
value: segment,
|
|
460
|
+
changed: false,
|
|
461
|
+
skippedByDisabledCategory: false,
|
|
462
|
+
considered: false,
|
|
463
|
+
alreadyRtk: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const rewrite = rewriteSingleSegmentCommand(core, config);
|
|
468
|
+
if (!rewrite.changed || !rewrite.rule) {
|
|
469
|
+
return {
|
|
470
|
+
value: segment,
|
|
471
|
+
changed: false,
|
|
472
|
+
rule: undefined,
|
|
473
|
+
skippedByDisabledCategory: rewrite.skippedByDisabledCategory,
|
|
474
|
+
considered: true,
|
|
475
|
+
alreadyRtk: rewrite.alreadyRtk,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const envMatch = core.match(ENV_PREFIX_PATTERN);
|
|
480
|
+
const envPrefix = envMatch?.[1] ?? "";
|
|
481
|
+
const commandBody = core.slice(envPrefix.length);
|
|
482
|
+
rewrite.rule.matcher.lastIndex = 0;
|
|
483
|
+
const rewrittenBody = rewrite.rewrittenBody ?? commandBody.replace(rewrite.rule.matcher, rewrite.rule.replacement);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
value: `${leadingWhitespace}${envPrefix}${rewrittenBody}${trailingWhitespace}`,
|
|
487
|
+
changed: true,
|
|
488
|
+
rule: rewrite.rule,
|
|
489
|
+
skippedByDisabledCategory: rewrite.skippedByDisabledCategory,
|
|
490
|
+
considered: true,
|
|
491
|
+
alreadyRtk: false,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function computeRewriteDecision(command: string, config: RtkIntegrationConfig): RewriteDecision {
|
|
496
|
+
const original = command;
|
|
497
|
+
const trimmed = command.trim();
|
|
498
|
+
if (!trimmed) {
|
|
499
|
+
return {
|
|
500
|
+
changed: false,
|
|
501
|
+
originalCommand: original,
|
|
502
|
+
rewrittenCommand: original,
|
|
503
|
+
reason: "empty",
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (trimmed.includes("<<")) {
|
|
508
|
+
return {
|
|
509
|
+
changed: false,
|
|
510
|
+
originalCommand: original,
|
|
511
|
+
rewrittenCommand: original,
|
|
512
|
+
reason: "heredoc",
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!isAlreadyRtkCommand(command) && shouldBypassWholeCommandRewrite(command)) {
|
|
517
|
+
return {
|
|
518
|
+
changed: false,
|
|
519
|
+
originalCommand: original,
|
|
520
|
+
rewrittenCommand: original,
|
|
521
|
+
reason: "no_match",
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const tokens = tokenizeCommand(command);
|
|
526
|
+
if (tokens.length === 0) {
|
|
527
|
+
return {
|
|
528
|
+
changed: false,
|
|
529
|
+
originalCommand: original,
|
|
530
|
+
rewrittenCommand: original,
|
|
531
|
+
reason: "no_match",
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let changed = false;
|
|
536
|
+
let skippedByDisabledCategory = false;
|
|
537
|
+
let firstRule: RtkRewriteRule | undefined;
|
|
538
|
+
let consideredSegments = 0;
|
|
539
|
+
let alreadyRtkSegments = 0;
|
|
540
|
+
|
|
541
|
+
const rewrittenTokens = tokens.map((token) => {
|
|
542
|
+
if (token.type === "separator") {
|
|
543
|
+
return token;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const result = rewriteSegment(token.value, config);
|
|
547
|
+
if (result.considered) {
|
|
548
|
+
consideredSegments += 1;
|
|
549
|
+
if (result.alreadyRtk) {
|
|
550
|
+
alreadyRtkSegments += 1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (result.skippedByDisabledCategory) {
|
|
554
|
+
skippedByDisabledCategory = true;
|
|
555
|
+
}
|
|
556
|
+
if (result.changed) {
|
|
557
|
+
changed = true;
|
|
558
|
+
if (!firstRule) {
|
|
559
|
+
firstRule = result.rule;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
type: "segment" as const,
|
|
565
|
+
value: result.value,
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
if (changed) {
|
|
570
|
+
return {
|
|
571
|
+
changed: true,
|
|
572
|
+
originalCommand: original,
|
|
573
|
+
rewrittenCommand: rewrittenTokens.map((token) => token.value).join(""),
|
|
574
|
+
rule: firstRule,
|
|
575
|
+
reason: "ok",
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (consideredSegments > 0 && consideredSegments === alreadyRtkSegments) {
|
|
580
|
+
return {
|
|
581
|
+
changed: false,
|
|
582
|
+
originalCommand: original,
|
|
583
|
+
rewrittenCommand: original,
|
|
584
|
+
reason: "already_rtk",
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
changed: false,
|
|
590
|
+
originalCommand: original,
|
|
591
|
+
rewrittenCommand: original,
|
|
592
|
+
reason: skippedByDisabledCategory ? "disabled_category" : "no_match",
|
|
593
|
+
};
|
|
594
|
+
}
|