@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.
- package/README.md +98 -0
- package/STRUCTURE.md +43 -0
- package/foundation.mjs +16 -0
- package/package.json +52 -0
- package/src/agent/agent.js +367 -0
- package/src/agent/agent.test.js +233 -0
- package/src/agent/context.js +143 -0
- package/src/agent/context.test.js +81 -0
- package/src/agent/index.js +2 -0
- package/src/agent/llm.js +127 -0
- package/src/agent/llm.test.js +139 -0
- package/src/auth/index.js +4 -0
- package/src/auth/keychain.js +58 -0
- package/src/auth/keychain.test.js +185 -0
- package/src/auth/login.js +421 -0
- package/src/auth/login.test.js +192 -0
- package/src/auth/oauth.js +203 -0
- package/src/auth/oauth.test.js +118 -0
- package/src/auth/resolve.js +78 -0
- package/src/auth/resolve.test.js +153 -0
- package/src/commands/index.js +268 -0
- package/src/config.js +24 -0
- package/src/config.test.js +70 -0
- package/src/doctor.js +487 -0
- package/src/doctor.test.js +134 -0
- package/src/plugins/api.js +37 -0
- package/src/plugins/api.test.js +95 -0
- package/src/plugins/discovery.js +78 -0
- package/src/plugins/discovery.test.js +92 -0
- package/src/plugins/hooks.js +13 -0
- package/src/plugins/hooks.test.js +118 -0
- package/src/plugins/index.js +3 -0
- package/src/plugins/loader.js +110 -0
- package/src/plugins/manifest.js +26 -0
- package/src/plugins/manifest.test.js +106 -0
- package/src/plugins/registry.js +14 -0
- package/src/plugins/registry.test.js +43 -0
- package/src/plugins/skills.js +126 -0
- package/src/plugins/skills.test.js +173 -0
- package/src/project.js +61 -0
- package/src/project.test.js +196 -0
- package/src/setup/aws.js +369 -0
- package/src/setup/aws.test.js +280 -0
- package/src/setup/index.js +3 -0
- package/src/setup/setup.js +161 -0
- package/src/setup/wizard.js +119 -0
- package/src/shell.js +9 -0
- package/src/shell.test.js +72 -0
- package/src/skills/foundation/SKILL.md +107 -0
- package/src/ui/banner.js +56 -0
- package/src/ui/banner.test.js +97 -0
- package/src/ui/confirm.js +97 -0
- package/src/ui/index.js +5 -0
- package/src/ui/input.js +199 -0
- package/src/ui/spinner.js +170 -0
- package/src/ui/spinner.test.js +29 -0
- package/src/ui/streaming.js +106 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("execa", () => ({
|
|
4
|
+
execa: vi.fn(),
|
|
5
|
+
execaSync: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("../auth/index.js", () => ({
|
|
9
|
+
resolveAnthropicApiKey: vi.fn(() => null),
|
|
10
|
+
resolveOpenAiApiKey: vi.fn(() => null),
|
|
11
|
+
readClaudeCodeKeychain: vi.fn(() => null),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("../ui/index.js", () => ({
|
|
15
|
+
renderSpinner: vi.fn(() => ({ stop: vi.fn() })),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const { execa, execaSync } = await import("execa");
|
|
19
|
+
const { readClaudeCodeKeychain } = await import("../auth/index.js");
|
|
20
|
+
const { hasClaudeCode, hasClaudeCodeAuth, runViaClaudeCode, streamViaClaudeCode } = await import("./llm.js");
|
|
21
|
+
|
|
22
|
+
describe("llm", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("hasClaudeCode", () => {
|
|
28
|
+
it("returns true when claude is on PATH", () => {
|
|
29
|
+
execaSync.mockReturnValue({ stdout: "/usr/local/bin/claude" });
|
|
30
|
+
expect(hasClaudeCode()).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns false when claude is not found", () => {
|
|
34
|
+
execaSync.mockImplementation(() => { throw new Error("not found"); });
|
|
35
|
+
expect(hasClaudeCode()).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("calls which claude", () => {
|
|
39
|
+
execaSync.mockReturnValue({ stdout: "" });
|
|
40
|
+
hasClaudeCode();
|
|
41
|
+
expect(execaSync).toHaveBeenCalledWith("which", ["claude"]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("hasClaudeCodeAuth", () => {
|
|
46
|
+
it("returns true when keychain has accessToken", () => {
|
|
47
|
+
readClaudeCodeKeychain.mockReturnValue({ accessToken: "sk-ant-oat01-xxx" });
|
|
48
|
+
expect(hasClaudeCodeAuth()).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns false when keychain returns null", () => {
|
|
52
|
+
readClaudeCodeKeychain.mockReturnValue(null);
|
|
53
|
+
expect(hasClaudeCodeAuth()).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns false when accessToken is missing", () => {
|
|
57
|
+
readClaudeCodeKeychain.mockReturnValue({ refreshToken: "rt-xxx" });
|
|
58
|
+
expect(hasClaudeCodeAuth()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns false when accessToken is empty", () => {
|
|
62
|
+
readClaudeCodeKeychain.mockReturnValue({ accessToken: "" });
|
|
63
|
+
expect(hasClaudeCodeAuth()).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns false when keychain returns empty object", () => {
|
|
67
|
+
readClaudeCodeKeychain.mockReturnValue({});
|
|
68
|
+
expect(hasClaudeCodeAuth()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("runViaClaudeCode", () => {
|
|
73
|
+
it("calls claude CLI with prompt and system prompt", async () => {
|
|
74
|
+
execa.mockResolvedValue({ stdout: "response text" });
|
|
75
|
+
const result = await runViaClaudeCode("my prompt", "system prompt");
|
|
76
|
+
expect(result).toBe("response text");
|
|
77
|
+
expect(execa).toHaveBeenCalledWith(
|
|
78
|
+
"claude",
|
|
79
|
+
["-p", "--no-session-persistence", "--append-system-prompt", "system prompt"],
|
|
80
|
+
expect.objectContaining({ input: "my prompt", encoding: "utf8" })
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("omits system prompt flag when not provided", async () => {
|
|
85
|
+
execa.mockResolvedValue({ stdout: "response" });
|
|
86
|
+
await runViaClaudeCode("prompt", null);
|
|
87
|
+
const args = execa.mock.calls[0][1];
|
|
88
|
+
expect(args).not.toContain("--append-system-prompt");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns empty string when stdout is empty", async () => {
|
|
92
|
+
execa.mockResolvedValue({ stdout: "" });
|
|
93
|
+
const result = await runViaClaudeCode("prompt", "sys");
|
|
94
|
+
expect(result).toBe("");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns empty string when stdout is null", async () => {
|
|
98
|
+
execa.mockResolvedValue({ stdout: null });
|
|
99
|
+
const result = await runViaClaudeCode("prompt", "sys");
|
|
100
|
+
expect(result).toBe("");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("streamViaClaudeCode", () => {
|
|
105
|
+
it("accumulates chunks and returns full text", async () => {
|
|
106
|
+
const { EventEmitter } = await import("node:events");
|
|
107
|
+
const mockStdout = new EventEmitter();
|
|
108
|
+
const mockProc = Promise.resolve();
|
|
109
|
+
mockProc.stdout = mockStdout;
|
|
110
|
+
|
|
111
|
+
execa.mockReturnValue(mockProc);
|
|
112
|
+
|
|
113
|
+
const chunks = [];
|
|
114
|
+
const resultPromise = streamViaClaudeCode("prompt", "sys", (chunk) => chunks.push(chunk));
|
|
115
|
+
|
|
116
|
+
mockStdout.emit("data", Buffer.from("hello "));
|
|
117
|
+
mockStdout.emit("data", Buffer.from("world"));
|
|
118
|
+
|
|
119
|
+
const result = await resultPromise;
|
|
120
|
+
expect(result).toBe("hello world");
|
|
121
|
+
expect(chunks).toEqual(["hello ", "world"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("works without onChunk callback", async () => {
|
|
125
|
+
const { EventEmitter } = await import("node:events");
|
|
126
|
+
const mockStdout = new EventEmitter();
|
|
127
|
+
const mockProc = Promise.resolve();
|
|
128
|
+
mockProc.stdout = mockStdout;
|
|
129
|
+
|
|
130
|
+
execa.mockReturnValue(mockProc);
|
|
131
|
+
|
|
132
|
+
const resultPromise = streamViaClaudeCode("prompt", "sys");
|
|
133
|
+
mockStdout.emit("data", Buffer.from("data"));
|
|
134
|
+
|
|
135
|
+
const result = await resultPromise;
|
|
136
|
+
expect(result).toBe("data");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { execaSync } from "execa";
|
|
3
|
+
|
|
4
|
+
const KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Try to read Claude Code's OAuth token from macOS Keychain.
|
|
8
|
+
* Returns { accessToken, refreshToken } or null.
|
|
9
|
+
*/
|
|
10
|
+
export function readClaudeCodeKeychain() {
|
|
11
|
+
if (process.platform !== "darwin") return null;
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = execaSync("security", [
|
|
14
|
+
"find-generic-password",
|
|
15
|
+
"-s", KEYCHAIN_SERVICE,
|
|
16
|
+
"-w"
|
|
17
|
+
], { encoding: "utf8" });
|
|
18
|
+
const data = JSON.parse(stdout.trim());
|
|
19
|
+
if (data?.claudeAiOauth?.accessToken) {
|
|
20
|
+
return {
|
|
21
|
+
accessToken: data.claudeAiOauth.accessToken,
|
|
22
|
+
refreshToken: data.claudeAiOauth.refreshToken || null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Not found or not on macOS
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Save OAuth token to macOS Keychain (same format as Claude Code).
|
|
33
|
+
*/
|
|
34
|
+
export function saveClaudeCodeKeychain(accessToken, refreshToken = null) {
|
|
35
|
+
if (process.platform !== "darwin") return false;
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.stringify({
|
|
38
|
+
claudeAiOauth: {
|
|
39
|
+
accessToken,
|
|
40
|
+
...(refreshToken && { refreshToken }),
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Delete existing entry first (ignore errors)
|
|
44
|
+
try {
|
|
45
|
+
execaSync("security", ["delete-generic-password", "-s", KEYCHAIN_SERVICE]);
|
|
46
|
+
} catch {}
|
|
47
|
+
// Add new entry
|
|
48
|
+
execaSync("security", [
|
|
49
|
+
"add-generic-password",
|
|
50
|
+
"-s", KEYCHAIN_SERVICE,
|
|
51
|
+
"-a", os.userInfo().username,
|
|
52
|
+
"-w", data,
|
|
53
|
+
]);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
});
|