@meshxdata/fops 0.0.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.

Potentially problematic release.


This version of @meshxdata/fops might be problematic. Click here for more details.

Files changed (57) hide show
  1. package/README.md +98 -0
  2. package/STRUCTURE.md +43 -0
  3. package/foundation.mjs +16 -0
  4. package/package.json +52 -0
  5. package/src/agent/agent.js +367 -0
  6. package/src/agent/agent.test.js +233 -0
  7. package/src/agent/context.js +143 -0
  8. package/src/agent/context.test.js +81 -0
  9. package/src/agent/index.js +2 -0
  10. package/src/agent/llm.js +127 -0
  11. package/src/agent/llm.test.js +139 -0
  12. package/src/auth/index.js +4 -0
  13. package/src/auth/keychain.js +58 -0
  14. package/src/auth/keychain.test.js +185 -0
  15. package/src/auth/login.js +421 -0
  16. package/src/auth/login.test.js +192 -0
  17. package/src/auth/oauth.js +203 -0
  18. package/src/auth/oauth.test.js +118 -0
  19. package/src/auth/resolve.js +78 -0
  20. package/src/auth/resolve.test.js +153 -0
  21. package/src/commands/index.js +268 -0
  22. package/src/config.js +24 -0
  23. package/src/config.test.js +70 -0
  24. package/src/doctor.js +487 -0
  25. package/src/doctor.test.js +134 -0
  26. package/src/plugins/api.js +37 -0
  27. package/src/plugins/api.test.js +95 -0
  28. package/src/plugins/discovery.js +78 -0
  29. package/src/plugins/discovery.test.js +92 -0
  30. package/src/plugins/hooks.js +13 -0
  31. package/src/plugins/hooks.test.js +118 -0
  32. package/src/plugins/index.js +3 -0
  33. package/src/plugins/loader.js +110 -0
  34. package/src/plugins/manifest.js +26 -0
  35. package/src/plugins/manifest.test.js +106 -0
  36. package/src/plugins/registry.js +14 -0
  37. package/src/plugins/registry.test.js +43 -0
  38. package/src/plugins/skills.js +126 -0
  39. package/src/plugins/skills.test.js +173 -0
  40. package/src/project.js +61 -0
  41. package/src/project.test.js +196 -0
  42. package/src/setup/aws.js +369 -0
  43. package/src/setup/aws.test.js +280 -0
  44. package/src/setup/index.js +3 -0
  45. package/src/setup/setup.js +161 -0
  46. package/src/setup/wizard.js +119 -0
  47. package/src/shell.js +9 -0
  48. package/src/shell.test.js +72 -0
  49. package/src/skills/foundation/SKILL.md +107 -0
  50. package/src/ui/banner.js +56 -0
  51. package/src/ui/banner.test.js +97 -0
  52. package/src/ui/confirm.js +97 -0
  53. package/src/ui/index.js +5 -0
  54. package/src/ui/input.js +199 -0
  55. package/src/ui/spinner.js +170 -0
  56. package/src/ui/spinner.test.js +29 -0
  57. package/src/ui/streaming.js +106 -0
@@ -0,0 +1,421 @@
1
+ import fs from "node:fs";
2
+ import http from "node:http";
3
+ import chalk from "chalk";
4
+ import { execaSync } from "execa";
5
+ import inquirer from "inquirer";
6
+ import { CLAUDE_DIR, CLAUDE_CREDENTIALS, resolveAnthropicApiKey } from "./resolve.js";
7
+
8
+ const ANTHROPIC_KEYS_URL = "https://console.anthropic.com/settings/keys";
9
+
10
+ export function authHelp() {
11
+ console.log(chalk.yellow("No API key found. Try one of:"));
12
+ console.log(chalk.gray(" • Open " + chalk.cyan(ANTHROPIC_KEYS_URL) + " in your browser to sign in and create a key"));
13
+ console.log(chalk.gray(" • ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
14
+ console.log(chalk.gray(" • ~/.claude/.credentials.json with anthropic_api_key or apiKey"));
15
+ console.log(chalk.gray(" • ~/.claude/settings.json with apiKeyHelper (script that prints the key)\n"));
16
+ }
17
+
18
+ export function openBrowser(url) {
19
+ const platform = process.platform;
20
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
21
+ try {
22
+ execaSync(cmd, [url], { reject: false });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ export function saveApiKey(apiKey) {
30
+ // Ensure ~/.claude directory exists
31
+ if (!fs.existsSync(CLAUDE_DIR)) {
32
+ fs.mkdirSync(CLAUDE_DIR, { mode: 0o700 });
33
+ }
34
+
35
+ // Read existing credentials or start fresh
36
+ let creds = {};
37
+ if (fs.existsSync(CLAUDE_CREDENTIALS)) {
38
+ try {
39
+ creds = JSON.parse(fs.readFileSync(CLAUDE_CREDENTIALS, "utf8"));
40
+ } catch {
41
+ // Start fresh if corrupted
42
+ }
43
+ }
44
+
45
+ creds.anthropic_api_key = apiKey.trim();
46
+ fs.writeFileSync(CLAUDE_CREDENTIALS, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
47
+
48
+ console.log(chalk.green("\nLogin successful"));
49
+ console.log(chalk.gray("Key saved to ~/.claude/.credentials.json\n"));
50
+ return true;
51
+ }
52
+
53
+ export async function offerClaudeLogin() {
54
+ const { startLogin } = await inquirer.prompt([
55
+ {
56
+ type: "confirm",
57
+ name: "startLogin",
58
+ message: "Open Anthropic Console in browser to sign in and get an API key?",
59
+ default: true,
60
+ },
61
+ ]);
62
+ if (!startLogin) return false;
63
+ console.log(chalk.blue("\n Opening " + ANTHROPIC_KEYS_URL + " …\n"));
64
+ const opened = openBrowser(ANTHROPIC_KEYS_URL);
65
+ if (!opened) {
66
+ console.log(chalk.yellow(" Could not open browser. Visit: " + ANTHROPIC_KEYS_URL + "\n"));
67
+ }
68
+ console.log(chalk.gray(" 1. Sign in and create an API key"));
69
+ console.log(chalk.gray(" 2. Add it to ~/.claude/.credentials.json:"));
70
+ console.log(chalk.gray(' { "anthropic_api_key": "sk-ant-..." }'));
71
+ console.log(chalk.gray(" 3. Or run: export ANTHROPIC_API_KEY=\"sk-ant-...\""));
72
+ console.log(chalk.gray(" 4. Then run foundation chat again.\n"));
73
+ return true;
74
+ }
75
+
76
+ export const LOGIN_HTML = `<!DOCTYPE html>
77
+ <html>
78
+ <head>
79
+ <title>Foundation CLI Login</title>
80
+ <style>
81
+ * { box-sizing: border-box; }
82
+ body {
83
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
84
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
85
+ color: #e4e4e7;
86
+ min-height: 100vh;
87
+ margin: 0;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ }
92
+ .container {
93
+ background: #1e1e2e;
94
+ border-radius: 12px;
95
+ padding: 40px;
96
+ max-width: 480px;
97
+ width: 90%;
98
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
99
+ }
100
+ h1 {
101
+ margin: 0 0 8px 0;
102
+ font-size: 24px;
103
+ color: #f4f4f5;
104
+ }
105
+ .subtitle {
106
+ color: #a1a1aa;
107
+ margin-bottom: 24px;
108
+ font-size: 14px;
109
+ }
110
+ .steps {
111
+ background: #27273a;
112
+ border-radius: 8px;
113
+ padding: 16px;
114
+ margin-bottom: 24px;
115
+ }
116
+ .step {
117
+ display: flex;
118
+ align-items: flex-start;
119
+ margin-bottom: 12px;
120
+ }
121
+ .step:last-child { margin-bottom: 0; }
122
+ .step-num {
123
+ background: #6366f1;
124
+ color: white;
125
+ width: 24px;
126
+ height: 24px;
127
+ border-radius: 50%;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ font-size: 12px;
132
+ font-weight: 600;
133
+ margin-right: 12px;
134
+ flex-shrink: 0;
135
+ }
136
+ .step-text {
137
+ color: #d4d4d8;
138
+ font-size: 14px;
139
+ line-height: 24px;
140
+ }
141
+ .step-text a {
142
+ color: #818cf8;
143
+ text-decoration: none;
144
+ }
145
+ .step-text a:hover { text-decoration: underline; }
146
+ input {
147
+ width: 100%;
148
+ padding: 12px 16px;
149
+ border: 2px solid #3f3f46;
150
+ border-radius: 8px;
151
+ background: #27273a;
152
+ color: #f4f4f5;
153
+ font-size: 14px;
154
+ font-family: monospace;
155
+ margin-bottom: 16px;
156
+ transition: border-color 0.2s;
157
+ }
158
+ input:focus {
159
+ outline: none;
160
+ border-color: #6366f1;
161
+ }
162
+ input::placeholder { color: #71717a; }
163
+ button {
164
+ width: 100%;
165
+ padding: 12px 24px;
166
+ background: #6366f1;
167
+ color: white;
168
+ border: none;
169
+ border-radius: 8px;
170
+ font-size: 14px;
171
+ font-weight: 600;
172
+ cursor: pointer;
173
+ transition: background 0.2s;
174
+ }
175
+ button:hover { background: #4f46e5; }
176
+ button:disabled {
177
+ background: #3f3f46;
178
+ cursor: not-allowed;
179
+ }
180
+ .error {
181
+ background: #7f1d1d;
182
+ color: #fecaca;
183
+ padding: 12px;
184
+ border-radius: 8px;
185
+ margin-bottom: 16px;
186
+ font-size: 14px;
187
+ display: none;
188
+ }
189
+ .success {
190
+ text-align: center;
191
+ padding: 40px 0;
192
+ }
193
+ .success-icon {
194
+ font-size: 48px;
195
+ margin-bottom: 16px;
196
+ }
197
+ .success h2 {
198
+ color: #4ade80;
199
+ margin: 0 0 8px 0;
200
+ }
201
+ .success p {
202
+ color: #a1a1aa;
203
+ margin: 0;
204
+ }
205
+ </style>
206
+ </head>
207
+ <body>
208
+ <div class="container">
209
+ <div id="form-view">
210
+ <h1>Foundation CLI</h1>
211
+ <p class="subtitle">Authenticate with your Anthropic API key</p>
212
+
213
+ <div class="steps">
214
+ <div class="step">
215
+ <span class="step-num">1</span>
216
+ <span class="step-text">Go to <a href="https://console.anthropic.com/settings/keys" target="_blank">console.anthropic.com/settings/keys</a></span>
217
+ </div>
218
+ <div class="step">
219
+ <span class="step-num">2</span>
220
+ <span class="step-text">Create a new API key (or copy existing)</span>
221
+ </div>
222
+ <div class="step">
223
+ <span class="step-num">3</span>
224
+ <span class="step-text">Paste it below and click Submit</span>
225
+ </div>
226
+ </div>
227
+
228
+ <div class="error" id="error"></div>
229
+
230
+ <form id="key-form">
231
+ <input type="password" id="api-key" name="key" placeholder="sk-ant-api03-..." autocomplete="off" autofocus />
232
+ <button type="submit" id="submit-btn">Submit API Key</button>
233
+ </form>
234
+ </div>
235
+
236
+ <div id="success-view" class="success" style="display: none;">
237
+ <div class="success-icon">✓</div>
238
+ <h2>Login Successful</h2>
239
+ <p>You can close this window and return to the terminal.</p>
240
+ </div>
241
+ </div>
242
+
243
+ <script>
244
+ const form = document.getElementById('key-form');
245
+ const input = document.getElementById('api-key');
246
+ const error = document.getElementById('error');
247
+ const btn = document.getElementById('submit-btn');
248
+ const formView = document.getElementById('form-view');
249
+ const successView = document.getElementById('success-view');
250
+
251
+ form.addEventListener('submit', async (e) => {
252
+ e.preventDefault();
253
+ const key = input.value.trim();
254
+
255
+ if (!key) {
256
+ error.textContent = 'Please enter your API key';
257
+ error.style.display = 'block';
258
+ return;
259
+ }
260
+
261
+ if (!key.startsWith('sk-ant-api')) {
262
+ error.textContent = 'Invalid key format. Should start with sk-ant-api (not OAuth tokens)';
263
+ error.style.display = 'block';
264
+ return;
265
+ }
266
+
267
+ error.style.display = 'none';
268
+ btn.disabled = true;
269
+ btn.textContent = 'Authenticating...';
270
+
271
+ try {
272
+ const res = await fetch('/callback', {
273
+ method: 'POST',
274
+ headers: { 'Content-Type': 'application/json' },
275
+ body: JSON.stringify({ key })
276
+ });
277
+
278
+ if (res.ok) {
279
+ formView.style.display = 'none';
280
+ successView.style.display = 'block';
281
+ } else {
282
+ const data = await res.json();
283
+ error.textContent = data.error || 'Authentication failed';
284
+ error.style.display = 'block';
285
+ btn.disabled = false;
286
+ btn.textContent = 'Submit API Key';
287
+ }
288
+ } catch (err) {
289
+ error.textContent = 'Connection error. Is the CLI still running?';
290
+ error.style.display = 'block';
291
+ btn.disabled = false;
292
+ btn.textContent = 'Submit API Key';
293
+ }
294
+ });
295
+ </script>
296
+ </body>
297
+ </html>`;
298
+
299
+ export async function runLogin(options = {}) {
300
+ // Check for existing API key
301
+ const existingKey = resolveAnthropicApiKey();
302
+ if (existingKey) {
303
+ const masked = existingKey.slice(0, 15) + "..." + existingKey.slice(-4);
304
+ const { overwrite } = await inquirer.prompt([
305
+ {
306
+ type: "confirm",
307
+ name: "overwrite",
308
+ message: `Already have an API key (${masked}). Replace it?`,
309
+ default: false,
310
+ },
311
+ ]);
312
+ if (!overwrite) {
313
+ console.log(chalk.gray("Keeping existing credentials."));
314
+ return true;
315
+ }
316
+ }
317
+
318
+ // Device flow with browser
319
+ if (options.browser !== false) {
320
+ return runDeviceLogin();
321
+ }
322
+
323
+ // Terminal-only flow
324
+ console.log(chalk.blue("\nGet an API key from: " + ANTHROPIC_KEYS_URL + "\n"));
325
+
326
+ const { apiKey } = await inquirer.prompt([
327
+ {
328
+ type: "password",
329
+ name: "apiKey",
330
+ message: "Paste your API key:",
331
+ mask: "*",
332
+ validate: (v) => {
333
+ const trimmed = v?.trim();
334
+ if (!trimmed) return "API key is required.";
335
+ if (!trimmed.startsWith("sk-ant-api")) return "Invalid key format. Should start with sk-ant-api";
336
+ return true;
337
+ },
338
+ },
339
+ ]);
340
+ return saveApiKey(apiKey);
341
+ }
342
+
343
+ async function runDeviceLogin() {
344
+ return new Promise((resolve) => {
345
+ const server = http.createServer(async (req, res) => {
346
+ if (req.method === "GET" && req.url === "/") {
347
+ res.writeHead(200, { "Content-Type": "text/html" });
348
+ res.end(LOGIN_HTML);
349
+ return;
350
+ }
351
+
352
+ if (req.method === "POST" && req.url === "/callback") {
353
+ let body = "";
354
+ req.on("data", (chunk) => { body += chunk; });
355
+ req.on("end", async () => {
356
+ try {
357
+ const { key } = JSON.parse(body);
358
+
359
+ if (!key || !key.startsWith("sk-ant-api")) {
360
+ res.writeHead(400, { "Content-Type": "application/json" });
361
+ res.end(JSON.stringify({ error: "Invalid API key format. Use an API key from console.anthropic.com (starts with sk-ant-api)" }));
362
+ return;
363
+ }
364
+
365
+ // Optionally validate key with Anthropic
366
+ try {
367
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
368
+ const client = new Anthropic({ apiKey: key });
369
+ await client.messages.create({
370
+ model: "claude-3-haiku-20240307",
371
+ max_tokens: 1,
372
+ messages: [{ role: "user", content: "hi" }],
373
+ });
374
+ } catch (apiErr) {
375
+ if (apiErr?.status === 401) {
376
+ res.writeHead(401, { "Content-Type": "application/json" });
377
+ res.end(JSON.stringify({ error: "Invalid API key" }));
378
+ return;
379
+ }
380
+ // Other errors (rate limit, etc) - key might still be valid
381
+ }
382
+
383
+ res.writeHead(200, { "Content-Type": "application/json" });
384
+ res.end(JSON.stringify({ success: true }));
385
+
386
+ // Save and close
387
+ saveApiKey(key);
388
+ server.close();
389
+ resolve(true);
390
+ } catch {
391
+ res.writeHead(400, { "Content-Type": "application/json" });
392
+ res.end(JSON.stringify({ error: "Invalid request" }));
393
+ }
394
+ });
395
+ return;
396
+ }
397
+
398
+ res.writeHead(404);
399
+ res.end();
400
+ });
401
+
402
+ // Find available port
403
+ server.listen(0, "127.0.0.1", () => {
404
+ const { port } = server.address();
405
+ const url = `http://127.0.0.1:${port}`;
406
+
407
+ console.log(chalk.blue("\nOpening browser for authentication...\n"));
408
+ console.log(chalk.gray(` If browser doesn't open, visit: ${chalk.cyan(url)}\n`));
409
+ console.log(chalk.gray(" Waiting for authentication..."));
410
+
411
+ openBrowser(url);
412
+ });
413
+
414
+ // Timeout after 5 minutes
415
+ setTimeout(() => {
416
+ console.log(chalk.yellow("\n Login timed out. Run foundation login again.\n"));
417
+ server.close();
418
+ resolve(false);
419
+ }, 5 * 60 * 1000);
420
+ });
421
+ }
@@ -0,0 +1,192 @@
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
+ });