pi-soly 1.9.3 → 1.11.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/ask/index.ts +12 -11
- package/ask/picker.ts +356 -76
- package/ask/prompt.ts +6 -2
- package/ask/tests/picker.test.ts +273 -82
- package/codemap.ts +276 -0
- package/hotreload.ts +239 -0
- package/init.ts +302 -0
- package/mcp/CHANGELOG.md +384 -0
- package/mcp/LICENSE +21 -0
- package/mcp/OAUTH.md +355 -0
- package/mcp/README.md +410 -0
- package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
- package/mcp/__tests__/cli.upstream-test.ts +97 -0
- package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
- package/mcp/__tests__/config.upstream-test.ts +303 -0
- package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
- package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
- package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
- package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
- package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
- package/mcp/__tests__/errors.upstream-test.ts +218 -0
- package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
- package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
- package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
- package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
- package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
- package/mcp/__tests__/logger.upstream-test.ts +175 -0
- package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
- package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
- package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
- package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
- package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
- package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
- package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
- package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
- package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
- package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
- package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
- package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
- package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
- package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
- package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
- package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
- package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
- package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
- package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
- package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
- package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
- package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
- package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
- package/mcp/agent-dir.ts +20 -0
- package/mcp/app-bridge.bundle.js +67 -0
- package/mcp/cli.js +184 -0
- package/mcp/commands.ts +422 -0
- package/mcp/config.ts +666 -0
- package/mcp/consent-manager.ts +64 -0
- package/mcp/direct-tools.ts +439 -0
- package/mcp/elicitation-handler.ts +347 -0
- package/mcp/errors.ts +219 -0
- package/mcp/glimpse-ui.ts +80 -0
- package/mcp/host-html-template.ts +427 -0
- package/mcp/index.ts +362 -0
- package/mcp/init.ts +362 -0
- package/mcp/lifecycle.ts +93 -0
- package/mcp/logger.ts +169 -0
- package/mcp/mcp-auth-flow.ts +559 -0
- package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
- package/mcp/mcp-auth.ts +302 -0
- package/mcp/mcp-auth.upstream-test.ts +373 -0
- package/mcp/mcp-callback-server.ts +372 -0
- package/mcp/mcp-callback-server.upstream-test.ts +416 -0
- package/mcp/mcp-oauth-provider.ts +369 -0
- package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
- package/mcp/mcp-panel.ts +829 -0
- package/mcp/mcp-setup-panel.ts +580 -0
- package/mcp/metadata-cache.ts +201 -0
- package/mcp/notify.ts +111 -0
- package/mcp/npx-resolver.ts +424 -0
- package/mcp/oauth-handler.ts +57 -0
- package/mcp/onboarding-state.ts +68 -0
- package/mcp/package.json +106 -0
- package/mcp/panel-keys.ts +37 -0
- package/mcp/proxy-modes.ts +949 -0
- package/mcp/resource-tools.ts +17 -0
- package/mcp/sampling-handler.ts +268 -0
- package/mcp/server-manager.ts +545 -0
- package/mcp/state.ts +41 -0
- package/mcp/tool-metadata.ts +216 -0
- package/mcp/tool-registrar.ts +46 -0
- package/mcp/tool-result-renderer.ts +161 -0
- package/mcp/types.ts +448 -0
- package/mcp/ui-resource-handler.ts +146 -0
- package/mcp/ui-server.ts +623 -0
- package/mcp/ui-session.ts +386 -0
- package/mcp/ui-stream-types.ts +89 -0
- package/mcp/utils.ts +129 -0
- package/mcp/vitest.config.ts +14 -0
- package/migrate.ts +258 -0
- package/notification.ts +218 -0
- package/notifications-log.ts +83 -0
- package/package.json +20 -3
- package/status.ts +140 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMcpPanel } from "../mcp-panel.ts";
|
|
3
|
+
import { computeServerHash, type MetadataCache } from "../metadata-cache.ts";
|
|
4
|
+
import type { McpConfig } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
function stripAnsi(input: string): string {
|
|
7
|
+
return input.replace(/\x1b\[[0-9;]*m/g, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("mcp-panel excludeTools", () => {
|
|
11
|
+
it("hides excluded tools from the panel view", () => {
|
|
12
|
+
const config: McpConfig = {
|
|
13
|
+
settings: { toolPrefix: "server" },
|
|
14
|
+
mcpServers: {
|
|
15
|
+
figma: {
|
|
16
|
+
command: "npx",
|
|
17
|
+
args: ["-y", "figma"],
|
|
18
|
+
directTools: true,
|
|
19
|
+
excludeTools: ["figma_get_screenshot", "get_figjam"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const cache: MetadataCache = {
|
|
25
|
+
version: 1,
|
|
26
|
+
servers: {
|
|
27
|
+
figma: {
|
|
28
|
+
configHash: computeServerHash(config.mcpServers.figma),
|
|
29
|
+
cachedAt: Date.now(),
|
|
30
|
+
tools: [
|
|
31
|
+
{ name: "get_screenshot", description: "Screenshot" },
|
|
32
|
+
{ name: "get_nodes", description: "Nodes" },
|
|
33
|
+
],
|
|
34
|
+
resources: [
|
|
35
|
+
{ name: "figjam", uri: "ui://figjam", description: "FigJam" },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const panel = createMcpPanel(
|
|
42
|
+
config,
|
|
43
|
+
cache,
|
|
44
|
+
new Map(),
|
|
45
|
+
{
|
|
46
|
+
reconnect: async () => true,
|
|
47
|
+
canAuthenticate: () => false,
|
|
48
|
+
authenticate: async () => ({ ok: false }),
|
|
49
|
+
getConnectionStatus: () => "idle",
|
|
50
|
+
refreshCacheAfterReconnect: () => null,
|
|
51
|
+
},
|
|
52
|
+
{ requestRender: () => {} },
|
|
53
|
+
() => {},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
panel.handleInput("g");
|
|
57
|
+
panel.handleInput("e");
|
|
58
|
+
panel.handleInput("t");
|
|
59
|
+
panel.handleInput("_");
|
|
60
|
+
|
|
61
|
+
const output = stripAnsi(panel.render(120).join("\n"));
|
|
62
|
+
|
|
63
|
+
expect(output).toContain("get_nodes");
|
|
64
|
+
expect(output).not.toContain("get_screenshot");
|
|
65
|
+
expect(output).not.toContain("get_figjam");
|
|
66
|
+
|
|
67
|
+
panel.dispose();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { KeybindingsManager, TUI_KEYBINDINGS } from "@earendil-works/pi-tui";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createMcpPanel } from "../mcp-panel.ts";
|
|
4
|
+
import { createMcpSetupPanel, type SetupPanelCallbacks } from "../mcp-setup-panel.ts";
|
|
5
|
+
import { createPanelKeys } from "../panel-keys.ts";
|
|
6
|
+
import type { McpDiscoverySummary } from "../config.ts";
|
|
7
|
+
import type { McpConfig, McpPanelCallbacks } from "../types.ts";
|
|
8
|
+
|
|
9
|
+
const CTRL_P = "\x10";
|
|
10
|
+
const CTRL_N = "\x0e";
|
|
11
|
+
const UP = "\x1b[A";
|
|
12
|
+
const DOWN = "\x1b[B";
|
|
13
|
+
const ENTER = "\r";
|
|
14
|
+
|
|
15
|
+
function createEmacsKeybindings(): KeybindingsManager {
|
|
16
|
+
return new KeybindingsManager(TUI_KEYBINDINGS, {
|
|
17
|
+
"tui.select.up": ["up", "ctrl+p"],
|
|
18
|
+
"tui.select.down": ["down", "ctrl+n"],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createTwoServerConfig(): McpConfig {
|
|
23
|
+
return {
|
|
24
|
+
mcpServers: {
|
|
25
|
+
alpha: { url: "https://alpha.example.com/mcp", auth: "oauth" },
|
|
26
|
+
beta: { url: "https://beta.example.com/mcp", auth: "oauth" },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createAuthCallbacks(): McpPanelCallbacks {
|
|
32
|
+
return {
|
|
33
|
+
reconnect: async () => true,
|
|
34
|
+
canAuthenticate: () => true,
|
|
35
|
+
authenticate: vi.fn(async () => ({ ok: true })),
|
|
36
|
+
getConnectionStatus: () => "needs-auth",
|
|
37
|
+
refreshCacheAfterReconnect: () => null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createEmptyDiscovery(): McpDiscoverySummary {
|
|
42
|
+
return {
|
|
43
|
+
sources: [],
|
|
44
|
+
imports: [],
|
|
45
|
+
hasAnyConfig: false,
|
|
46
|
+
hasAnyDetectedPaths: false,
|
|
47
|
+
hasSharedServers: false,
|
|
48
|
+
hasPiOwnedServers: false,
|
|
49
|
+
totalServerCount: 0,
|
|
50
|
+
fingerprint: "test",
|
|
51
|
+
repoPrompt: { configured: false },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createSetupCallbacks(): SetupPanelCallbacks {
|
|
56
|
+
const preview = { path: "/tmp/x", existed: false, changed: true, beforeText: "", afterText: "", diffText: "" };
|
|
57
|
+
return {
|
|
58
|
+
previewImports: () => preview,
|
|
59
|
+
previewStarterProject: () => preview,
|
|
60
|
+
previewRepoPrompt: () => null,
|
|
61
|
+
adoptImports: async () => ({ added: [], path: "/tmp/x" }),
|
|
62
|
+
scaffoldProjectConfig: vi.fn(async () => ({ path: "/tmp/x" })),
|
|
63
|
+
addRepoPrompt: async () => ({ path: "/tmp/x", serverName: "repoprompt" }),
|
|
64
|
+
openPath: async () => {},
|
|
65
|
+
markSetupCompleted: () => {},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("panel-keys", () => {
|
|
70
|
+
it("honors user keybindings when a manager is provided", () => {
|
|
71
|
+
const keys = createPanelKeys(createEmacsKeybindings());
|
|
72
|
+
expect(keys.selectUp(CTRL_P)).toBe(true);
|
|
73
|
+
expect(keys.selectUp(UP)).toBe(true);
|
|
74
|
+
expect(keys.selectDown(CTRL_N)).toBe(true);
|
|
75
|
+
expect(keys.selectDown(DOWN)).toBe(true);
|
|
76
|
+
expect(keys.selectConfirm(ENTER)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("falls back to hardcoded defaults without a manager", () => {
|
|
80
|
+
const keys = createPanelKeys();
|
|
81
|
+
expect(keys.selectUp(UP)).toBe(true);
|
|
82
|
+
expect(keys.selectUp(CTRL_P)).toBe(false);
|
|
83
|
+
expect(keys.selectDown(DOWN)).toBe(true);
|
|
84
|
+
expect(keys.selectDown(CTRL_N)).toBe(false);
|
|
85
|
+
expect(keys.selectConfirm(ENTER)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("respects rebinding that removes a default key", () => {
|
|
89
|
+
const manager = new KeybindingsManager(TUI_KEYBINDINGS, {
|
|
90
|
+
"tui.select.up": "ctrl+p",
|
|
91
|
+
});
|
|
92
|
+
const keys = createPanelKeys(manager);
|
|
93
|
+
expect(keys.selectUp(CTRL_P)).toBe(true);
|
|
94
|
+
expect(keys.selectUp(UP)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("mcp-panel custom keybindings", () => {
|
|
99
|
+
it("navigates with ctrl+n/ctrl+p when bound to tui.select.down/up", async () => {
|
|
100
|
+
const callbacks = createAuthCallbacks();
|
|
101
|
+
const panel = createMcpPanel(
|
|
102
|
+
createTwoServerConfig(),
|
|
103
|
+
null,
|
|
104
|
+
new Map(),
|
|
105
|
+
callbacks,
|
|
106
|
+
{ requestRender: () => {} },
|
|
107
|
+
() => {},
|
|
108
|
+
{ authOnly: true, keybindings: createEmacsKeybindings() },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
panel.handleInput(CTRL_N);
|
|
112
|
+
panel.handleInput(ENTER);
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
expect(callbacks.authenticate).toHaveBeenLastCalledWith("beta");
|
|
115
|
+
|
|
116
|
+
panel.handleInput(CTRL_P);
|
|
117
|
+
panel.handleInput(ENTER);
|
|
118
|
+
await Promise.resolve();
|
|
119
|
+
expect(callbacks.authenticate).toHaveBeenLastCalledWith("alpha");
|
|
120
|
+
panel.dispose();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("keeps arrow keys working alongside custom bindings", async () => {
|
|
124
|
+
const callbacks = createAuthCallbacks();
|
|
125
|
+
const panel = createMcpPanel(
|
|
126
|
+
createTwoServerConfig(),
|
|
127
|
+
null,
|
|
128
|
+
new Map(),
|
|
129
|
+
callbacks,
|
|
130
|
+
{ requestRender: () => {} },
|
|
131
|
+
() => {},
|
|
132
|
+
{ authOnly: true, keybindings: createEmacsKeybindings() },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
panel.handleInput(DOWN);
|
|
136
|
+
panel.handleInput(ENTER);
|
|
137
|
+
await Promise.resolve();
|
|
138
|
+
expect(callbacks.authenticate).toHaveBeenLastCalledWith("beta");
|
|
139
|
+
panel.dispose();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("treats ctrl+p/ctrl+n as unbound without a keybindings manager", async () => {
|
|
143
|
+
const callbacks = createAuthCallbacks();
|
|
144
|
+
const panel = createMcpPanel(
|
|
145
|
+
createTwoServerConfig(),
|
|
146
|
+
null,
|
|
147
|
+
new Map(),
|
|
148
|
+
callbacks,
|
|
149
|
+
{ requestRender: () => {} },
|
|
150
|
+
() => {},
|
|
151
|
+
{ authOnly: true },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
panel.handleInput(CTRL_N);
|
|
155
|
+
panel.handleInput(ENTER);
|
|
156
|
+
await Promise.resolve();
|
|
157
|
+
// Cursor did not move: still authenticates the first server.
|
|
158
|
+
expect(callbacks.authenticate).toHaveBeenLastCalledWith("alpha");
|
|
159
|
+
panel.dispose();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("mcp-setup-panel custom keybindings", () => {
|
|
164
|
+
it("navigates actions with ctrl+n and confirms with enter", async () => {
|
|
165
|
+
const callbacks = createSetupCallbacks();
|
|
166
|
+
const panel = createMcpSetupPanel(
|
|
167
|
+
createEmptyDiscovery(),
|
|
168
|
+
callbacks,
|
|
169
|
+
{
|
|
170
|
+
mode: "setup",
|
|
171
|
+
onboardingState: { version: 1, sharedConfigHintShown: false, setupCompleted: false },
|
|
172
|
+
keybindings: createEmacsKeybindings(),
|
|
173
|
+
},
|
|
174
|
+
{ requestRender: () => {} },
|
|
175
|
+
() => {},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Actions for this discovery: view-example, scaffold-project, show-precedence, close.
|
|
179
|
+
panel.handleInput(CTRL_N);
|
|
180
|
+
panel.handleInput(ENTER);
|
|
181
|
+
await Promise.resolve();
|
|
182
|
+
await Promise.resolve();
|
|
183
|
+
expect(callbacks.scaffoldProjectConfig).toHaveBeenCalledTimes(1);
|
|
184
|
+
panel.dispose();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
describe("npx-resolver cache path", () => {
|
|
7
|
+
const originalHome = process.env.HOME;
|
|
8
|
+
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
9
|
+
const originalNpmCache = process.env.NPM_CONFIG_CACHE;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
process.env.HOME = originalHome;
|
|
17
|
+
if (originalAgentDir === undefined) {
|
|
18
|
+
delete process.env.PI_CODING_AGENT_DIR;
|
|
19
|
+
} else {
|
|
20
|
+
process.env.PI_CODING_AGENT_DIR = originalAgentDir;
|
|
21
|
+
}
|
|
22
|
+
if (originalNpmCache === undefined) {
|
|
23
|
+
delete process.env.NPM_CONFIG_CACHE;
|
|
24
|
+
} else {
|
|
25
|
+
process.env.NPM_CONFIG_CACHE = originalNpmCache;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("writes mcp-npx-cache.json to PI_CODING_AGENT_DIR", async () => {
|
|
30
|
+
const home = mkdtempSync(join(tmpdir(), "pi-mcp-npx-home-"));
|
|
31
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-mcp-npx-agent-"));
|
|
32
|
+
const npmCache = mkdtempSync(join(tmpdir(), "pi-mcp-npx-cache-"));
|
|
33
|
+
|
|
34
|
+
process.env.HOME = home;
|
|
35
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
36
|
+
process.env.NPM_CONFIG_CACHE = npmCache;
|
|
37
|
+
|
|
38
|
+
const packageDir = join(npmCache, "_npx", "fixture", "node_modules", "demo-pkg");
|
|
39
|
+
mkdirSync(join(packageDir, "bin"), { recursive: true });
|
|
40
|
+
writeFileSync(
|
|
41
|
+
join(packageDir, "package.json"),
|
|
42
|
+
JSON.stringify({ name: "demo-pkg", version: "1.0.0", bin: "bin/cli.js" }),
|
|
43
|
+
"utf-8",
|
|
44
|
+
);
|
|
45
|
+
writeFileSync(join(packageDir, "bin", "cli.js"), "#!/usr/bin/env node\nconsole.log('ok')\n", "utf-8");
|
|
46
|
+
|
|
47
|
+
const { resolveNpxBinary } = await import("../npx-resolver.ts");
|
|
48
|
+
const result = await resolveNpxBinary("npx", ["-y", "demo-pkg"]);
|
|
49
|
+
|
|
50
|
+
expect(result).not.toBeNull();
|
|
51
|
+
expect(existsSync(join(agentDir, "mcp-npx-cache.json"))).toBe(true);
|
|
52
|
+
expect(existsSync(join(home, ".pi", "agent", "mcp-npx-cache.json"))).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
describe("oauth-handler path resolution", () => {
|
|
7
|
+
const originalHome = process.env.HOME;
|
|
8
|
+
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
9
|
+
const originalOAuthDir = process.env.MCP_OAUTH_DIR;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
process.env.HOME = originalHome;
|
|
17
|
+
if (originalAgentDir === undefined) {
|
|
18
|
+
delete process.env.PI_CODING_AGENT_DIR;
|
|
19
|
+
} else {
|
|
20
|
+
process.env.PI_CODING_AGENT_DIR = originalAgentDir;
|
|
21
|
+
}
|
|
22
|
+
if (originalOAuthDir === undefined) {
|
|
23
|
+
delete process.env.MCP_OAUTH_DIR;
|
|
24
|
+
} else {
|
|
25
|
+
process.env.MCP_OAUTH_DIR = originalOAuthDir;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("reads tokens from PI_CODING_AGENT_DIR when MCP_OAUTH_DIR is unset", async () => {
|
|
30
|
+
const home = mkdtempSync(join(tmpdir(), "pi-mcp-oauth-handler-home-"));
|
|
31
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-mcp-oauth-handler-agent-"));
|
|
32
|
+
process.env.HOME = home;
|
|
33
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
34
|
+
delete process.env.MCP_OAUTH_DIR;
|
|
35
|
+
|
|
36
|
+
const { getAuthEntryFilePath } = await import("../mcp-auth.ts");
|
|
37
|
+
const tokensPath = getAuthEntryFilePath("demo");
|
|
38
|
+
mkdirSync(dirname(tokensPath), { recursive: true });
|
|
39
|
+
writeFileSync(tokensPath, JSON.stringify({ access_token: "abc", token_type: "bearer" }), "utf-8");
|
|
40
|
+
|
|
41
|
+
const { getStoredTokens } = await import("../oauth-handler.ts");
|
|
42
|
+
expect(getStoredTokens("demo")).toEqual({
|
|
43
|
+
access_token: "abc",
|
|
44
|
+
token_type: "bearer",
|
|
45
|
+
refresh_token: undefined,
|
|
46
|
+
expires_in: undefined,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("prefers MCP_OAUTH_DIR over PI_CODING_AGENT_DIR", async () => {
|
|
51
|
+
const home = mkdtempSync(join(tmpdir(), "pi-mcp-oauth-handler-home-"));
|
|
52
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-mcp-oauth-handler-agent-"));
|
|
53
|
+
const oauthDir = mkdtempSync(join(tmpdir(), "pi-mcp-oauth-handler-oauth-"));
|
|
54
|
+
process.env.HOME = home;
|
|
55
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
56
|
+
process.env.MCP_OAUTH_DIR = oauthDir;
|
|
57
|
+
|
|
58
|
+
const { getAuthEntryFilePath } = await import("../mcp-auth.ts");
|
|
59
|
+
|
|
60
|
+
delete process.env.MCP_OAUTH_DIR;
|
|
61
|
+
const agentTokensPath = getAuthEntryFilePath("demo");
|
|
62
|
+
mkdirSync(dirname(agentTokensPath), { recursive: true });
|
|
63
|
+
writeFileSync(agentTokensPath, JSON.stringify({ access_token: "from-agent" }), "utf-8");
|
|
64
|
+
|
|
65
|
+
process.env.MCP_OAUTH_DIR = oauthDir;
|
|
66
|
+
const overrideTokensPath = getAuthEntryFilePath("demo");
|
|
67
|
+
mkdirSync(dirname(overrideTokensPath), { recursive: true });
|
|
68
|
+
writeFileSync(
|
|
69
|
+
overrideTokensPath,
|
|
70
|
+
JSON.stringify({ access_token: "from-override", token_type: "bearer" }),
|
|
71
|
+
"utf-8",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const { getStoredTokens } = await import("../oauth-handler.ts");
|
|
75
|
+
expect(getStoredTokens("demo")?.access_token).toBe("from-override");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
describe("onboarding state", () => {
|
|
7
|
+
const originalHome = process.env.HOME;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.resetModules();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
process.env.HOME = originalHome;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns the default state when no file exists", async () => {
|
|
18
|
+
process.env.HOME = mkdtempSync(join(tmpdir(), "pi-mcp-onboarding-home-"));
|
|
19
|
+
const { loadOnboardingState, getOnboardingStatePath } = await import("../onboarding-state.ts");
|
|
20
|
+
|
|
21
|
+
expect(loadOnboardingState()).toEqual({
|
|
22
|
+
version: 1,
|
|
23
|
+
sharedConfigHintShown: false,
|
|
24
|
+
setupCompleted: false,
|
|
25
|
+
});
|
|
26
|
+
expect(existsSync(getOnboardingStatePath())).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("persists hint and setup completion state", async () => {
|
|
30
|
+
process.env.HOME = mkdtempSync(join(tmpdir(), "pi-mcp-onboarding-home-"));
|
|
31
|
+
const {
|
|
32
|
+
markSharedConfigHintShown,
|
|
33
|
+
markSetupCompleted,
|
|
34
|
+
loadOnboardingState,
|
|
35
|
+
getOnboardingStatePath,
|
|
36
|
+
} = await import("../onboarding-state.ts");
|
|
37
|
+
|
|
38
|
+
markSharedConfigHintShown("first");
|
|
39
|
+
markSetupCompleted("second");
|
|
40
|
+
|
|
41
|
+
expect(loadOnboardingState()).toEqual({
|
|
42
|
+
version: 1,
|
|
43
|
+
sharedConfigHintShown: true,
|
|
44
|
+
setupCompleted: true,
|
|
45
|
+
lastDiscoveryFingerprint: "second",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const raw = JSON.parse(readFileSync(getOnboardingStatePath(), "utf-8"));
|
|
49
|
+
expect(raw.sharedConfigHintShown).toBe(true);
|
|
50
|
+
expect(raw.setupCompleted).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
+
const packageJson = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf-8")) as {
|
|
8
|
+
files?: string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("package.json files", () => {
|
|
12
|
+
it("publishes every root runtime TypeScript module", () => {
|
|
13
|
+
const publishedFiles = new Set(packageJson.files ?? []);
|
|
14
|
+
const runtimeModules = readdirSync(repoRoot)
|
|
15
|
+
.filter((entry) => entry.endsWith(".ts"))
|
|
16
|
+
.filter((entry) => !entry.endsWith(".test.ts"))
|
|
17
|
+
.filter((entry) => entry !== "vitest.config.ts");
|
|
18
|
+
|
|
19
|
+
expect(runtimeModules.length).toBeGreaterThan(0);
|
|
20
|
+
expect(runtimeModules.filter((entry) => !publishedFiles.has(entry))).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
});
|