@meshxdata/fops 0.0.1 → 0.0.4
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 +161 -68
- package/src/agent/agents.js +224 -0
- package/src/agent/context.js +287 -96
- package/src/agent/index.js +1 -0
- package/src/agent/llm.js +134 -20
- package/src/auth/coda.js +128 -0
- package/src/auth/index.js +1 -0
- package/src/auth/login.js +13 -13
- package/src/auth/oauth.js +4 -4
- package/src/commands/index.js +94 -21
- package/src/config.js +2 -2
- package/src/doctor.js +208 -22
- package/src/feature-flags.js +197 -0
- package/src/plugins/api.js +23 -0
- package/src/plugins/builtins/stack-api.js +36 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/knowledge.js +124 -0
- package/src/plugins/loader.js +67 -0
- package/src/plugins/registry.js +3 -0
- package/src/project.js +20 -1
- package/src/setup/aws.js +7 -7
- package/src/setup/setup.js +18 -12
- package/src/setup/wizard.js +86 -15
- package/src/shell.js +2 -2
- package/src/skills/foundation/SKILL.md +200 -66
- package/src/ui/confirm.js +3 -2
- package/src/ui/input.js +31 -34
- package/src/ui/spinner.js +39 -13
- package/src/ui/streaming.js +2 -2
- 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
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
|
-
});
|
package/src/config.test.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import { PKG, CLI_BRAND, printFoundationBanner } from "./config.js";
|
|
4
|
-
|
|
5
|
-
describe("config", () => {
|
|
6
|
-
describe("PKG", () => {
|
|
7
|
-
it("exports name and version", () => {
|
|
8
|
-
expect(PKG.name).toBe("@meshxdata/fops");
|
|
9
|
-
expect(typeof PKG.version).toBe("string");
|
|
10
|
-
expect(PKG.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("only exposes name and version (no extra fields)", () => {
|
|
14
|
-
expect(Object.keys(PKG).sort()).toEqual(["name", "version"]);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("CLI_BRAND", () => {
|
|
19
|
-
it("has the expected shape", () => {
|
|
20
|
-
expect(CLI_BRAND.title).toBe("Foundation Operator CLI");
|
|
21
|
-
expect(CLI_BRAND.version).toMatch(/^v\d+/);
|
|
22
|
-
expect(CLI_BRAND.byline).toContain("meshx");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("version matches PKG.version", () => {
|
|
26
|
-
expect(CLI_BRAND.version).toBe(`v${PKG.version}`);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("printFoundationBanner", () => {
|
|
31
|
-
it("prints CLI info to stdout", () => {
|
|
32
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
33
|
-
printFoundationBanner("/tmp/test-project");
|
|
34
|
-
expect(spy).toHaveBeenCalled();
|
|
35
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
36
|
-
expect(output).toContain("Foundation Operator CLI");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("shortens home dir to ~", () => {
|
|
40
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
41
|
-
const home = os.homedir();
|
|
42
|
-
printFoundationBanner(home + "/projects/test");
|
|
43
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
44
|
-
expect(output).toContain("~/projects/test");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("does not replace ~ when path is not under homedir", () => {
|
|
48
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
49
|
-
printFoundationBanner("/opt/data/project");
|
|
50
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
51
|
-
expect(output).toContain("/opt/data/project");
|
|
52
|
-
expect(output).not.toContain("~");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("prints version and byline", () => {
|
|
56
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
57
|
-
printFoundationBanner("/tmp");
|
|
58
|
-
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
59
|
-
expect(output).toContain(CLI_BRAND.version);
|
|
60
|
-
expect(output).toContain("meshx");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("prints a trailing blank line", () => {
|
|
64
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
65
|
-
printFoundationBanner("/tmp");
|
|
66
|
-
// Last call should be empty string (blank line)
|
|
67
|
-
expect(spy.mock.calls[spy.mock.calls.length - 1][0]).toBe("");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
});
|
package/src/doctor.test.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const doctorSource = fs.readFileSync(path.join(__dirname, "doctor.js"), "utf8");
|
|
8
|
-
|
|
9
|
-
// readNetrcToken is not exported, so we inline a copy for testing
|
|
10
|
-
function readNetrcToken(content, machine) {
|
|
11
|
-
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
12
|
-
let inMachine = false;
|
|
13
|
-
for (const line of lines) {
|
|
14
|
-
const tokens = line.trim().split(/\s+/);
|
|
15
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
16
|
-
if (tokens[i] === "machine" && tokens[i + 1] === machine) {
|
|
17
|
-
inMachine = true;
|
|
18
|
-
} else if (tokens[i] === "machine" && tokens[i + 1] !== machine) {
|
|
19
|
-
if (inMachine) return null;
|
|
20
|
-
}
|
|
21
|
-
if (inMachine && tokens[i] === "password" && tokens[i + 1]) {
|
|
22
|
-
return tokens[i + 1];
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("doctor", () => {
|
|
30
|
-
describe("readNetrcToken", () => {
|
|
31
|
-
it("returns token for multi-line netrc", () => {
|
|
32
|
-
const content = `machine github.com\nlogin user\npassword ghp_abc123\n`;
|
|
33
|
-
expect(readNetrcToken(content, "github.com")).toBe("ghp_abc123");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns token for single-line netrc", () => {
|
|
37
|
-
const content = `machine github.com login user password ghp_abc123`;
|
|
38
|
-
expect(readNetrcToken(content, "github.com")).toBe("ghp_abc123");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("returns null for wrong machine", () => {
|
|
42
|
-
const content = `machine gitlab.com\nlogin user\npassword glpat_xyz\n`;
|
|
43
|
-
expect(readNetrcToken(content, "github.com")).toBe(null);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns null for missing password", () => {
|
|
47
|
-
const content = `machine github.com\nlogin user\n`;
|
|
48
|
-
expect(readNetrcToken(content, "github.com")).toBe(null);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("handles multiple machines", () => {
|
|
52
|
-
const content = [
|
|
53
|
-
"machine gitlab.com login a password glpat_xyz",
|
|
54
|
-
"machine github.com login b password ghp_123",
|
|
55
|
-
].join("\n");
|
|
56
|
-
expect(readNetrcToken(content, "github.com")).toBe("ghp_123");
|
|
57
|
-
expect(readNetrcToken(content, "gitlab.com")).toBe("glpat_xyz");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("stops at next machine block", () => {
|
|
61
|
-
const content = [
|
|
62
|
-
"machine github.com",
|
|
63
|
-
"login user",
|
|
64
|
-
"machine other.com",
|
|
65
|
-
"password secret",
|
|
66
|
-
].join("\n");
|
|
67
|
-
expect(readNetrcToken(content, "github.com")).toBe(null);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("handles windows line endings", () => {
|
|
71
|
-
const content = "machine github.com\r\nlogin user\r\npassword ghp_win\r\n";
|
|
72
|
-
expect(readNetrcToken(content, "github.com")).toBe("ghp_win");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("handles empty content", () => {
|
|
76
|
-
expect(readNetrcToken("", "github.com")).toBe(null);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("handles whitespace-only content", () => {
|
|
80
|
-
expect(readNetrcToken(" \n \n ", "github.com")).toBe(null);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("handles machine with no following tokens", () => {
|
|
84
|
-
const content = "machine\ngithub.com\npassword ghp_123";
|
|
85
|
-
// "machine" without immediate next token matching
|
|
86
|
-
expect(readNetrcToken(content, "github.com")).toBe(null);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("handles mixed multi-line and single-line entries", () => {
|
|
90
|
-
const content = [
|
|
91
|
-
"machine gitlab.com login gl password glpat_abc",
|
|
92
|
-
"",
|
|
93
|
-
"machine github.com",
|
|
94
|
-
"login gh",
|
|
95
|
-
"password ghp_xyz",
|
|
96
|
-
].join("\n");
|
|
97
|
-
expect(readNetrcToken(content, "github.com")).toBe("ghp_xyz");
|
|
98
|
-
expect(readNetrcToken(content, "gitlab.com")).toBe("glpat_abc");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("returns first password for a machine", () => {
|
|
102
|
-
const content = "machine github.com login user password first_token";
|
|
103
|
-
expect(readNetrcToken(content, "github.com")).toBe("first_token");
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("KEY_PORTS", () => {
|
|
108
|
-
it("defines all expected ports", () => {
|
|
109
|
-
expect(doctorSource).toContain("5432");
|
|
110
|
-
expect(doctorSource).toContain("Postgres");
|
|
111
|
-
expect(doctorSource).toContain("9092");
|
|
112
|
-
expect(doctorSource).toContain("Kafka");
|
|
113
|
-
expect(doctorSource).toContain("9001");
|
|
114
|
-
expect(doctorSource).toContain("Backend");
|
|
115
|
-
expect(doctorSource).toContain("3002");
|
|
116
|
-
expect(doctorSource).toContain("Frontend");
|
|
117
|
-
expect(doctorSource).toContain("8081");
|
|
118
|
-
expect(doctorSource).toContain("Trino");
|
|
119
|
-
expect(doctorSource).toContain("9083");
|
|
120
|
-
expect(doctorSource).toContain("Hive Metastore");
|
|
121
|
-
expect(doctorSource).toContain("8181");
|
|
122
|
-
expect(doctorSource).toContain("OPA");
|
|
123
|
-
expect(doctorSource).toContain("18201");
|
|
124
|
-
expect(doctorSource).toContain("Vault");
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("runDoctor export", () => {
|
|
129
|
-
it("is exported as a function", async () => {
|
|
130
|
-
const { runDoctor } = await import("./doctor.js");
|
|
131
|
-
expect(typeof runDoctor).toBe("function");
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
});
|