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.
Files changed (38) hide show
  1. package/CHANGELOG.md +102 -67
  2. package/README.md +292 -290
  3. package/config/config.example.json +36 -35
  4. package/package.json +4 -4
  5. package/src/additional-coverage-test.ts +278 -0
  6. package/src/boolean-format.ts +3 -0
  7. package/src/command-rewriter-test.ts +160 -120
  8. package/src/command-rewriter.ts +594 -585
  9. package/src/config-modal-test.ts +168 -0
  10. package/src/config-modal.ts +613 -600
  11. package/src/config-store.ts +224 -217
  12. package/src/index-test.ts +54 -0
  13. package/src/index.ts +410 -289
  14. package/src/output-compactor-test.ts +500 -158
  15. package/src/output-compactor.ts +432 -349
  16. package/src/record-utils.ts +6 -0
  17. package/src/rewrite-bypass.ts +332 -173
  18. package/src/rewrite-pipeline-safety.ts +154 -0
  19. package/src/rewrite-rules.ts +255 -255
  20. package/src/rtk-command-environment.ts +64 -0
  21. package/src/runtime-guard-test.ts +42 -50
  22. package/src/runtime-guard.ts +14 -14
  23. package/src/techniques/build.ts +155 -155
  24. package/src/techniques/emoji.ts +91 -0
  25. package/src/techniques/git.ts +231 -229
  26. package/src/techniques/index.ts +10 -16
  27. package/src/techniques/linter.ts +151 -161
  28. package/src/techniques/path-utils.ts +67 -0
  29. package/src/techniques/rtk.ts +136 -0
  30. package/src/techniques/search.ts +67 -76
  31. package/src/techniques/source.ts +253 -253
  32. package/src/techniques/test-output.ts +172 -172
  33. package/src/test-helpers.ts +10 -0
  34. package/src/tool-execution-sanitizer.ts +69 -0
  35. package/src/types-shims.d.ts +192 -183
  36. package/src/types.ts +103 -114
  37. package/src/zellij-modal.ts +1001 -1001
  38. package/src/compat-commands.ts +0 -207
@@ -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
- const tokens = tokenizeCommand(command);
517
- if (tokens.length === 0) {
518
- return {
519
- changed: false,
520
- originalCommand: original,
521
- rewrittenCommand: original,
522
- reason: "no_match",
523
- };
524
- }
525
-
526
- let changed = false;
527
- let skippedByDisabledCategory = false;
528
- let firstRule: RtkRewriteRule | undefined;
529
- let consideredSegments = 0;
530
- let alreadyRtkSegments = 0;
531
-
532
- const rewrittenTokens = tokens.map((token) => {
533
- if (token.type === "separator") {
534
- return token;
535
- }
536
-
537
- const result = rewriteSegment(token.value, config);
538
- if (result.considered) {
539
- consideredSegments += 1;
540
- if (result.alreadyRtk) {
541
- alreadyRtkSegments += 1;
542
- }
543
- }
544
- if (result.skippedByDisabledCategory) {
545
- skippedByDisabledCategory = true;
546
- }
547
- if (result.changed) {
548
- changed = true;
549
- if (!firstRule) {
550
- firstRule = result.rule;
551
- }
552
- }
553
-
554
- return {
555
- type: "segment" as const,
556
- value: result.value,
557
- };
558
- });
559
-
560
- if (changed) {
561
- return {
562
- changed: true,
563
- originalCommand: original,
564
- rewrittenCommand: rewrittenTokens.map((token) => token.value).join(""),
565
- rule: firstRule,
566
- reason: "ok",
567
- };
568
- }
569
-
570
- if (consideredSegments > 0 && consideredSegments === alreadyRtkSegments) {
571
- return {
572
- changed: false,
573
- originalCommand: original,
574
- rewrittenCommand: original,
575
- reason: "already_rtk",
576
- };
577
- }
578
-
579
- return {
580
- changed: false,
581
- originalCommand: original,
582
- rewrittenCommand: original,
583
- reason: skippedByDisabledCategory ? "disabled_category" : "no_match",
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
+ }