@simplysm/sd-claude 13.0.69 → 13.0.71
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 +12 -601
- package/claude/agents/sd-api-reviewer.md +0 -1
- package/claude/agents/sd-code-reviewer.md +0 -1
- package/claude/agents/sd-code-simplifier.md +1 -1
- package/claude/agents/sd-security-reviewer.md +0 -1
- package/claude/refs/sd-angular.md +26 -26
- package/claude/refs/sd-orm-v12.md +17 -17
- package/claude/rules/sd-refs-linker.md +14 -14
- package/claude/sd-statusline.js +21 -21
- package/claude/skills/sd-api-name-review/SKILL.md +1 -2
- package/claude/skills/sd-brainstorm/SKILL.md +3 -4
- package/claude/skills/sd-check/SKILL.md +1 -2
- package/claude/skills/sd-commit/SKILL.md +2 -3
- package/claude/skills/sd-debug/SKILL.md +1 -2
- package/claude/skills/sd-discuss/SKILL.md +1 -3
- package/claude/skills/sd-document/SKILL.md +99 -0
- package/claude/skills/sd-document/extract_docx.py +92 -0
- package/claude/skills/sd-document/extract_pdf.py +102 -0
- package/claude/skills/sd-document/extract_pptx.py +77 -0
- package/claude/skills/sd-document/extract_xlsx.py +83 -0
- package/claude/skills/sd-email-analyze/SKILL.md +6 -6
- package/claude/skills/sd-plan/SKILL.md +1 -3
- package/claude/skills/sd-plan-dev/SKILL.md +94 -111
- package/claude/skills/sd-plan-dev/code-quality-reviewer-prompt.md +1 -1
- package/claude/skills/sd-plan-dev/final-review-prompt.md +1 -1
- package/claude/skills/sd-plan-dev/spec-reviewer-prompt.md +1 -1
- package/claude/skills/sd-readme/SKILL.md +107 -88
- package/claude/skills/sd-review/SKILL.md +14 -16
- package/claude/skills/sd-skill/SKILL.md +6 -317
- package/claude/skills/sd-skill/cso-guide.md +161 -0
- package/claude/skills/sd-skill/writing-guide.md +163 -0
- package/claude/skills/sd-tdd/SKILL.md +1 -3
- package/claude/skills/sd-use/SKILL.md +1 -3
- package/claude/skills/sd-worktree/SKILL.md +52 -2
- package/dist/commands/auth-add.d.ts +2 -0
- package/dist/commands/auth-add.d.ts.map +1 -0
- package/dist/commands/auth-add.js +32 -0
- package/dist/commands/auth-add.js.map +6 -0
- package/dist/commands/auth-list.d.ts +2 -0
- package/dist/commands/auth-list.d.ts.map +1 -0
- package/dist/commands/auth-list.js +37 -0
- package/dist/commands/auth-list.js.map +6 -0
- package/dist/commands/auth-remove.d.ts +2 -0
- package/dist/commands/auth-remove.d.ts.map +1 -0
- package/dist/commands/auth-remove.js +22 -0
- package/dist/commands/auth-remove.js.map +6 -0
- package/dist/commands/auth-use.d.ts +2 -0
- package/dist/commands/auth-use.d.ts.map +1 -0
- package/dist/commands/auth-use.js +33 -0
- package/dist/commands/auth-use.js.map +6 -0
- package/dist/commands/auth-utils.d.ts +11 -0
- package/dist/commands/auth-utils.d.ts.map +1 -0
- package/dist/commands/auth-utils.js +57 -0
- package/dist/commands/auth-utils.js.map +6 -0
- package/dist/commands/install.js +3 -3
- package/dist/commands/install.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/sd-claude.js +68 -3
- package/dist/sd-claude.js.map +1 -1
- package/package.json +3 -2
- package/scripts/sync-claude-assets.mjs +1 -1
- package/src/commands/auth-add.ts +36 -0
- package/src/commands/auth-list.ts +44 -0
- package/src/commands/auth-remove.ts +26 -0
- package/src/commands/auth-use.ts +53 -0
- package/src/commands/auth-utils.ts +65 -0
- package/src/commands/install.ts +19 -19
- package/src/index.ts +5 -0
- package/src/sd-claude.ts +81 -3
- package/tests/auth-add.spec.ts +74 -0
- package/tests/auth-list.spec.ts +175 -0
- package/tests/auth-remove.spec.ts +74 -0
- package/tests/auth-use.spec.ts +153 -0
- package/tests/auth-utils.spec.ts +173 -0
- package/claude/skills/sd-explore/SKILL.md +0 -78
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { runAuthRemove } from "../src/commands/auth-remove";
|
|
6
|
+
|
|
7
|
+
describe("runAuthRemove", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-claude-auth-remove-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("removes profile directory successfully", () => {
|
|
20
|
+
// Set up a profile directory with auth.json
|
|
21
|
+
const profileDir = path.join(tmpDir, ".sd-claude", "auth", "work");
|
|
22
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(profileDir, "auth.json"),
|
|
25
|
+
JSON.stringify({ userID: "user-123", oauthAccount: {} }),
|
|
26
|
+
);
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(profileDir, "credentials.json"),
|
|
29
|
+
JSON.stringify({ accessToken: "abc" }),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Set up .claude.json with a DIFFERENT userID (not active)
|
|
33
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-999" }));
|
|
34
|
+
|
|
35
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
36
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
37
|
+
|
|
38
|
+
runAuthRemove("work", tmpDir);
|
|
39
|
+
|
|
40
|
+
expect(fs.existsSync(profileDir)).toBe(false);
|
|
41
|
+
expect(logSpy).toHaveBeenCalledWith("Removed profile 'work'");
|
|
42
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("warns when removing active profile (still removes it)", () => {
|
|
46
|
+
// Set up a profile directory with auth.json
|
|
47
|
+
const profileDir = path.join(tmpDir, ".sd-claude", "auth", "work");
|
|
48
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
path.join(profileDir, "auth.json"),
|
|
51
|
+
JSON.stringify({ userID: "user-123", oauthAccount: {} }),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Set up .claude.json with the SAME userID (active)
|
|
55
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-123" }));
|
|
56
|
+
|
|
57
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
58
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
59
|
+
|
|
60
|
+
runAuthRemove("work", tmpDir);
|
|
61
|
+
|
|
62
|
+
expect(fs.existsSync(profileDir)).toBe(false);
|
|
63
|
+
expect(warnSpy).toHaveBeenCalledWith("Warning: 'work' is currently active.");
|
|
64
|
+
expect(logSpy).toHaveBeenCalledWith("Removed profile 'work'");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("throws when profile not found", () => {
|
|
68
|
+
expect(() => runAuthRemove("nonexistent", tmpDir)).toThrow("Profile 'nonexistent' not found.");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("throws with invalid name", () => {
|
|
72
|
+
expect(() => runAuthRemove("BAD NAME!", tmpDir)).toThrow("Invalid name");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { runAuthUse } from "../src/commands/auth-use";
|
|
6
|
+
|
|
7
|
+
describe("runAuthUse", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-claude-auth-use-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function setupProfile(name: string, opts?: { expiresAt?: number }): void {
|
|
20
|
+
const profileDir = path.join(tmpDir, ".sd-claude", "auth", name);
|
|
21
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
const authJson = {
|
|
24
|
+
oauthAccount: { emailAddress: "work@example.com", token: "oauth-work" },
|
|
25
|
+
userID: "user-work-456",
|
|
26
|
+
};
|
|
27
|
+
fs.writeFileSync(path.join(profileDir, "auth.json"), JSON.stringify(authJson));
|
|
28
|
+
|
|
29
|
+
const credJson = {
|
|
30
|
+
claudeAiOauth: {
|
|
31
|
+
accessToken: "saved-access-token",
|
|
32
|
+
refreshToken: "saved-refresh-token",
|
|
33
|
+
expiresAt: opts?.expiresAt ?? Date.now() + 3_600_000,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
fs.writeFileSync(path.join(profileDir, "credentials.json"), JSON.stringify(credJson));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setupClaudeJson(extra?: Record<string, unknown>): void {
|
|
40
|
+
const claudeJson = {
|
|
41
|
+
oauthAccount: { emailAddress: "old@example.com", token: "old-token" },
|
|
42
|
+
userID: "user-old-111",
|
|
43
|
+
someCustomSetting: "preserve-me",
|
|
44
|
+
anotherField: 42,
|
|
45
|
+
...extra,
|
|
46
|
+
};
|
|
47
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify(claudeJson));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setupCredentialsJson(): void {
|
|
51
|
+
const claudeDir = path.join(tmpDir, ".claude");
|
|
52
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
53
|
+
const credentials = { oldKey: "old-value", claudeAiOauth: { accessToken: "old-access" } };
|
|
54
|
+
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(credentials));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("replaces oauthAccount and userID while preserving other fields in ~/.claude.json", () => {
|
|
58
|
+
setupProfile("work");
|
|
59
|
+
setupClaudeJson();
|
|
60
|
+
setupCredentialsJson();
|
|
61
|
+
|
|
62
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
63
|
+
|
|
64
|
+
runAuthUse("work", tmpDir);
|
|
65
|
+
|
|
66
|
+
const result = JSON.parse(
|
|
67
|
+
fs.readFileSync(path.join(tmpDir, ".claude.json"), "utf-8"),
|
|
68
|
+
) as Record<string, unknown>;
|
|
69
|
+
|
|
70
|
+
// oauthAccount and userID should be replaced
|
|
71
|
+
expect(result["oauthAccount"]).toEqual({
|
|
72
|
+
emailAddress: "work@example.com",
|
|
73
|
+
token: "oauth-work",
|
|
74
|
+
});
|
|
75
|
+
expect(result["userID"]).toBe("user-work-456");
|
|
76
|
+
|
|
77
|
+
// Other fields must be preserved
|
|
78
|
+
expect(result["someCustomSetting"]).toBe("preserve-me");
|
|
79
|
+
expect(result["anotherField"]).toBe(42);
|
|
80
|
+
|
|
81
|
+
logSpy.mockRestore();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("replaces credentials.json entirely", () => {
|
|
85
|
+
setupProfile("work");
|
|
86
|
+
setupClaudeJson();
|
|
87
|
+
setupCredentialsJson();
|
|
88
|
+
|
|
89
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
90
|
+
|
|
91
|
+
runAuthUse("work", tmpDir);
|
|
92
|
+
|
|
93
|
+
const result = JSON.parse(
|
|
94
|
+
fs.readFileSync(path.join(tmpDir, ".claude", ".credentials.json"), "utf-8"),
|
|
95
|
+
) as Record<string, unknown>;
|
|
96
|
+
|
|
97
|
+
// Should be entirely replaced with saved credentials (no oldKey)
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
claudeAiOauth: {
|
|
100
|
+
accessToken: "saved-access-token",
|
|
101
|
+
refreshToken: "saved-refresh-token",
|
|
102
|
+
expiresAt: expect.any(Number) as number,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
expect(result["oldKey"]).toBeUndefined();
|
|
106
|
+
|
|
107
|
+
logSpy.mockRestore();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("warns when token is expired (should not throw)", () => {
|
|
111
|
+
const expiredTime = Date.now() - 1_000;
|
|
112
|
+
setupProfile("work", { expiresAt: expiredTime });
|
|
113
|
+
setupClaudeJson();
|
|
114
|
+
setupCredentialsJson();
|
|
115
|
+
|
|
116
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
117
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
118
|
+
|
|
119
|
+
// Should NOT throw
|
|
120
|
+
expect(() => runAuthUse("work", tmpDir)).not.toThrow();
|
|
121
|
+
|
|
122
|
+
// Should warn about expired token
|
|
123
|
+
expect(warnSpy).toHaveBeenCalledWith("Warning: Token expired. Run /login after switching.");
|
|
124
|
+
|
|
125
|
+
warnSpy.mockRestore();
|
|
126
|
+
logSpy.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("throws when profile not found", () => {
|
|
130
|
+
setupClaudeJson();
|
|
131
|
+
setupCredentialsJson();
|
|
132
|
+
|
|
133
|
+
expect(() => runAuthUse("nonexistent", tmpDir)).toThrow("Profile 'nonexistent' not found.");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("throws with invalid name", () => {
|
|
137
|
+
expect(() => runAuthUse("BAD NAME!", tmpDir)).toThrow("Invalid name");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("logs switched message with name and email", () => {
|
|
141
|
+
setupProfile("work");
|
|
142
|
+
setupClaudeJson();
|
|
143
|
+
setupCredentialsJson();
|
|
144
|
+
|
|
145
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
146
|
+
|
|
147
|
+
runAuthUse("work", tmpDir);
|
|
148
|
+
|
|
149
|
+
expect(logSpy).toHaveBeenCalledWith("Switched to work (work@example.com)");
|
|
150
|
+
|
|
151
|
+
logSpy.mockRestore();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import {
|
|
6
|
+
validateName,
|
|
7
|
+
getProfileDir,
|
|
8
|
+
profileExists,
|
|
9
|
+
listProfiles,
|
|
10
|
+
readCurrentAuth,
|
|
11
|
+
readCurrentCredentials,
|
|
12
|
+
getCurrentUserID,
|
|
13
|
+
} from "../src/commands/auth-utils";
|
|
14
|
+
|
|
15
|
+
describe("auth-utils", () => {
|
|
16
|
+
let tmpDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-claude-auth-test-"));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("validateName", () => {
|
|
27
|
+
test("accepts lowercase letters", () => {
|
|
28
|
+
expect(() => validateName("abc")).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("accepts digits", () => {
|
|
32
|
+
expect(() => validateName("123")).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("accepts hyphens and underscores", () => {
|
|
36
|
+
expect(() => validateName("my-profile_1")).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("rejects uppercase letters", () => {
|
|
40
|
+
expect(() => validateName("ABC")).toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("rejects spaces", () => {
|
|
44
|
+
expect(() => validateName("my profile")).toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("rejects special characters", () => {
|
|
48
|
+
expect(() => validateName("my@profile")).toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("rejects empty string", () => {
|
|
52
|
+
expect(() => validateName("")).toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("error message includes the invalid name", () => {
|
|
56
|
+
expect(() => validateName("BAD!")).toThrow(/BAD!/);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("getProfileDir", () => {
|
|
61
|
+
test("returns correct path with homeDir", () => {
|
|
62
|
+
const result = getProfileDir("work", tmpDir);
|
|
63
|
+
expect(result).toBe(path.join(tmpDir, ".sd-claude", "auth", "work"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("uses os.homedir when homeDir is not provided", () => {
|
|
67
|
+
const result = getProfileDir("test");
|
|
68
|
+
expect(result).toBe(path.join(os.homedir(), ".sd-claude", "auth", "test"));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("profileExists", () => {
|
|
73
|
+
test("returns false when profile directory does not exist", () => {
|
|
74
|
+
expect(profileExists("nonexistent", tmpDir)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns true when profile directory exists", () => {
|
|
78
|
+
const profileDir = path.join(tmpDir, ".sd-claude", "auth", "work");
|
|
79
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
80
|
+
expect(profileExists("work", tmpDir)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("listProfiles", () => {
|
|
85
|
+
test("returns empty array when auth directory does not exist", () => {
|
|
86
|
+
expect(listProfiles(tmpDir)).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("returns subdirectory names", () => {
|
|
90
|
+
const authDir = path.join(tmpDir, ".sd-claude", "auth");
|
|
91
|
+
fs.mkdirSync(path.join(authDir, "work"), { recursive: true });
|
|
92
|
+
fs.mkdirSync(path.join(authDir, "personal"), { recursive: true });
|
|
93
|
+
|
|
94
|
+
const result = listProfiles(tmpDir);
|
|
95
|
+
expect(result.sort()).toEqual(["personal", "work"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("ignores files in auth directory", () => {
|
|
99
|
+
const authDir = path.join(tmpDir, ".sd-claude", "auth");
|
|
100
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
101
|
+
fs.writeFileSync(path.join(authDir, "some-file.txt"), "data");
|
|
102
|
+
fs.mkdirSync(path.join(authDir, "profile1"), { recursive: true });
|
|
103
|
+
|
|
104
|
+
expect(listProfiles(tmpDir)).toEqual(["profile1"]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("readCurrentAuth", () => {
|
|
109
|
+
test("reads oauthAccount and userID from .claude.json", () => {
|
|
110
|
+
const claudeJson = {
|
|
111
|
+
oauthAccount: { email: "test@example.com", token: "abc" },
|
|
112
|
+
userID: "user-123",
|
|
113
|
+
};
|
|
114
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify(claudeJson));
|
|
115
|
+
|
|
116
|
+
const result = readCurrentAuth(tmpDir);
|
|
117
|
+
expect(result.oauthAccount).toEqual({ email: "test@example.com", token: "abc" });
|
|
118
|
+
expect(result.userID).toBe("user-123");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("throws when .claude.json does not exist", () => {
|
|
122
|
+
expect(() => readCurrentAuth(tmpDir)).toThrow();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("throws when oauthAccount is missing", () => {
|
|
126
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-123" }));
|
|
127
|
+
expect(() => readCurrentAuth(tmpDir)).toThrow("Not logged in");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("throws when userID is missing", () => {
|
|
131
|
+
fs.writeFileSync(
|
|
132
|
+
path.join(tmpDir, ".claude.json"),
|
|
133
|
+
JSON.stringify({ oauthAccount: { email: "test@example.com" } }),
|
|
134
|
+
);
|
|
135
|
+
expect(() => readCurrentAuth(tmpDir)).toThrow("Not logged in");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("readCurrentCredentials", () => {
|
|
140
|
+
test("reads credentials from .claude/.credentials.json", () => {
|
|
141
|
+
const credentials = { key: "value", secret: "s3cret" };
|
|
142
|
+
const claudeDir = path.join(tmpDir, ".claude");
|
|
143
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
144
|
+
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(credentials));
|
|
145
|
+
|
|
146
|
+
const result = readCurrentCredentials(tmpDir);
|
|
147
|
+
expect(result).toEqual({ key: "value", secret: "s3cret" });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("throws when .credentials.json does not exist", () => {
|
|
151
|
+
expect(() => readCurrentCredentials(tmpDir)).toThrow();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("getCurrentUserID", () => {
|
|
156
|
+
test("returns userID from .claude.json", () => {
|
|
157
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-456" }));
|
|
158
|
+
expect(getCurrentUserID(tmpDir)).toBe("user-456");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("returns undefined when .claude.json does not exist", () => {
|
|
162
|
+
expect(getCurrentUserID(tmpDir)).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("returns undefined when userID is missing from .claude.json", () => {
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
path.join(tmpDir, ".claude.json"),
|
|
168
|
+
JSON.stringify({ someOtherField: "value" }),
|
|
169
|
+
);
|
|
170
|
+
expect(getCurrentUserID(tmpDir)).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sd-explore
|
|
3
|
-
description: Deep codebase analysis - execution paths, architecture, dependencies
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
model: sonnet
|
|
6
|
-
context: fork
|
|
7
|
-
allowed-tools: Read, Glob, Grep
|
|
8
|
-
user-invocable: false
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# sd-explore
|
|
12
|
-
|
|
13
|
-
You are an expert code analyst specializing in tracing and understanding feature implementations across codebases.
|
|
14
|
-
|
|
15
|
-
## Target Selection
|
|
16
|
-
|
|
17
|
-
**When invoked with `$ARGUMENTS`:**
|
|
18
|
-
|
|
19
|
-
- If path is provided → **Immediately start analysis** (don't ask clarifying questions)
|
|
20
|
-
- If path is a package directory → Trace all major features, architecture, and patterns
|
|
21
|
-
- If path is a single file → Trace its role, dependencies, and usage
|
|
22
|
-
|
|
23
|
-
**Critical**: This skill is `user-invocable: false` — you are called programmatically by other agents. Start analysis immediately without user interaction.
|
|
24
|
-
|
|
25
|
-
## Core Mission
|
|
26
|
-
|
|
27
|
-
Provide a complete understanding of how the target code works by tracing its implementation from entry points to data storage, through all abstraction layers. **Analysis only — no code modifications.**
|
|
28
|
-
|
|
29
|
-
When analyzing a package/directory, cover:
|
|
30
|
-
|
|
31
|
-
- Overall architecture and design patterns
|
|
32
|
-
- Major features and entry points
|
|
33
|
-
- Key abstractions and interfaces
|
|
34
|
-
- Cross-cutting concerns
|
|
35
|
-
- Critical files with file:line references
|
|
36
|
-
|
|
37
|
-
## Analysis Approach
|
|
38
|
-
|
|
39
|
-
### 1. Feature Discovery
|
|
40
|
-
|
|
41
|
-
- Find entry points (APIs, UI components, CLI commands)
|
|
42
|
-
- Locate core implementation files
|
|
43
|
-
- Map feature boundaries and configuration
|
|
44
|
-
|
|
45
|
-
### 2. Code Flow Tracing
|
|
46
|
-
|
|
47
|
-
- Follow call chains from entry to output
|
|
48
|
-
- Trace data transformations at each step
|
|
49
|
-
- Identify all dependencies and integrations
|
|
50
|
-
- Document state changes and side effects
|
|
51
|
-
|
|
52
|
-
### 3. Architecture Analysis
|
|
53
|
-
|
|
54
|
-
- Map abstraction layers (presentation → business logic → data)
|
|
55
|
-
- Identify design patterns and architectural decisions
|
|
56
|
-
- Document interfaces between components
|
|
57
|
-
- Note cross-cutting concerns (auth, logging, caching)
|
|
58
|
-
|
|
59
|
-
### 4. Implementation Details
|
|
60
|
-
|
|
61
|
-
- Key algorithms and data structures
|
|
62
|
-
- Error handling and edge cases
|
|
63
|
-
- Performance considerations
|
|
64
|
-
- Technical debt or improvement areas
|
|
65
|
-
|
|
66
|
-
## Output Guidance
|
|
67
|
-
|
|
68
|
-
Provide a comprehensive analysis that helps developers understand the code deeply enough to modify or extend it. Include:
|
|
69
|
-
|
|
70
|
-
- Entry points with file:line references
|
|
71
|
-
- Step-by-step execution flow with data transformations
|
|
72
|
-
- Key components and their responsibilities
|
|
73
|
-
- Architecture insights: patterns, layers, design decisions
|
|
74
|
-
- Dependencies (external and internal)
|
|
75
|
-
- Observations about strengths, issues, or opportunities
|
|
76
|
-
- List of files absolutely essential to understanding the target
|
|
77
|
-
|
|
78
|
-
Structure your response for maximum clarity and usefulness. Always include specific file paths and line numbers.
|