pi-rtk-optimizer 0.7.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.
- package/CHANGELOG.md +16 -1
- package/README.md +8 -5
- package/package.json +11 -8
- package/src/additional-coverage-test.ts +116 -57
- package/src/command-rewriter-test.ts +187 -118
- package/src/command-rewriter.ts +5 -2
- package/src/config-modal-test.ts +95 -29
- package/src/config-modal.ts +13 -3
- package/src/index-test.ts +196 -1
- package/src/index.ts +48 -4
- package/src/output-compactor.ts +22 -15
- package/src/rewrite-pipeline-safety.ts +203 -178
- package/src/rtk-command-environment.ts +73 -69
- package/src/rtk-executable-resolver.ts +97 -0
- package/src/rtk-rewrite-provider.ts +39 -3
- package/src/shell-env-prefix.ts +5 -1
- package/src/test-helpers.ts +23 -10
- package/src/tool-execution-sanitizer.ts +80 -69
- package/src/types.ts +4 -0
- package/src/windows-command-helpers.ts +92 -16
- package/src/zellij-modal.ts +1 -1
|
@@ -1,118 +1,187 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
|
|
3
|
-
import { computeRewriteDecision } from "./command-rewriter.ts";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
assert.equal(
|
|
73
|
-
assert.equal(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
assert.equal(decision.
|
|
89
|
-
assert.equal(decision.reason, "
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
runTest("
|
|
93
|
-
const config = cloneDefaultConfig();
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
assert.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import { computeRewriteDecision } from "./command-rewriter.ts";
|
|
4
|
+
import { resolveRtkRewrite } from "./rtk-rewrite-provider.ts";
|
|
5
|
+
import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
|
|
6
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
function createMockPi(execResult: { code: number; stdout?: string; stderr?: string }): ExtensionAPI {
|
|
9
|
+
return {
|
|
10
|
+
exec: async (command: string) => {
|
|
11
|
+
if (command === "which" || command === "where") {
|
|
12
|
+
return { code: 0, stdout: "/usr/local/bin/rtk\n", stderr: "" };
|
|
13
|
+
}
|
|
14
|
+
return execResult;
|
|
15
|
+
},
|
|
16
|
+
} as unknown as ExtensionAPI;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await runTest("rtk rewrite uses resolved POSIX executable path", async () => {
|
|
20
|
+
const calls: Array<{ command: string; args: string[] }> = [];
|
|
21
|
+
const pi = {
|
|
22
|
+
exec: async (command: string, args: string[]) => {
|
|
23
|
+
calls.push({ command, args });
|
|
24
|
+
if (command === "which") {
|
|
25
|
+
return { code: 0, stdout: "/opt/rtk/bin/rtk\n", stderr: "" };
|
|
26
|
+
}
|
|
27
|
+
return { code: 3, stdout: "rtk git status", stderr: "" };
|
|
28
|
+
},
|
|
29
|
+
} as unknown as ExtensionAPI;
|
|
30
|
+
|
|
31
|
+
const result = await resolveRtkRewrite(pi, "git status", { platform: "linux" });
|
|
32
|
+
|
|
33
|
+
assert.equal(result.changed, true);
|
|
34
|
+
assert.equal(result.rewrittenCommand, "rtk git status");
|
|
35
|
+
assert.equal(result.executableResolution?.resolvedPath, "/opt/rtk/bin/rtk");
|
|
36
|
+
assert.deepEqual(calls.map((call) => call.command), ["which", "/opt/rtk/bin/rtk"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await runTest("rtk rewrite uses resolved Windows executable path", async () => {
|
|
40
|
+
const calls: Array<{ command: string; args: string[] }> = [];
|
|
41
|
+
const pi = {
|
|
42
|
+
exec: async (command: string, args: string[]) => {
|
|
43
|
+
calls.push({ command, args });
|
|
44
|
+
if (command === "where") {
|
|
45
|
+
return { code: 0, stdout: "C:\\Tools\\rtk.exe\r\nC:\\Other\\rtk.exe\r\n", stderr: "" };
|
|
46
|
+
}
|
|
47
|
+
return { code: 3, stdout: "rtk git status", stderr: "" };
|
|
48
|
+
},
|
|
49
|
+
} as unknown as ExtensionAPI;
|
|
50
|
+
|
|
51
|
+
const result = await resolveRtkRewrite(pi, "git status", { platform: "win32" });
|
|
52
|
+
|
|
53
|
+
assert.equal(result.changed, true);
|
|
54
|
+
assert.equal(result.executableResolution?.resolvedPath, "C:\\Tools\\rtk.exe");
|
|
55
|
+
assert.deepEqual(calls.map((call) => call.command), ["where", "C:\\Tools\\rtk.exe"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await runTest("rtk rewrite preserves behavior when executable path resolution fails", async () => {
|
|
59
|
+
const calls: Array<{ command: string; args: string[] }> = [];
|
|
60
|
+
const pi = {
|
|
61
|
+
exec: async (command: string, args: string[]) => {
|
|
62
|
+
calls.push({ command, args });
|
|
63
|
+
if (command === "which") {
|
|
64
|
+
return { code: 1, stdout: "", stderr: "not found" };
|
|
65
|
+
}
|
|
66
|
+
return { code: 3, stdout: "rtk git status", stderr: "" };
|
|
67
|
+
},
|
|
68
|
+
} as unknown as ExtensionAPI;
|
|
69
|
+
|
|
70
|
+
const result = await resolveRtkRewrite(pi, "git status", { platform: "linux" });
|
|
71
|
+
|
|
72
|
+
assert.equal(result.changed, true);
|
|
73
|
+
assert.equal(result.executableResolution?.command, "rtk");
|
|
74
|
+
assert.ok(result.executableResolution?.warning?.includes("which failed"));
|
|
75
|
+
assert.deepEqual(calls.map((call) => call.command), ["which", "rtk"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await runTest("empty command unchanged", async () => {
|
|
79
|
+
const config = cloneDefaultConfig();
|
|
80
|
+
const decision = await computeRewriteDecision("", config, createMockPi({ code: 1 }));
|
|
81
|
+
assert.equal(decision.changed, false);
|
|
82
|
+
assert.equal(decision.reason, "empty");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await runTest("already rtk unchanged", async () => {
|
|
86
|
+
const config = cloneDefaultConfig();
|
|
87
|
+
const decision = await computeRewriteDecision("rtk status", config, createMockPi({ code: 1 }));
|
|
88
|
+
assert.equal(decision.changed, false);
|
|
89
|
+
assert.equal(decision.reason, "already_rtk");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await runTest("rtk unsupported heredoc result leaves command unchanged", async () => {
|
|
93
|
+
const config = cloneDefaultConfig();
|
|
94
|
+
const decision = await computeRewriteDecision("cat <<EOF", config, createMockPi({ code: 1 }));
|
|
95
|
+
assert.equal(decision.changed, false);
|
|
96
|
+
assert.equal(decision.reason, "no_match");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await runTest("quoted heredoc marker is delegated to RTK rewrite", async () => {
|
|
100
|
+
const config = cloneDefaultConfig();
|
|
101
|
+
const command = 'echo "<<not heredoc" && git status';
|
|
102
|
+
const decision = await computeRewriteDecision(
|
|
103
|
+
command,
|
|
104
|
+
config,
|
|
105
|
+
createMockPi({ code: 3, stdout: 'echo "<<not heredoc" && rtk git status' }),
|
|
106
|
+
);
|
|
107
|
+
assert.equal(decision.changed, true);
|
|
108
|
+
assert.equal(decision.rewrittenCommand, 'echo "<<not heredoc" && rtk git status');
|
|
109
|
+
assert.equal(decision.reason, "ok");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await runTest("legacy category toggles do not pre-filter RTK rewrite source of truth", async () => {
|
|
113
|
+
const config = { ...cloneDefaultConfig(), rewriteGitGithub: false };
|
|
114
|
+
const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
|
|
115
|
+
assert.equal(decision.changed, true);
|
|
116
|
+
assert.equal(decision.rewrittenCommand, "rtk git status");
|
|
117
|
+
assert.equal(decision.reason, "ok");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await runTest("rtk exit 0 rewrites", async () => {
|
|
121
|
+
const config = cloneDefaultConfig();
|
|
122
|
+
const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 0, stdout: "rtk git status" }));
|
|
123
|
+
assert.equal(decision.changed, true);
|
|
124
|
+
assert.equal(decision.rewrittenCommand, "rtk git status");
|
|
125
|
+
assert.equal(decision.reason, "ok");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await runTest("rtk exit 3 rewrites", async () => {
|
|
129
|
+
const config = cloneDefaultConfig();
|
|
130
|
+
const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
|
|
131
|
+
assert.equal(decision.changed, true);
|
|
132
|
+
assert.equal(decision.rewrittenCommand, "rtk git status");
|
|
133
|
+
assert.equal(decision.reason, "ok");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await runTest("exit 1 leaves unchanged", async () => {
|
|
137
|
+
const config = cloneDefaultConfig();
|
|
138
|
+
const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 1 }));
|
|
139
|
+
assert.equal(decision.changed, false);
|
|
140
|
+
assert.equal(decision.reason, "no_match");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await runTest("exit 2 leaves unchanged and surfaces RTK detail", async () => {
|
|
144
|
+
const config = cloneDefaultConfig();
|
|
145
|
+
const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 2, stderr: "denied" }));
|
|
146
|
+
assert.equal(decision.changed, false);
|
|
147
|
+
assert.equal(decision.reason, "no_match");
|
|
148
|
+
assert.equal(decision.warning, "denied");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await runTest("unknown category passes through to RTK", async () => {
|
|
152
|
+
const config = cloneDefaultConfig();
|
|
153
|
+
const pi = createMockPi({ code: 0, stdout: "rtk custom" });
|
|
154
|
+
const decision = await computeRewriteDecision("custom-cmd", config, pi);
|
|
155
|
+
assert.equal(decision.changed, true);
|
|
156
|
+
assert.equal(decision.rewrittenCommand, "rtk custom");
|
|
157
|
+
assert.equal(decision.reason, "ok");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await runTest("exec error/timeout leaves unchanged and surfaces error detail", async () => {
|
|
161
|
+
const config = cloneDefaultConfig();
|
|
162
|
+
const pi = {
|
|
163
|
+
exec: async () => {
|
|
164
|
+
throw new Error("timeout");
|
|
165
|
+
},
|
|
166
|
+
} as unknown as ExtensionAPI;
|
|
167
|
+
const decision = await computeRewriteDecision("git status", config, pi);
|
|
168
|
+
assert.equal(decision.changed, false);
|
|
169
|
+
assert.equal(decision.reason, "no_match");
|
|
170
|
+
assert.equal(decision.warning, "timeout");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await runTest("compound commands forwarded to RTK", async () => {
|
|
174
|
+
const config = cloneDefaultConfig();
|
|
175
|
+
let capturedArgs: string[] = [];
|
|
176
|
+
const pi = {
|
|
177
|
+
exec: async (_cmd: string, args: string[]) => {
|
|
178
|
+
capturedArgs = args;
|
|
179
|
+
return { code: 0, stdout: "rtk result" };
|
|
180
|
+
},
|
|
181
|
+
} as unknown as ExtensionAPI;
|
|
182
|
+
const decision = await computeRewriteDecision("git status && cargo test", config, pi);
|
|
183
|
+
assert.equal(decision.changed, true);
|
|
184
|
+
assert.deepEqual(capturedArgs, ["rewrite", "git status && cargo test"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
console.log("All command-rewriter tests passed.");
|
package/src/command-rewriter.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveRtkRewrite } from "./rtk-rewrite-provider.js";
|
|
1
|
+
import { resolveRtkRewrite, type RtkRewriteProviderOptions } from "./rtk-rewrite-provider.js";
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import type { RtkIntegrationConfig } from "./types.js";
|
|
4
4
|
|
|
@@ -7,12 +7,14 @@ export interface RewriteDecision {
|
|
|
7
7
|
originalCommand: string;
|
|
8
8
|
rewrittenCommand: string;
|
|
9
9
|
reason: "ok" | "empty" | "already_rtk" | "no_match";
|
|
10
|
+
warning?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export async function computeRewriteDecision(
|
|
13
14
|
command: string,
|
|
14
15
|
_config: RtkIntegrationConfig,
|
|
15
16
|
pi: ExtensionAPI,
|
|
17
|
+
rewriteOptions: RtkRewriteProviderOptions = {},
|
|
16
18
|
): Promise<RewriteDecision> {
|
|
17
19
|
if (!command || !command.trim()) {
|
|
18
20
|
return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "empty" };
|
|
@@ -23,7 +25,7 @@ export async function computeRewriteDecision(
|
|
|
23
25
|
return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "already_rtk" };
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const result = await resolveRtkRewrite(pi, command);
|
|
28
|
+
const result = await resolveRtkRewrite(pi, command, rewriteOptions);
|
|
27
29
|
|
|
28
30
|
if (result.changed && result.rewrittenCommand) {
|
|
29
31
|
return {
|
|
@@ -39,5 +41,6 @@ export async function computeRewriteDecision(
|
|
|
39
41
|
originalCommand: command,
|
|
40
42
|
rewrittenCommand: command,
|
|
41
43
|
reason: "no_match",
|
|
44
|
+
warning: result.error,
|
|
42
45
|
};
|
|
43
46
|
}
|
package/src/config-modal-test.ts
CHANGED
|
@@ -8,26 +8,40 @@ mock.module("@mariozechner/pi-coding-agent", () => ({
|
|
|
8
8
|
getSettingsListTheme: () => ({}),
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
|
+
const settingsListInputs: string[] = [];
|
|
12
|
+
const settingsListUpdates: Array<{ id: string; value: string }> = [];
|
|
13
|
+
|
|
11
14
|
mock.module("@mariozechner/pi-tui", () => ({
|
|
12
|
-
Box: class {
|
|
15
|
+
Box: class {
|
|
16
|
+
addChild(): void {}
|
|
17
|
+
},
|
|
13
18
|
Container: class {
|
|
14
19
|
addChild(): void {}
|
|
15
20
|
render(): string[] {
|
|
16
|
-
return [];
|
|
21
|
+
return ["settings-content"];
|
|
17
22
|
}
|
|
18
23
|
invalidate(): void {}
|
|
19
24
|
},
|
|
20
25
|
SettingsList: class {
|
|
21
|
-
handleInput(): void {
|
|
22
|
-
|
|
26
|
+
handleInput(data: string): void {
|
|
27
|
+
settingsListInputs.push(data);
|
|
28
|
+
}
|
|
29
|
+
updateValue(id: string, value: string): void {
|
|
30
|
+
settingsListUpdates.push({ id, value });
|
|
31
|
+
}
|
|
23
32
|
},
|
|
24
33
|
Spacer: class {},
|
|
25
34
|
Text: class {},
|
|
26
|
-
truncateToWidth: (text: string) => text,
|
|
35
|
+
truncateToWidth: (text: string, width: number) => text.slice(0, width),
|
|
27
36
|
visibleWidth: (text: string) => text.length,
|
|
28
37
|
}));
|
|
29
38
|
|
|
39
|
+
function stripAnsi(text: string): string {
|
|
40
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
const { registerRtkIntegrationCommand } = await import("./config-modal.ts");
|
|
44
|
+
const { ZellijModal, ZellijSettingsModal } = await import("./zellij-modal.ts");
|
|
31
45
|
const { getRtkArgumentCompletions } = await import("./command-completions.ts");
|
|
32
46
|
|
|
33
47
|
type Notification = { message: string; level: "info" | "warning" | "error" };
|
|
@@ -62,6 +76,60 @@ function lastNotification(notifications: Notification[]): Notification {
|
|
|
62
76
|
return notifications[notifications.length - 1] as Notification;
|
|
63
77
|
}
|
|
64
78
|
|
|
79
|
+
function createThemeStub(): { fg: (_name: string, text: string) => string; bold: (text: string) => string } {
|
|
80
|
+
return {
|
|
81
|
+
fg: (_name: string, text: string) => text,
|
|
82
|
+
bold: (text: string) => text,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
runTest("zellij settings modal renders overlay frame and delegates non-enter input", () => {
|
|
87
|
+
settingsListInputs.length = 0;
|
|
88
|
+
settingsListUpdates.length = 0;
|
|
89
|
+
const settingsModal = new ZellijSettingsModal(
|
|
90
|
+
{
|
|
91
|
+
title: "RTK Integration Settings",
|
|
92
|
+
settings: [
|
|
93
|
+
{
|
|
94
|
+
id: "enabled",
|
|
95
|
+
label: "Enabled",
|
|
96
|
+
description: "Enable integration",
|
|
97
|
+
currentValue: "on",
|
|
98
|
+
values: ["on", "off"],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
onChange: () => {},
|
|
102
|
+
onClose: () => {},
|
|
103
|
+
helpText: "Esc: close",
|
|
104
|
+
},
|
|
105
|
+
createThemeStub() as never,
|
|
106
|
+
);
|
|
107
|
+
const modal = new ZellijModal(settingsModal, {
|
|
108
|
+
titleBar: {
|
|
109
|
+
left: { text: "RTK Integration Settings", maxWidth: 30, color: "accent" },
|
|
110
|
+
right: { text: "pi-rtk-optimizer", maxWidth: 20, color: "dim" },
|
|
111
|
+
},
|
|
112
|
+
helpUndertitle: { text: "Esc: close", color: "dim" },
|
|
113
|
+
overlay: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const rendered = modal.renderModal(86);
|
|
117
|
+
settingsModal.handleInput("\r");
|
|
118
|
+
settingsModal.handleInput("j");
|
|
119
|
+
settingsModal.updateValue("enabled", "off");
|
|
120
|
+
|
|
121
|
+
assert.equal(rendered.visibleWidth, 86);
|
|
122
|
+
assert.equal(rendered.contentWidth, 82);
|
|
123
|
+
assert.ok(stripAnsi(rendered.lines[0] ?? "").includes("RTK Integration Settings"));
|
|
124
|
+
assert.ok(stripAnsi(rendered.lines[rendered.lines.length - 1] ?? "").includes("Esc: close"));
|
|
125
|
+
assert.deepEqual(modal.getOverlayOptions(), {
|
|
126
|
+
overlay: true,
|
|
127
|
+
overlayOptions: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
|
|
128
|
+
});
|
|
129
|
+
assert.deepEqual(settingsListInputs, ["j"]);
|
|
130
|
+
assert.deepEqual(settingsListUpdates, [{ id: "enabled", value: "off" }]);
|
|
131
|
+
});
|
|
132
|
+
|
|
65
133
|
runTest("command completions return top-level and filtered RTK subcommands", () => {
|
|
66
134
|
const topLevel = getRtkArgumentCompletions("");
|
|
67
135
|
assert.ok(Array.isArray(topLevel));
|
|
@@ -77,12 +145,7 @@ runTest("command completions return top-level and filtered RTK subcommands", ()
|
|
|
77
145
|
assert.equal(getRtkArgumentCompletions("zzz"), null);
|
|
78
146
|
});
|
|
79
147
|
|
|
80
|
-
|
|
81
|
-
await testFn();
|
|
82
|
-
console.log(`[PASS] ${name}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
await runAsyncTest("config modal command handlers route RTK subcommands to controller actions", async () => {
|
|
148
|
+
await runTest("config modal command handlers route RTK subcommands to controller actions", async () => {
|
|
86
149
|
const config = cloneDefaultConfig();
|
|
87
150
|
const controllerState = {
|
|
88
151
|
config,
|
|
@@ -101,7 +164,7 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
|
|
|
101
164
|
getRuntimeStatus: () => ({ rtkAvailable: false, lastError: "not found" }),
|
|
102
165
|
refreshRuntimeStatus: async () => {
|
|
103
166
|
controllerState.refreshed += 1;
|
|
104
|
-
return { rtkAvailable:
|
|
167
|
+
return { rtkAvailable: true, rtkExecutablePath: "C:/Tools/rtk.exe" };
|
|
105
168
|
},
|
|
106
169
|
getMetricsSummary: () => "metrics summary",
|
|
107
170
|
clearMetrics: () => {
|
|
@@ -110,15 +173,16 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
|
|
|
110
173
|
};
|
|
111
174
|
|
|
112
175
|
let registeredName = "";
|
|
113
|
-
|
|
176
|
+
type RegisteredCommandDefinition = {
|
|
114
177
|
description: string;
|
|
115
178
|
getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
|
|
116
179
|
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
|
117
|
-
}
|
|
180
|
+
};
|
|
181
|
+
let definition: RegisteredCommandDefinition | undefined;
|
|
118
182
|
|
|
119
183
|
registerRtkIntegrationCommand(
|
|
120
184
|
{
|
|
121
|
-
registerCommand(name: string, nextDefinition:
|
|
185
|
+
registerCommand(name: string, nextDefinition: RegisteredCommandDefinition) {
|
|
122
186
|
registeredName = name;
|
|
123
187
|
definition = nextDefinition;
|
|
124
188
|
},
|
|
@@ -127,44 +191,46 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
|
|
|
127
191
|
);
|
|
128
192
|
|
|
129
193
|
assert.equal(registeredName, "rtk");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
if (!definition) {
|
|
195
|
+
throw new Error("Expected /rtk command definition to be registered");
|
|
196
|
+
}
|
|
197
|
+
assert.ok(definition.description.includes("Configure RTK rewrite"));
|
|
198
|
+
assert.ok(typeof definition.getArgumentCompletions === "function");
|
|
133
199
|
|
|
134
200
|
const infoCtx = createNotifyContext(true);
|
|
135
|
-
await definition
|
|
201
|
+
await definition.handler("help", infoCtx.ctx);
|
|
136
202
|
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
|
|
137
203
|
|
|
138
|
-
await definition
|
|
204
|
+
await definition.handler("show", infoCtx.ctx);
|
|
139
205
|
assert.ok(lastNotification(infoCtx.notifications).message.includes("mode=rewrite"));
|
|
140
206
|
assert.ok(lastNotification(infoCtx.notifications).message.includes("rewriteSource=rtk"));
|
|
141
207
|
assert.equal(lastNotification(infoCtx.notifications).message.includes("categories="), false);
|
|
142
208
|
|
|
143
|
-
await definition
|
|
209
|
+
await definition.handler("path", infoCtx.ctx);
|
|
144
210
|
assert.equal(lastNotification(infoCtx.notifications).message, "rtk config: C:/tmp/pi-rtk-optimizer/config.json");
|
|
145
211
|
|
|
146
|
-
await definition
|
|
212
|
+
await definition.handler("verify", infoCtx.ctx);
|
|
147
213
|
assert.equal(controllerState.refreshed, 1);
|
|
148
|
-
assert.equal(lastNotification(infoCtx.notifications).level, "
|
|
149
|
-
assert.ok(lastNotification(infoCtx.notifications).message.includes("
|
|
214
|
+
assert.equal(lastNotification(infoCtx.notifications).level, "info");
|
|
215
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("available at C:/Tools/rtk.exe"));
|
|
150
216
|
|
|
151
|
-
await definition
|
|
217
|
+
await definition.handler("stats", infoCtx.ctx);
|
|
152
218
|
assert.equal(lastNotification(infoCtx.notifications).message, "metrics summary");
|
|
153
219
|
|
|
154
|
-
await definition
|
|
220
|
+
await definition.handler("clear-stats", infoCtx.ctx);
|
|
155
221
|
assert.equal(controllerState.cleared, 1);
|
|
156
222
|
assert.equal(lastNotification(infoCtx.notifications).message, "RTK metrics cleared.");
|
|
157
223
|
|
|
158
|
-
await definition
|
|
224
|
+
await definition.handler("reset", infoCtx.ctx);
|
|
159
225
|
assert.equal(controllerState.lastSavedMode, "rewrite");
|
|
160
226
|
assert.equal(lastNotification(infoCtx.notifications).message, "RTK integration settings reset to defaults.");
|
|
161
227
|
|
|
162
|
-
await definition
|
|
228
|
+
await definition.handler("unknown", infoCtx.ctx);
|
|
163
229
|
assert.equal(lastNotification(infoCtx.notifications).level, "warning");
|
|
164
230
|
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
|
|
165
231
|
|
|
166
232
|
const headlessCtx = createNotifyContext(false);
|
|
167
|
-
await definition
|
|
233
|
+
await definition.handler("", headlessCtx.ctx);
|
|
168
234
|
assert.equal(lastNotification(headlessCtx.notifications).message, "/rtk requires interactive TUI mode.");
|
|
169
235
|
});
|
|
170
236
|
|
package/src/config-modal.ts
CHANGED
|
@@ -53,12 +53,21 @@ function parseIntegerInRange(value: string, min: number, max: number): number |
|
|
|
53
53
|
return parsed;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function
|
|
56
|
+
function summarizeRuntimeStatus(runtimeStatus: RuntimeStatus): string {
|
|
57
57
|
const runtime = runtimeStatus.rtkAvailable
|
|
58
58
|
? "rtk=available"
|
|
59
59
|
: `rtk=missing${runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : ""}`;
|
|
60
|
+
const executable = runtimeStatus.rtkExecutablePath
|
|
61
|
+
? `, rtkPath=${runtimeStatus.rtkExecutablePath}`
|
|
62
|
+
: runtimeStatus.rtkExecutableResolutionWarning
|
|
63
|
+
? `, rtkPath=unresolved (${runtimeStatus.rtkExecutableResolutionWarning})`
|
|
64
|
+
: "";
|
|
65
|
+
|
|
66
|
+
return `${runtime}${executable}`;
|
|
67
|
+
}
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
function summarizeConfig(config: RtkIntegrationConfig, runtimeStatus: RuntimeStatus): string {
|
|
70
|
+
return `enabled=${config.enabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, readCompaction=${config.outputCompaction.readCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${summarizeRuntimeStatus(runtimeStatus)}`;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
|
|
@@ -474,7 +483,8 @@ async function handleArgs(
|
|
|
474
483
|
if (normalized === "verify") {
|
|
475
484
|
const runtimeStatus = await controller.refreshRuntimeStatus();
|
|
476
485
|
if (runtimeStatus.rtkAvailable) {
|
|
477
|
-
|
|
486
|
+
const pathDetail = runtimeStatus.rtkExecutablePath ? ` at ${runtimeStatus.rtkExecutablePath}` : "";
|
|
487
|
+
ctx.ui.notify(`RTK binary is available${pathDetail}.`, "info");
|
|
478
488
|
} else {
|
|
479
489
|
ctx.ui.notify(
|
|
480
490
|
`RTK binary is not available${runtimeStatus.lastError ? `: ${runtimeStatus.lastError}` : ""}.`,
|