@meshxdata/fops 0.0.1 → 0.0.3
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/README.md +62 -40
- package/package.json +4 -3
- package/src/agent/agent.js +26 -34
- package/src/agent/context.js +155 -98
- package/src/agent/llm.js +54 -11
- package/src/auth/coda.js +128 -0
- package/src/auth/index.js +1 -0
- package/src/commands/index.js +23 -9
- package/src/doctor.js +143 -15
- package/src/plugins/api.js +9 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/knowledge.js +124 -0
- package/src/plugins/registry.js +1 -0
- package/src/setup/setup.js +10 -5
- package/src/setup/wizard.js +15 -11
- package/src/shell.js +2 -2
- package/src/skills/foundation/SKILL.md +200 -66
- package/src/ui/input.js +31 -34
- package/src/ui/spinner.js +36 -10
- package/STRUCTURE.md +0 -43
- package/src/agent/agent.test.js +0 -233
- package/src/agent/context.test.js +0 -81
- package/src/agent/llm.test.js +0 -139
- package/src/auth/keychain.test.js +0 -185
- package/src/auth/login.test.js +0 -192
- package/src/auth/oauth.test.js +0 -118
- package/src/auth/resolve.test.js +0 -153
- package/src/config.test.js +0 -70
- package/src/doctor.test.js +0 -134
- package/src/plugins/api.test.js +0 -95
- package/src/plugins/discovery.test.js +0 -92
- package/src/plugins/hooks.test.js +0 -118
- package/src/plugins/manifest.test.js +0 -106
- package/src/plugins/registry.test.js +0 -43
- package/src/plugins/skills.test.js +0 -173
- package/src/project.test.js +0 -196
- package/src/setup/aws.test.js +0 -280
- package/src/shell.test.js +0 -72
- package/src/ui/banner.test.js +0 -97
- package/src/ui/spinner.test.js +0 -29
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("execa", () => ({
|
|
4
|
-
execaSync: vi.fn(),
|
|
5
|
-
}));
|
|
6
|
-
|
|
7
|
-
const { execaSync } = await import("execa");
|
|
8
|
-
const { readClaudeCodeKeychain, saveClaudeCodeKeychain } = await import("./keychain.js");
|
|
9
|
-
|
|
10
|
-
describe("auth/keychain", () => {
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
vi.clearAllMocks();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("readClaudeCodeKeychain", () => {
|
|
16
|
-
it("returns null on non-darwin platforms", () => {
|
|
17
|
-
const original = process.platform;
|
|
18
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
19
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
20
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("returns null on win32", () => {
|
|
24
|
-
const original = process.platform;
|
|
25
|
-
Object.defineProperty(process, "platform", { value: "win32" });
|
|
26
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
27
|
-
expect(execaSync).not.toHaveBeenCalled();
|
|
28
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("returns tokens on darwin when keychain has data", () => {
|
|
32
|
-
const original = process.platform;
|
|
33
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
34
|
-
execaSync.mockReturnValue({
|
|
35
|
-
stdout: JSON.stringify({
|
|
36
|
-
claudeAiOauth: {
|
|
37
|
-
accessToken: "sk-ant-oat01-test",
|
|
38
|
-
refreshToken: "rt-test",
|
|
39
|
-
},
|
|
40
|
-
}),
|
|
41
|
-
});
|
|
42
|
-
const result = readClaudeCodeKeychain();
|
|
43
|
-
expect(result).toEqual({
|
|
44
|
-
accessToken: "sk-ant-oat01-test",
|
|
45
|
-
refreshToken: "rt-test",
|
|
46
|
-
});
|
|
47
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("returns null refreshToken when not in keychain data", () => {
|
|
51
|
-
const original = process.platform;
|
|
52
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
53
|
-
execaSync.mockReturnValue({
|
|
54
|
-
stdout: JSON.stringify({
|
|
55
|
-
claudeAiOauth: {
|
|
56
|
-
accessToken: "sk-ant-oat01-test",
|
|
57
|
-
},
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
60
|
-
const result = readClaudeCodeKeychain();
|
|
61
|
-
expect(result).toEqual({
|
|
62
|
-
accessToken: "sk-ant-oat01-test",
|
|
63
|
-
refreshToken: null,
|
|
64
|
-
});
|
|
65
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("returns null on darwin when keychain throws", () => {
|
|
69
|
-
const original = process.platform;
|
|
70
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
71
|
-
execaSync.mockImplementation(() => { throw new Error("not found"); });
|
|
72
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
73
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("returns null when keychain data has no claudeAiOauth", () => {
|
|
77
|
-
const original = process.platform;
|
|
78
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
79
|
-
execaSync.mockReturnValue({ stdout: JSON.stringify({ other: "data" }) });
|
|
80
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
81
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("returns null when stdout is not valid JSON", () => {
|
|
85
|
-
const original = process.platform;
|
|
86
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
87
|
-
execaSync.mockReturnValue({ stdout: "not json" });
|
|
88
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
89
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("calls security find-generic-password on darwin", () => {
|
|
93
|
-
const original = process.platform;
|
|
94
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
95
|
-
execaSync.mockReturnValue({ stdout: "{}" });
|
|
96
|
-
readClaudeCodeKeychain();
|
|
97
|
-
expect(execaSync).toHaveBeenCalledWith(
|
|
98
|
-
"security",
|
|
99
|
-
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
100
|
-
{ encoding: "utf8" }
|
|
101
|
-
);
|
|
102
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("saveClaudeCodeKeychain", () => {
|
|
107
|
-
it("returns false on non-darwin platforms", () => {
|
|
108
|
-
const original = process.platform;
|
|
109
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
110
|
-
expect(saveClaudeCodeKeychain("token")).toBe(false);
|
|
111
|
-
expect(execaSync).not.toHaveBeenCalled();
|
|
112
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("calls security commands on darwin", () => {
|
|
116
|
-
const original = process.platform;
|
|
117
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
118
|
-
execaSync.mockReturnValue({});
|
|
119
|
-
const result = saveClaudeCodeKeychain("access-token", "refresh-token");
|
|
120
|
-
expect(result).toBe(true);
|
|
121
|
-
expect(execaSync).toHaveBeenCalledWith(
|
|
122
|
-
"security",
|
|
123
|
-
expect.arrayContaining(["delete-generic-password"])
|
|
124
|
-
);
|
|
125
|
-
expect(execaSync).toHaveBeenCalledWith(
|
|
126
|
-
"security",
|
|
127
|
-
expect.arrayContaining(["add-generic-password"])
|
|
128
|
-
);
|
|
129
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("succeeds even when delete fails (no existing entry)", () => {
|
|
133
|
-
const original = process.platform;
|
|
134
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
135
|
-
let callCount = 0;
|
|
136
|
-
execaSync.mockImplementation((_cmd, args) => {
|
|
137
|
-
if (args.includes("delete-generic-password")) {
|
|
138
|
-
throw new Error("item not found");
|
|
139
|
-
}
|
|
140
|
-
return {};
|
|
141
|
-
});
|
|
142
|
-
const result = saveClaudeCodeKeychain("access-token");
|
|
143
|
-
expect(result).toBe(true);
|
|
144
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("returns false when add-generic-password fails", () => {
|
|
148
|
-
const original = process.platform;
|
|
149
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
150
|
-
execaSync.mockImplementation((_cmd, args) => {
|
|
151
|
-
if (args.includes("add-generic-password")) {
|
|
152
|
-
throw new Error("keychain locked");
|
|
153
|
-
}
|
|
154
|
-
return {};
|
|
155
|
-
});
|
|
156
|
-
const result = saveClaudeCodeKeychain("token");
|
|
157
|
-
expect(result).toBe(false);
|
|
158
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("saves without refreshToken when not provided", () => {
|
|
162
|
-
const original = process.platform;
|
|
163
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
164
|
-
execaSync.mockReturnValue({});
|
|
165
|
-
saveClaudeCodeKeychain("access-only");
|
|
166
|
-
const addCall = execaSync.mock.calls.find((c) => c[1].includes("add-generic-password"));
|
|
167
|
-
const data = JSON.parse(addCall[1][addCall[1].indexOf("-w") + 1]);
|
|
168
|
-
expect(data.claudeAiOauth.accessToken).toBe("access-only");
|
|
169
|
-
expect(data.claudeAiOauth.refreshToken).toBeUndefined();
|
|
170
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("includes refreshToken when provided", () => {
|
|
174
|
-
const original = process.platform;
|
|
175
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
176
|
-
execaSync.mockReturnValue({});
|
|
177
|
-
saveClaudeCodeKeychain("access", "refresh");
|
|
178
|
-
const addCall = execaSync.mock.calls.find((c) => c[1].includes("add-generic-password"));
|
|
179
|
-
const data = JSON.parse(addCall[1][addCall[1].indexOf("-w") + 1]);
|
|
180
|
-
expect(data.claudeAiOauth.accessToken).toBe("access");
|
|
181
|
-
expect(data.claudeAiOauth.refreshToken).toBe("refresh");
|
|
182
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|
package/src/auth/login.test.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
vi.mock("execa", () => ({
|
|
7
|
-
execaSync: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
vi.mock("inquirer", () => ({
|
|
11
|
-
default: { prompt: vi.fn() },
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock("./resolve.js", () => ({
|
|
15
|
-
CLAUDE_DIR: path.join(os.tmpdir(), ".claude-test-login"),
|
|
16
|
-
CLAUDE_CREDENTIALS: path.join(os.tmpdir(), ".claude-test-login", ".credentials.json"),
|
|
17
|
-
CLAUDE_JSON: path.join(os.tmpdir(), ".claude-test-login", ".claude.json"),
|
|
18
|
-
resolveAnthropicApiKey: vi.fn(() => null),
|
|
19
|
-
readJsonKey: vi.fn(),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
const { CLAUDE_DIR, CLAUDE_CREDENTIALS } = await import("./resolve.js");
|
|
23
|
-
const { execaSync } = await import("execa");
|
|
24
|
-
const { authHelp, openBrowser, saveApiKey, LOGIN_HTML } = await import("./login.js");
|
|
25
|
-
|
|
26
|
-
describe("auth/login", () => {
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
if (fs.existsSync(CLAUDE_DIR)) fs.rmSync(CLAUDE_DIR, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
if (fs.existsSync(CLAUDE_DIR)) fs.rmSync(CLAUDE_DIR, { recursive: true, force: true });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("authHelp", () => {
|
|
36
|
-
it("prints help text about API keys", () => {
|
|
37
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
38
|
-
authHelp();
|
|
39
|
-
expect(spy).toHaveBeenCalled();
|
|
40
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
41
|
-
expect(output).toContain("API key");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("mentions environment variables", () => {
|
|
45
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
46
|
-
authHelp();
|
|
47
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
48
|
-
expect(output).toContain("ANTHROPIC_API_KEY");
|
|
49
|
-
expect(output).toContain("OPENAI_API_KEY");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("mentions credentials file", () => {
|
|
53
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
54
|
-
authHelp();
|
|
55
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
56
|
-
expect(output).toContain(".credentials.json");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("mentions apiKeyHelper", () => {
|
|
60
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
61
|
-
authHelp();
|
|
62
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
63
|
-
expect(output).toContain("apiKeyHelper");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("openBrowser", () => {
|
|
68
|
-
it("calls open on darwin", () => {
|
|
69
|
-
const original = process.platform;
|
|
70
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
71
|
-
execaSync.mockReturnValue({});
|
|
72
|
-
const result = openBrowser("https://example.com");
|
|
73
|
-
expect(execaSync).toHaveBeenCalledWith("open", ["https://example.com"], { reject: false });
|
|
74
|
-
expect(result).toBe(true);
|
|
75
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("calls xdg-open on linux", () => {
|
|
79
|
-
const original = process.platform;
|
|
80
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
81
|
-
execaSync.mockReturnValue({});
|
|
82
|
-
openBrowser("https://example.com");
|
|
83
|
-
expect(execaSync).toHaveBeenCalledWith("xdg-open", ["https://example.com"], { reject: false });
|
|
84
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("calls start on win32", () => {
|
|
88
|
-
const original = process.platform;
|
|
89
|
-
Object.defineProperty(process, "platform", { value: "win32" });
|
|
90
|
-
execaSync.mockReturnValue({});
|
|
91
|
-
openBrowser("https://example.com");
|
|
92
|
-
expect(execaSync).toHaveBeenCalledWith("start", ["https://example.com"], { reject: false });
|
|
93
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("returns false on error", () => {
|
|
97
|
-
execaSync.mockImplementation(() => { throw new Error("fail"); });
|
|
98
|
-
expect(openBrowser("https://example.com")).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("returns true on success", () => {
|
|
102
|
-
execaSync.mockReturnValue({});
|
|
103
|
-
expect(openBrowser("https://example.com")).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("saveApiKey", () => {
|
|
108
|
-
it("creates directory and saves key", () => {
|
|
109
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
110
|
-
saveApiKey("sk-ant-api03-test123");
|
|
111
|
-
expect(fs.existsSync(CLAUDE_CREDENTIALS)).toBe(true);
|
|
112
|
-
const creds = JSON.parse(fs.readFileSync(CLAUDE_CREDENTIALS, "utf8"));
|
|
113
|
-
expect(creds.anthropic_api_key).toBe("sk-ant-api03-test123");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("preserves existing keys", () => {
|
|
117
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
118
|
-
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
119
|
-
fs.writeFileSync(CLAUDE_CREDENTIALS, JSON.stringify({ openai_api_key: "sk-openai" }));
|
|
120
|
-
saveApiKey("sk-ant-api03-new");
|
|
121
|
-
const creds = JSON.parse(fs.readFileSync(CLAUDE_CREDENTIALS, "utf8"));
|
|
122
|
-
expect(creds.anthropic_api_key).toBe("sk-ant-api03-new");
|
|
123
|
-
expect(creds.openai_api_key).toBe("sk-openai");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("trims the API key", () => {
|
|
127
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
128
|
-
saveApiKey(" sk-ant-api03-trimme ");
|
|
129
|
-
const creds = JSON.parse(fs.readFileSync(CLAUDE_CREDENTIALS, "utf8"));
|
|
130
|
-
expect(creds.anthropic_api_key).toBe("sk-ant-api03-trimme");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("overwrites existing anthropic key", () => {
|
|
134
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
135
|
-
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
136
|
-
fs.writeFileSync(CLAUDE_CREDENTIALS, JSON.stringify({ anthropic_api_key: "old-key" }));
|
|
137
|
-
saveApiKey("new-key");
|
|
138
|
-
const creds = JSON.parse(fs.readFileSync(CLAUDE_CREDENTIALS, "utf8"));
|
|
139
|
-
expect(creds.anthropic_api_key).toBe("new-key");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("handles corrupted existing credentials", () => {
|
|
143
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
144
|
-
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
145
|
-
fs.writeFileSync(CLAUDE_CREDENTIALS, "not valid json{{{");
|
|
146
|
-
saveApiKey("sk-ant-api03-fresh");
|
|
147
|
-
const creds = JSON.parse(fs.readFileSync(CLAUDE_CREDENTIALS, "utf8"));
|
|
148
|
-
expect(creds.anthropic_api_key).toBe("sk-ant-api03-fresh");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("returns true", () => {
|
|
152
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
153
|
-
expect(saveApiKey("sk-ant-api03-x")).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("prints success message", () => {
|
|
157
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
158
|
-
saveApiKey("sk-ant-api03-x");
|
|
159
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
160
|
-
expect(output).toContain("Login successful");
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe("LOGIN_HTML", () => {
|
|
165
|
-
it("contains expected HTML structure", () => {
|
|
166
|
-
expect(LOGIN_HTML).toContain("<!DOCTYPE html>");
|
|
167
|
-
expect(LOGIN_HTML).toContain("Foundation CLI Login");
|
|
168
|
-
expect(LOGIN_HTML).toContain("sk-ant-api");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("contains form elements", () => {
|
|
172
|
-
expect(LOGIN_HTML).toContain("<form");
|
|
173
|
-
expect(LOGIN_HTML).toContain("api-key");
|
|
174
|
-
expect(LOGIN_HTML).toContain("submit-btn");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("contains validation logic", () => {
|
|
178
|
-
expect(LOGIN_HTML).toContain("sk-ant-api");
|
|
179
|
-
expect(LOGIN_HTML).toContain("/callback");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("contains success view", () => {
|
|
183
|
-
expect(LOGIN_HTML).toContain("success-view");
|
|
184
|
-
expect(LOGIN_HTML).toContain("Login Successful");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("has step-by-step instructions", () => {
|
|
188
|
-
expect(LOGIN_HTML).toContain("step-num");
|
|
189
|
-
expect(LOGIN_HTML).toContain("console.anthropic.com");
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
});
|
package/src/auth/oauth.test.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import { OAUTH_CONFIG, generatePKCE, getResultHTML } from "./oauth.js";
|
|
4
|
-
|
|
5
|
-
describe("auth/oauth", () => {
|
|
6
|
-
describe("OAUTH_CONFIG", () => {
|
|
7
|
-
it("has required OAuth fields", () => {
|
|
8
|
-
expect(OAUTH_CONFIG.authorizationEndpoint).toContain("claude.ai");
|
|
9
|
-
expect(OAUTH_CONFIG.tokenEndpoint).toContain("claude.ai");
|
|
10
|
-
expect(typeof OAUTH_CONFIG.clientId).toBe("string");
|
|
11
|
-
expect(OAUTH_CONFIG.clientId.length).toBeGreaterThan(0);
|
|
12
|
-
expect(OAUTH_CONFIG.scope).toBe("user:inference");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("authorization endpoint is a valid URL", () => {
|
|
16
|
-
expect(() => new URL(OAUTH_CONFIG.authorizationEndpoint)).not.toThrow();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("token endpoint is a valid URL", () => {
|
|
20
|
-
expect(() => new URL(OAUTH_CONFIG.tokenEndpoint)).not.toThrow();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("clientId is a UUID format", () => {
|
|
24
|
-
expect(OAUTH_CONFIG.clientId).toMatch(
|
|
25
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("endpoints use HTTPS", () => {
|
|
30
|
-
expect(OAUTH_CONFIG.authorizationEndpoint).toMatch(/^https:/);
|
|
31
|
-
expect(OAUTH_CONFIG.tokenEndpoint).toMatch(/^https:/);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("generatePKCE", () => {
|
|
36
|
-
it("returns verifier and challenge", () => {
|
|
37
|
-
const { verifier, challenge } = generatePKCE();
|
|
38
|
-
expect(typeof verifier).toBe("string");
|
|
39
|
-
expect(typeof challenge).toBe("string");
|
|
40
|
-
expect(verifier.length).toBeGreaterThan(0);
|
|
41
|
-
expect(challenge.length).toBeGreaterThan(0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("generates base64url strings (no +, /, = chars)", () => {
|
|
45
|
-
const { verifier, challenge } = generatePKCE();
|
|
46
|
-
expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
47
|
-
expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("generates unique values each time", () => {
|
|
51
|
-
const a = generatePKCE();
|
|
52
|
-
const b = generatePKCE();
|
|
53
|
-
expect(a.verifier).not.toBe(b.verifier);
|
|
54
|
-
expect(a.challenge).not.toBe(b.challenge);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("challenge is SHA-256 of verifier", () => {
|
|
58
|
-
const { verifier, challenge } = generatePKCE();
|
|
59
|
-
const expected = crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
60
|
-
expect(challenge).toBe(expected);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("verifier is 43 chars (32 bytes base64url)", () => {
|
|
64
|
-
const { verifier } = generatePKCE();
|
|
65
|
-
// 32 bytes in base64url = ceil(32 * 4/3) = 43 chars
|
|
66
|
-
expect(verifier.length).toBe(43);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("challenge is 43 chars (SHA-256 = 32 bytes base64url)", () => {
|
|
70
|
-
const { challenge } = generatePKCE();
|
|
71
|
-
expect(challenge.length).toBe(43);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("getResultHTML", () => {
|
|
76
|
-
it("returns success HTML", () => {
|
|
77
|
-
const html = getResultHTML(true, "All good");
|
|
78
|
-
expect(html).toContain("<!DOCTYPE html>");
|
|
79
|
-
expect(html).toContain("Success");
|
|
80
|
-
expect(html).toContain("All good");
|
|
81
|
-
expect(html).toContain("#4ade80");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("returns error HTML", () => {
|
|
85
|
-
const html = getResultHTML(false, "Something failed");
|
|
86
|
-
expect(html).toContain("Error");
|
|
87
|
-
expect(html).toContain("Something failed");
|
|
88
|
-
expect(html).toContain("#f87171");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("success uses checkmark icon", () => {
|
|
92
|
-
const html = getResultHTML(true, "ok");
|
|
93
|
-
expect(html).toContain("✓");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("error uses cross icon", () => {
|
|
97
|
-
const html = getResultHTML(false, "fail");
|
|
98
|
-
expect(html).toContain("✗");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("escapes message into HTML body", () => {
|
|
102
|
-
const html = getResultHTML(true, "Token saved");
|
|
103
|
-
expect(html).toContain("Token saved");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("returns valid HTML with head and body", () => {
|
|
107
|
-
const html = getResultHTML(true, "test");
|
|
108
|
-
expect(html).toContain("<head>");
|
|
109
|
-
expect(html).toContain("<body>");
|
|
110
|
-
expect(html).toContain("</html>");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("includes Foundation CLI title", () => {
|
|
114
|
-
const html = getResultHTML(true, "test");
|
|
115
|
-
expect(html).toContain("Foundation CLI");
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
});
|
package/src/auth/resolve.test.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
vi.mock("execa", () => ({
|
|
7
|
-
execaSync: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
describe("auth/resolve", () => {
|
|
11
|
-
describe("readJsonKey", () => {
|
|
12
|
-
let readJsonKey;
|
|
13
|
-
|
|
14
|
-
beforeEach(async () => {
|
|
15
|
-
({ readJsonKey } = await import("./resolve.js"));
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("returns first matching string key", () => {
|
|
19
|
-
expect(readJsonKey({ apiKey: "sk-123" }, ["apiKey"])).toBe("sk-123");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("tries multiple keys in order", () => {
|
|
23
|
-
expect(readJsonKey({ api_key: "sk-456" }, ["apiKey", "api_key"])).toBe("sk-456");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("returns first match when multiple keys exist", () => {
|
|
27
|
-
expect(readJsonKey({ apiKey: "first", api_key: "second" }, ["apiKey", "api_key"])).toBe("first");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("trims whitespace", () => {
|
|
31
|
-
expect(readJsonKey({ apiKey: " sk-789 " }, ["apiKey"])).toBe("sk-789");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("returns null for empty string", () => {
|
|
35
|
-
expect(readJsonKey({ apiKey: " " }, ["apiKey"])).toBe(null);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("returns null for non-string values", () => {
|
|
39
|
-
expect(readJsonKey({ apiKey: 123 }, ["apiKey"])).toBe(null);
|
|
40
|
-
expect(readJsonKey({ apiKey: true }, ["apiKey"])).toBe(null);
|
|
41
|
-
expect(readJsonKey({ apiKey: null }, ["apiKey"])).toBe(null);
|
|
42
|
-
expect(readJsonKey({ apiKey: [] }, ["apiKey"])).toBe(null);
|
|
43
|
-
expect(readJsonKey({ apiKey: {} }, ["apiKey"])).toBe(null);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns null for null/undefined input", () => {
|
|
47
|
-
expect(readJsonKey(null, ["apiKey"])).toBe(null);
|
|
48
|
-
expect(readJsonKey(undefined, ["apiKey"])).toBe(null);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("returns null for non-object input", () => {
|
|
52
|
-
expect(readJsonKey("string", ["apiKey"])).toBe(null);
|
|
53
|
-
expect(readJsonKey(42, ["apiKey"])).toBe(null);
|
|
54
|
-
expect(readJsonKey(true, ["apiKey"])).toBe(null);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("returns null when key is not found", () => {
|
|
58
|
-
expect(readJsonKey({ other: "value" }, ["apiKey"])).toBe(null);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("skips empty keys and continues", () => {
|
|
62
|
-
expect(readJsonKey({ first: "", second: "found" }, ["first", "second"])).toBe("found");
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("resolveAnthropicApiKey", () => {
|
|
67
|
-
let resolveAnthropicApiKey;
|
|
68
|
-
const originalEnv = { ...process.env };
|
|
69
|
-
|
|
70
|
-
beforeEach(async () => {
|
|
71
|
-
vi.resetModules();
|
|
72
|
-
vi.mock("execa", () => ({ execaSync: vi.fn() }));
|
|
73
|
-
({ resolveAnthropicApiKey } = await import("./resolve.js"));
|
|
74
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
process.env = { ...originalEnv };
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("returns env var when set", () => {
|
|
82
|
-
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-env";
|
|
83
|
-
expect(resolveAnthropicApiKey()).toBe("sk-ant-api03-env");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("trims env var whitespace", () => {
|
|
87
|
-
process.env.ANTHROPIC_API_KEY = " sk-ant-api03-padded ";
|
|
88
|
-
expect(resolveAnthropicApiKey()).toBe("sk-ant-api03-padded");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("returns null when nothing is configured", () => {
|
|
92
|
-
const result = resolveAnthropicApiKey();
|
|
93
|
-
expect(result === null || typeof result === "string").toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("prioritizes env var over files", () => {
|
|
97
|
-
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-env-wins";
|
|
98
|
-
// Even if files exist, env should be returned
|
|
99
|
-
expect(resolveAnthropicApiKey()).toBe("sk-ant-api03-env-wins");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("resolveOpenAiApiKey", () => {
|
|
104
|
-
let resolveOpenAiApiKey;
|
|
105
|
-
const originalEnv = { ...process.env };
|
|
106
|
-
|
|
107
|
-
beforeEach(async () => {
|
|
108
|
-
vi.resetModules();
|
|
109
|
-
vi.mock("execa", () => ({ execaSync: vi.fn() }));
|
|
110
|
-
({ resolveOpenAiApiKey } = await import("./resolve.js"));
|
|
111
|
-
delete process.env.OPENAI_API_KEY;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
afterEach(() => {
|
|
115
|
-
process.env = { ...originalEnv };
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("returns env var when set", () => {
|
|
119
|
-
process.env.OPENAI_API_KEY = "sk-openai-env";
|
|
120
|
-
expect(resolveOpenAiApiKey()).toBe("sk-openai-env");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("trims env var whitespace", () => {
|
|
124
|
-
process.env.OPENAI_API_KEY = " sk-openai-padded ";
|
|
125
|
-
expect(resolveOpenAiApiKey()).toBe("sk-openai-padded");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("returns null when nothing configured", () => {
|
|
129
|
-
const result = resolveOpenAiApiKey();
|
|
130
|
-
expect(result === null || typeof result === "string").toBe(true);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe("CLAUDE_DIR / CLAUDE_JSON / CLAUDE_CREDENTIALS exports", () => {
|
|
135
|
-
let CLAUDE_DIR, CLAUDE_JSON, CLAUDE_CREDENTIALS;
|
|
136
|
-
|
|
137
|
-
beforeEach(async () => {
|
|
138
|
-
({ CLAUDE_DIR, CLAUDE_JSON, CLAUDE_CREDENTIALS } = await import("./resolve.js"));
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("CLAUDE_DIR points to ~/.claude", () => {
|
|
142
|
-
expect(CLAUDE_DIR).toBe(path.join(os.homedir(), ".claude"));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("CLAUDE_JSON points to ~/.claude.json", () => {
|
|
146
|
-
expect(CLAUDE_JSON).toBe(path.join(os.homedir(), ".claude.json"));
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("CLAUDE_CREDENTIALS points to ~/.claude/.credentials.json", () => {
|
|
150
|
-
expect(CLAUDE_CREDENTIALS).toBe(path.join(os.homedir(), ".claude", ".credentials.json"));
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
});
|