pi-rtk-optimizer 0.5.5 → 0.7.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 +17 -0
- package/README.md +20 -41
- package/config/config.example.json +6 -12
- package/package.json +64 -64
- package/src/additional-coverage-test.ts +48 -6
- 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 +17 -105
- package/src/config-store.ts +32 -29
- package/src/index-test.ts +31 -2
- package/src/index.ts +8 -7
- package/src/output-compactor-test.ts +45 -2
- package/src/output-compactor.ts +4 -0
- package/src/rewrite-pipeline-safety.ts +23 -2
- package/src/rtk-command-environment.ts +69 -64
- 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 +9 -21
- package/src/rewrite-bypass.ts +0 -332
- package/src/rewrite-rules.ts +0 -255
package/src/command-rewriter.ts
CHANGED
|
@@ -1,594 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import type { RtkIntegrationConfig } from "./types.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
}
|
|
1
|
+
import { resolveRtkRewrite } from "./rtk-rewrite-provider.js";
|
|
2
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { RtkIntegrationConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export interface RewriteDecision {
|
|
6
|
+
changed: boolean;
|
|
7
|
+
originalCommand: string;
|
|
8
|
+
rewrittenCommand: string;
|
|
9
|
+
reason: "ok" | "empty" | "already_rtk" | "no_match";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function computeRewriteDecision(
|
|
13
|
+
command: string,
|
|
14
|
+
_config: RtkIntegrationConfig,
|
|
15
|
+
pi: ExtensionAPI,
|
|
16
|
+
): Promise<RewriteDecision> {
|
|
17
|
+
if (!command || !command.trim()) {
|
|
18
|
+
return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "empty" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const trimmedStart = command.trimStart();
|
|
22
|
+
if (trimmedStart === "rtk" || trimmedStart.startsWith("rtk ")) {
|
|
23
|
+
return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "already_rtk" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = await resolveRtkRewrite(pi, command);
|
|
27
|
+
|
|
28
|
+
if (result.changed && result.rewrittenCommand) {
|
|
29
|
+
return {
|
|
30
|
+
changed: true,
|
|
31
|
+
originalCommand: command,
|
|
32
|
+
rewrittenCommand: result.rewrittenCommand,
|
|
33
|
+
reason: "ok",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
changed: false,
|
|
39
|
+
originalCommand: command,
|
|
40
|
+
rewrittenCommand: command,
|
|
41
|
+
reason: "no_match",
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/config-modal-test.ts
CHANGED
|
@@ -137,6 +137,8 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
|
|
|
137
137
|
|
|
138
138
|
await definition?.handler("show", infoCtx.ctx);
|
|
139
139
|
assert.ok(lastNotification(infoCtx.notifications).message.includes("mode=rewrite"));
|
|
140
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("rewriteSource=rtk"));
|
|
141
|
+
assert.equal(lastNotification(infoCtx.notifications).message.includes("categories="), false);
|
|
140
142
|
|
|
141
143
|
await definition?.handler("path", infoCtx.ctx);
|
|
142
144
|
assert.equal(lastNotification(infoCtx.notifications).message, "rtk config: C:/tmp/pi-rtk-optimizer/config.json");
|