pi-rtk-optimizer 0.6.0 → 0.7.1

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.
@@ -140,6 +140,10 @@ function shouldPreserveExactReadOutput(
140
140
  input: Record<string, unknown>,
141
141
  config: RtkIntegrationConfig,
142
142
  ): boolean {
143
+ if (!config.outputCompaction.readCompaction.enabled) {
144
+ return true;
145
+ }
146
+
143
147
  if (hasExplicitReadRange(input)) {
144
148
  return true;
145
149
  }
@@ -186,6 +190,10 @@ function hasLossyCompaction(techniques: string[]): boolean {
186
190
  );
187
191
  }
188
192
 
193
+ function normalizeTechniqueResult(result: string | null, currentText: string): string {
194
+ return result === null ? currentText : result;
195
+ }
196
+
189
197
  function compactBashText(
190
198
  text: string,
191
199
  command: string | undefined,
@@ -203,45 +211,45 @@ function compactBashText(
203
211
  }
204
212
  }
205
213
 
206
- const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
207
- if (withoutRtkHookWarnings !== null && withoutRtkHookWarnings !== nextText) {
214
+ const withoutRtkHookWarnings = normalizeTechniqueResult(stripRtkHookWarnings(nextText, command), nextText);
215
+ if (withoutRtkHookWarnings !== nextText) {
208
216
  nextText = withoutRtkHookWarnings;
209
217
  techniques.push("rtk-hook-warning");
210
218
  }
211
219
 
212
- const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
213
- if (withoutRtkEmoji !== null && withoutRtkEmoji !== nextText) {
220
+ const withoutRtkEmoji = normalizeTechniqueResult(sanitizeRtkEmojiOutput(nextText, command), nextText);
221
+ if (withoutRtkEmoji !== nextText) {
214
222
  nextText = withoutRtkEmoji;
215
223
  techniques.push("rtk-emoji");
216
224
  }
217
225
 
218
226
  if (compaction.filterBuildOutput) {
219
- const compacted = filterBuildOutput(nextText, command);
220
- if (compacted !== null && compacted !== nextText) {
227
+ const compacted = normalizeTechniqueResult(filterBuildOutput(nextText, command), nextText);
228
+ if (compacted !== nextText) {
221
229
  nextText = compacted;
222
230
  techniques.push("build");
223
231
  }
224
232
  }
225
233
 
226
234
  if (compaction.aggregateTestOutput) {
227
- const compacted = aggregateTestOutput(nextText, command);
228
- if (compacted !== null && compacted !== nextText) {
235
+ const compacted = normalizeTechniqueResult(aggregateTestOutput(nextText, command), nextText);
236
+ if (compacted !== nextText) {
229
237
  nextText = compacted;
230
238
  techniques.push("test");
231
239
  }
232
240
  }
233
241
 
234
242
  if (compaction.compactGitOutput) {
235
- const compacted = compactGitOutput(nextText, command);
236
- if (compacted !== null && compacted !== nextText) {
243
+ const compacted = normalizeTechniqueResult(compactGitOutput(nextText, command), nextText);
244
+ if (compacted !== nextText) {
237
245
  nextText = compacted;
238
246
  techniques.push("git");
239
247
  }
240
248
  }
241
249
 
242
250
  if (compaction.aggregateLinterOutput) {
243
- const compacted = aggregateLinterOutput(nextText, command);
244
- if (compacted !== null && compacted !== nextText) {
251
+ const compacted = normalizeTechniqueResult(aggregateLinterOutput(nextText, command), nextText);
252
+ if (compacted !== nextText) {
245
253
  nextText = compacted;
246
254
  techniques.push("linter");
247
255
  }
@@ -284,7 +292,10 @@ function compactReadText(
284
292
  compaction.sourceCodeFiltering !== "none" &&
285
293
  shouldApplyReadSourceFiltering(text, config)
286
294
  ) {
287
- const filtered = filterSourceCode(nextText, language, compaction.sourceCodeFiltering);
295
+ const filtered = normalizeTechniqueResult(
296
+ filterSourceCode(nextText, language, compaction.sourceCodeFiltering),
297
+ nextText,
298
+ );
288
299
  if (filtered !== nextText) {
289
300
  nextText = filtered;
290
301
  techniques.push(`source:${compaction.sourceCodeFiltering}`);
@@ -328,8 +339,8 @@ function compactGrepText(text: string, config: RtkIntegrationConfig): { text: st
328
339
  }
329
340
 
330
341
  if (compaction.groupSearchOutput) {
331
- const grouped = groupSearchResults(nextText);
332
- if (grouped !== null && grouped !== nextText) {
342
+ const grouped = normalizeTechniqueResult(groupSearchResults(nextText), nextText);
343
+ if (grouped !== nextText) {
333
344
  nextText = grouped;
334
345
  techniques.push("search");
335
346
  }
@@ -1,157 +1,203 @@
1
- import { splitLeadingEnvAssignments } from "./shell-env-prefix.js";
2
-
3
- interface ParsedPipeline {
4
- segments: string[];
5
- separators: string[];
6
- }
7
-
8
- interface ProducerRewritePlan {
9
- command: string;
10
- captureStderr: boolean;
11
- }
12
-
13
- function isTopLevelQuoteCharacter(character: string): character is '"' | "'" | "`" {
14
- return character === '"' || character === "'" || character === "`";
15
- }
16
-
17
- function parseSimpleTopLevelPipeline(command: string): ParsedPipeline | null {
18
- const segments: string[] = [];
19
- const separators: string[] = [];
20
- let quote: '"' | "'" | "`" | null = null;
21
- let escaped = false;
22
- let segmentStart = 0;
23
-
24
- for (let index = 0; index < command.length; index += 1) {
25
- const character = command[index] ?? "";
26
- const nextCharacter = command[index + 1] ?? "";
27
- const previousCharacter = index > 0 ? (command[index - 1] ?? "") : "";
28
-
29
- if (escaped) {
30
- escaped = false;
31
- continue;
32
- }
33
-
34
- if (quote !== null) {
35
- if (character === "\\" && quote !== "'") {
36
- escaped = true;
37
- continue;
38
- }
39
- if (character === quote) {
40
- quote = null;
41
- }
42
- continue;
43
- }
44
-
45
- if (character === "\\") {
46
- escaped = true;
47
- continue;
48
- }
49
-
50
- if (isTopLevelQuoteCharacter(character)) {
51
- quote = character;
52
- continue;
53
- }
54
-
55
- if (character === "|" && nextCharacter === "|") {
56
- return null;
57
- }
58
-
59
- if (character === "|" && previousCharacter !== ">") {
60
- const separatorLength = nextCharacter === "&" ? 2 : 1;
61
- segments.push(command.slice(segmentStart, index));
62
- separators.push(command.slice(index, index + separatorLength));
63
- segmentStart = index + separatorLength;
64
- if (separatorLength === 2) {
65
- index += 1;
66
- }
67
- continue;
68
- }
69
-
70
- if (character === "&" && nextCharacter === "&") {
71
- return null;
72
- }
73
-
74
- if (character === "&" && nextCharacter !== ">" && previousCharacter !== ">" && previousCharacter !== "<") {
75
- return null;
76
- }
77
-
78
- if (character === ";") {
79
- return null;
80
- }
81
- }
82
-
83
- if (separators.length === 0) {
84
- return null;
85
- }
86
-
87
- segments.push(command.slice(segmentStart));
88
- return { segments, separators };
89
- }
90
-
91
- function extractProducerRewritePlan(segment: string, firstSeparator: string): ProducerRewritePlan | null {
92
- const trimmed = segment.trim();
93
- const { envPrefix, command: commandWithOptionalRedirect } = splitLeadingEnvAssignments(trimmed);
94
- if (!/^rtk\s+/i.test(commandWithOptionalRedirect)) {
95
- return null;
96
- }
97
-
98
- const stderrMergeMatch = commandWithOptionalRedirect.match(/^(.*?)(?:\s+)?2>\s*&1\s*$/u);
99
- if (stderrMergeMatch) {
100
- const command = stderrMergeMatch[1]?.trimEnd() ?? "";
101
- return command ? { command: `${envPrefix}${command}`.trim(), captureStderr: true } : null;
102
- }
103
-
104
- return {
105
- command: `${envPrefix}${commandWithOptionalRedirect}`.trim(),
106
- captureStderr: firstSeparator === "|&",
107
- };
108
- }
109
-
110
- function buildBufferedPipelineCommand(
111
- producer: ProducerRewritePlan,
112
- remainder: string,
113
- ): string {
114
- const tempFileVariable = "__pi_rtk_pipe_tmp";
115
- const statusVariable = "__pi_rtk_pipe_status";
116
- const producerRedirect = producer.captureStderr ? `> "$${tempFileVariable}" 2>&1` : `> "$${tempFileVariable}"`;
117
- const cleanupTrap = `rm -f "$${tempFileVariable}"`;
118
-
119
- return [
120
- "{",
121
- `${tempFileVariable}="$(mktemp)" || exit $?;`,
122
- `${statusVariable}=0;`,
123
- `trap '${cleanupTrap}' EXIT HUP INT TERM;`,
124
- `${producer.command} ${producerRedirect};`,
125
- `${statusVariable}=$?;`,
126
- `if [ $${statusVariable} -eq 0 ]; then (${remainder}) < "$${tempFileVariable}"; ${statusVariable}=$?; fi;`,
127
- `exit $${statusVariable};`,
128
- "}",
129
- ].join(" ");
130
- }
131
-
132
- export function applyRewrittenCommandShellSafetyFixups(command: string): string {
133
- if (process.platform !== "win32") {
134
- return command;
135
- }
136
-
137
- const parsedPipeline = parseSimpleTopLevelPipeline(command);
138
- if (!parsedPipeline) {
139
- return command;
140
- }
141
-
142
- const producer = extractProducerRewritePlan(parsedPipeline.segments[0] ?? "", parsedPipeline.separators[0] ?? "");
143
- if (!producer) {
144
- return command;
145
- }
146
-
147
- const remainder = parsedPipeline.segments
148
- .slice(1)
149
- .map((segment, index) => `${index === 0 ? "" : (parsedPipeline.separators[index] ?? "")}${segment}`)
150
- .join("")
151
- .trim();
152
- if (!remainder) {
153
- return command;
154
- }
155
-
156
- return buildBufferedPipelineCommand(producer, remainder);
157
- }
1
+ import { splitLeadingEnvAssignments } from "./shell-env-prefix.js";
2
+
3
+ interface ParsedPipeline {
4
+ segments: string[];
5
+ separators: string[];
6
+ suffix: string;
7
+ }
8
+
9
+ interface ProducerRewritePlan {
10
+ command: string;
11
+ captureStderr: boolean;
12
+ }
13
+
14
+ interface ShellSafetyTarget {
15
+ environmentPrelude: string;
16
+ command: string;
17
+ }
18
+
19
+ const SINGLE_QUOTED_SHELL_VALUE_PATTERN = "'(?:'\\\\''|[^'])*'";
20
+ const SHELL_ENV_VALUE_PATTERN = `(?:"(?:\\\\.|[^"])*"|${SINGLE_QUOTED_SHELL_VALUE_PATTERN}|[^\\s;]+)`;
21
+ const LEADING_RTK_DB_PATH_EXPORT_PRELUDE_PATTERN = new RegExp(
22
+ `^(\\s*export\\s+RTK_DB_PATH=${SHELL_ENV_VALUE_PATTERN}\\s*;\\s*)([\\s\\S]*)$`,
23
+ "u",
24
+ );
25
+
26
+ function splitLeadingRtkDbPathExportPrelude(command: string): ShellSafetyTarget {
27
+ const match = command.match(LEADING_RTK_DB_PATH_EXPORT_PRELUDE_PATTERN);
28
+ if (!match) {
29
+ return { environmentPrelude: "", command };
30
+ }
31
+
32
+ return {
33
+ environmentPrelude: match[1] ?? "",
34
+ command: match[2] ?? "",
35
+ };
36
+ }
37
+
38
+ function isTopLevelQuoteCharacter(character: string): character is '"' | "'" | "`" {
39
+ return character === '"' || character === "'" || character === "`";
40
+ }
41
+
42
+ function parseSimpleTopLevelPipeline(command: string): ParsedPipeline | null {
43
+ const segments: string[] = [];
44
+ const separators: string[] = [];
45
+ let quote: '"' | "'" | "`" | null = null;
46
+ let escaped = false;
47
+ let segmentStart = 0;
48
+ let suffix = "";
49
+
50
+ for (let index = 0; index < command.length; index += 1) {
51
+ const character = command[index] ?? "";
52
+ const nextCharacter = command[index + 1] ?? "";
53
+ const previousCharacter = index > 0 ? (command[index - 1] ?? "") : "";
54
+
55
+ if (escaped) {
56
+ escaped = false;
57
+ continue;
58
+ }
59
+
60
+ if (quote !== null) {
61
+ if (character === "\\" && quote !== "'") {
62
+ escaped = true;
63
+ continue;
64
+ }
65
+ if (character === quote) {
66
+ quote = null;
67
+ }
68
+ continue;
69
+ }
70
+
71
+ if (character === "\\") {
72
+ escaped = true;
73
+ continue;
74
+ }
75
+
76
+ if (isTopLevelQuoteCharacter(character)) {
77
+ quote = character;
78
+ continue;
79
+ }
80
+
81
+ if (character === "|" && nextCharacter === "|") {
82
+ if (separators.length === 0) {
83
+ return null;
84
+ }
85
+ segments.push(command.slice(segmentStart, index));
86
+ suffix = command.slice(index);
87
+ break;
88
+ }
89
+
90
+ if (character === "|" && previousCharacter !== ">") {
91
+ const separatorLength = nextCharacter === "&" ? 2 : 1;
92
+ segments.push(command.slice(segmentStart, index));
93
+ separators.push(command.slice(index, index + separatorLength));
94
+ segmentStart = index + separatorLength;
95
+ if (separatorLength === 2) {
96
+ index += 1;
97
+ }
98
+ continue;
99
+ }
100
+
101
+ if (character === "&" && nextCharacter === "&") {
102
+ if (separators.length === 0) {
103
+ return null;
104
+ }
105
+ segments.push(command.slice(segmentStart, index));
106
+ suffix = command.slice(index);
107
+ break;
108
+ }
109
+
110
+ if (character === "&" && nextCharacter !== ">" && previousCharacter !== ">" && previousCharacter !== "<") {
111
+ return null;
112
+ }
113
+
114
+ if (character === ";") {
115
+ if (separators.length === 0) {
116
+ return null;
117
+ }
118
+ segments.push(command.slice(segmentStart, index));
119
+ suffix = command.slice(index);
120
+ break;
121
+ }
122
+ }
123
+
124
+ if (separators.length === 0) {
125
+ return null;
126
+ }
127
+
128
+ if (!suffix) {
129
+ segments.push(command.slice(segmentStart));
130
+ }
131
+
132
+ return { segments, separators, suffix };
133
+ }
134
+
135
+ function extractProducerRewritePlan(segment: string, firstSeparator: string): ProducerRewritePlan | null {
136
+ const trimmed = segment.trim();
137
+ const { envPrefix, command: commandWithOptionalRedirect } = splitLeadingEnvAssignments(trimmed);
138
+ if (!/^rtk\s+/i.test(commandWithOptionalRedirect)) {
139
+ return null;
140
+ }
141
+
142
+ const stderrMergeMatch = commandWithOptionalRedirect.match(/^(.*?)(?:\s+)?2>\s*&1\s*$/u);
143
+ if (stderrMergeMatch) {
144
+ const command = stderrMergeMatch[1]?.trimEnd() ?? "";
145
+ return command ? { command: `${envPrefix}${command}`.trim(), captureStderr: true } : null;
146
+ }
147
+
148
+ return {
149
+ command: `${envPrefix}${commandWithOptionalRedirect}`.trim(),
150
+ captureStderr: firstSeparator === "|&",
151
+ };
152
+ }
153
+
154
+ function buildBufferedPipelineCommand(
155
+ producer: ProducerRewritePlan,
156
+ remainder: string,
157
+ ): string {
158
+ const tempFileVariable = "__pi_rtk_pipe_tmp";
159
+ const statusVariable = "__pi_rtk_pipe_status";
160
+ const producerRedirect = producer.captureStderr ? `> "$${tempFileVariable}" 2>&1` : `> "$${tempFileVariable}"`;
161
+ const cleanupTrap = `rm -f "$${tempFileVariable}"`;
162
+
163
+ return [
164
+ "{",
165
+ `${tempFileVariable}="$(mktemp)" || exit $?;`,
166
+ `${statusVariable}=0;`,
167
+ `trap '${cleanupTrap}' EXIT HUP INT TERM;`,
168
+ `${producer.command} ${producerRedirect};`,
169
+ `${statusVariable}=$?;`,
170
+ `if [ $${statusVariable} -eq 0 ]; then (${remainder}) < "$${tempFileVariable}"; ${statusVariable}=$?; fi;`,
171
+ `exit $${statusVariable};`,
172
+ "}",
173
+ ].join(" ");
174
+ }
175
+
176
+ export function applyRewrittenCommandShellSafetyFixups(command: string, platform: string = process.platform): string {
177
+ if (platform !== "win32") {
178
+ return command;
179
+ }
180
+
181
+ const target = splitLeadingRtkDbPathExportPrelude(command);
182
+ const parsedPipeline = parseSimpleTopLevelPipeline(target.command);
183
+ if (!parsedPipeline) {
184
+ return command;
185
+ }
186
+
187
+ const producer = extractProducerRewritePlan(parsedPipeline.segments[0] ?? "", parsedPipeline.separators[0] ?? "");
188
+ if (!producer) {
189
+ return command;
190
+ }
191
+
192
+ const remainder = parsedPipeline.segments
193
+ .slice(1)
194
+ .map((segment, index) => `${index === 0 ? "" : (parsedPipeline.separators[index] ?? "")}${segment}`)
195
+ .join("")
196
+ .trim();
197
+ if (!remainder) {
198
+ return command;
199
+ }
200
+
201
+ const suffix = parsedPipeline.suffix ? ` ${parsedPipeline.suffix.trimStart()}` : "";
202
+ return `${target.environmentPrelude}${buildBufferedPipelineCommand(producer, remainder)}${suffix}`;
203
+ }
@@ -3,7 +3,12 @@ import { join } from "node:path";
3
3
  import { splitLeadingEnvAssignments } from "./shell-env-prefix.js";
4
4
 
5
5
  const RTK_DB_PATH_ENV_NAME = "RTK_DB_PATH";
6
- const RTK_DB_PATH_ASSIGNMENT_PATTERN = /(?:^|\s)RTK_DB_PATH=(?:"[^"]*"|'[^']*'|[^\s]+)(?=\s|$)/;
6
+ const SINGLE_QUOTED_SHELL_VALUE_PATTERN = "'(?:'\\\\''|[^'])*'";
7
+ const SHELL_ENV_VALUE_PATTERN = `(?:"[^"]*"|${SINGLE_QUOTED_SHELL_VALUE_PATTERN}|[^\\s;]+)`;
8
+ const RTK_DB_PATH_ASSIGNMENT_PATTERN = new RegExp(
9
+ `(?:^|\\s)RTK_DB_PATH=${SHELL_ENV_VALUE_PATTERN}(?=\\s|$)`,
10
+ );
11
+ const RTK_DB_PATH_EXPORT_PATTERN = new RegExp(`^export\\s+RTK_DB_PATH=${SHELL_ENV_VALUE_PATTERN}(?=\\s*(?:;|$))`);
7
12
 
8
13
  function resolveTemporaryDirectory(): string {
9
14
  if (process.platform === "win32") {
@@ -44,11 +49,15 @@ function getTemporaryRtkHistoryDbPath(): string {
44
49
 
45
50
  function quoteForShellEnv(value: string): string {
46
51
  const normalizedValue = process.platform === "win32" ? value.replace(/\\/g, "/") : value;
47
- return `"${normalizedValue.replace(/"/g, '\\"')}"`;
52
+ return `'${normalizedValue.replace(/'/g, `'\\''`)}'`;
48
53
  }
49
54
 
50
55
  function hasLeadingRtkDbPathAssignment(command: string): boolean {
51
- return RTK_DB_PATH_ASSIGNMENT_PATTERN.test(splitLeadingEnvAssignments(command).envPrefix);
56
+ const trimmed = command.trimStart();
57
+ return (
58
+ RTK_DB_PATH_ASSIGNMENT_PATTERN.test(splitLeadingEnvAssignments(trimmed).envPrefix) ||
59
+ RTK_DB_PATH_EXPORT_PATTERN.test(trimmed)
60
+ );
52
61
  }
53
62
 
54
63
  export function applyRtkCommandEnvironment(command: string): string {
@@ -60,5 +69,5 @@ export function applyRtkCommandEnvironment(command: string): string {
60
69
  return command;
61
70
  }
62
71
 
63
- return `${RTK_DB_PATH_ENV_NAME}=${quoteForShellEnv(getTemporaryRtkHistoryDbPath())} ${command}`;
72
+ return `export ${RTK_DB_PATH_ENV_NAME}=${quoteForShellEnv(getTemporaryRtkHistoryDbPath())}; ${command}`;
64
73
  }
@@ -0,0 +1,97 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ export type RtkExecutableResolverName = "where" | "which";
4
+
5
+ export interface RtkExecutableResolution {
6
+ command: string;
7
+ resolvedPath?: string;
8
+ resolver: RtkExecutableResolverName;
9
+ warning?: string;
10
+ }
11
+
12
+ interface ResolverCommand {
13
+ command: RtkExecutableResolverName;
14
+ args: string[];
15
+ }
16
+
17
+ export interface ResolveRtkExecutableOptions {
18
+ platform?: typeof process.platform;
19
+ timeoutMs?: number;
20
+ }
21
+
22
+ function trimResolutionDetail(value: string | undefined): string {
23
+ return (value ?? "").replace(/\s+/g, " ").trim();
24
+ }
25
+
26
+ function stripWrappingQuotes(value: string): string {
27
+ if (value.length < 2) {
28
+ return value;
29
+ }
30
+
31
+ const first = value[0];
32
+ const last = value[value.length - 1];
33
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
34
+ return value.slice(1, -1);
35
+ }
36
+
37
+ return value;
38
+ }
39
+
40
+ export function parseRtkExecutablePath(stdout: string): string | undefined {
41
+ for (const line of stdout.split(/\r?\n/)) {
42
+ const candidate = stripWrappingQuotes(line.trim());
43
+ if (candidate) {
44
+ return candidate;
45
+ }
46
+ }
47
+
48
+ return undefined;
49
+ }
50
+
51
+ function getResolverCommand(platform: typeof process.platform): ResolverCommand {
52
+ if (platform === "win32") {
53
+ return { command: "where", args: ["rtk"] };
54
+ }
55
+
56
+ return { command: "which", args: ["rtk"] };
57
+ }
58
+
59
+ function fallbackResolution(resolver: RtkExecutableResolverName, warning: string): RtkExecutableResolution {
60
+ return {
61
+ command: "rtk",
62
+ resolver,
63
+ warning,
64
+ };
65
+ }
66
+
67
+ export async function resolveRtkExecutable(
68
+ pi: ExtensionAPI,
69
+ options: ResolveRtkExecutableOptions = {},
70
+ ): Promise<RtkExecutableResolution> {
71
+ const resolver = getResolverCommand(options.platform ?? process.platform);
72
+ const timeout = options.timeoutMs ?? 1000;
73
+
74
+ try {
75
+ const result = await pi.exec(resolver.command, resolver.args, { timeout });
76
+ const resolvedPath = parseRtkExecutablePath(result.stdout ?? "");
77
+ if (result.code === 0 && resolvedPath) {
78
+ return {
79
+ command: resolvedPath,
80
+ resolvedPath,
81
+ resolver: resolver.command,
82
+ };
83
+ }
84
+
85
+ const detail = trimResolutionDetail(result.stderr || result.stdout || `exit ${result.code}`);
86
+ return fallbackResolution(
87
+ resolver.command,
88
+ `rtk executable path resolution via ${resolver.command} failed${detail ? `: ${detail}` : ""}`,
89
+ );
90
+ } catch (error) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ return fallbackResolution(
93
+ resolver.command,
94
+ `rtk executable path resolution via ${resolver.command} failed: ${trimResolutionDetail(message)}`,
95
+ );
96
+ }
97
+ }