@simplysm/sd-claude 13.0.82 → 13.0.83

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.
Files changed (51) hide show
  1. package/README.md +43 -0
  2. package/claude/skills/sd-plan/SKILL.md +4 -3
  3. package/package.json +3 -19
  4. package/scripts/postinstall.mjs +126 -9
  5. package/scripts/sd-entries.mjs +33 -0
  6. package/scripts/sync-claude-assets.mjs +4 -21
  7. package/dist/commands/auth-add.d.ts +0 -2
  8. package/dist/commands/auth-add.d.ts.map +0 -1
  9. package/dist/commands/auth-add.js +0 -32
  10. package/dist/commands/auth-add.js.map +0 -6
  11. package/dist/commands/auth-list.d.ts +0 -2
  12. package/dist/commands/auth-list.d.ts.map +0 -1
  13. package/dist/commands/auth-list.js +0 -95
  14. package/dist/commands/auth-list.js.map +0 -6
  15. package/dist/commands/auth-remove.d.ts +0 -2
  16. package/dist/commands/auth-remove.d.ts.map +0 -1
  17. package/dist/commands/auth-remove.js +0 -22
  18. package/dist/commands/auth-remove.js.map +0 -6
  19. package/dist/commands/auth-use.d.ts +0 -2
  20. package/dist/commands/auth-use.d.ts.map +0 -1
  21. package/dist/commands/auth-use.js +0 -33
  22. package/dist/commands/auth-use.js.map +0 -6
  23. package/dist/commands/auth-utils.d.ts +0 -11
  24. package/dist/commands/auth-utils.d.ts.map +0 -1
  25. package/dist/commands/auth-utils.js +0 -57
  26. package/dist/commands/auth-utils.js.map +0 -6
  27. package/dist/commands/install.d.ts +0 -2
  28. package/dist/commands/install.d.ts.map +0 -1
  29. package/dist/commands/install.js +0 -127
  30. package/dist/commands/install.js.map +0 -6
  31. package/dist/index.d.ts +0 -7
  32. package/dist/index.d.ts.map +0 -1
  33. package/dist/index.js +0 -7
  34. package/dist/index.js.map +0 -6
  35. package/dist/sd-claude.d.ts +0 -3
  36. package/dist/sd-claude.d.ts.map +0 -1
  37. package/dist/sd-claude.js +0 -78
  38. package/dist/sd-claude.js.map +0 -6
  39. package/src/commands/auth-add.ts +0 -36
  40. package/src/commands/auth-list.ts +0 -130
  41. package/src/commands/auth-remove.ts +0 -26
  42. package/src/commands/auth-use.ts +0 -53
  43. package/src/commands/auth-utils.ts +0 -65
  44. package/src/commands/install.ts +0 -183
  45. package/src/index.ts +0 -7
  46. package/src/sd-claude.ts +0 -98
  47. package/tests/auth-add.spec.ts +0 -74
  48. package/tests/auth-list.spec.ts +0 -198
  49. package/tests/auth-remove.spec.ts +0 -74
  50. package/tests/auth-use.spec.ts +0 -153
  51. package/tests/auth-utils.spec.ts +0 -173
@@ -1,183 +0,0 @@
1
- /**
2
- * Installs Claude Code assets to the project's .claude/ directory.
3
- * Executed via postinstall script or `sd-claude install`.
4
- */
5
- import fs from "fs";
6
- import path from "path";
7
- import { fileURLToPath } from "url";
8
-
9
- export function runInstall(): void {
10
- try {
11
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
- // dist/commands/ → package root
13
- const pkgRoot = path.resolve(__dirname, "../..");
14
- const sourceDir = path.join(pkgRoot, "claude");
15
-
16
- const projectRoot = findProjectRoot(__dirname);
17
- if (projectRoot == null) {
18
- // eslint-disable-next-line no-console
19
- console.log("[@simplysm/sd-claude] Could not find project root, skipping installation.");
20
- return;
21
- }
22
-
23
- // Skip execution if this is the simplysm monorepo with the same major version
24
- if (isSimplysmMonorepoSameMajor(projectRoot, pkgRoot)) {
25
- return;
26
- }
27
-
28
- // Skip if the source directory doesn't exist (claude/ may not exist in monorepo dev environment)
29
- if (!fs.existsSync(sourceDir)) {
30
- return;
31
- }
32
-
33
- const sourceEntries = collectSdEntries(sourceDir);
34
- if (sourceEntries.length === 0) {
35
- return;
36
- }
37
-
38
- const targetDir = path.join(projectRoot, ".claude");
39
-
40
- cleanSdEntries(targetDir);
41
- copySdEntries(sourceDir, targetDir, sourceEntries);
42
- setupSettings(targetDir);
43
-
44
- // eslint-disable-next-line no-console
45
- console.log(`[@simplysm/sd-claude] Installed ${sourceEntries.length} sd-* entries.`);
46
- } catch (err) {
47
- // Ignore errors to prevent postinstall failure from blocking the entire pnpm install
48
- // eslint-disable-next-line no-console
49
- console.warn("[@simplysm/sd-claude] postinstall warning:", (err as Error).message);
50
- }
51
- }
52
-
53
- /** Finds the project root from INIT_CWD or node_modules path. */
54
- function findProjectRoot(dirname: string): string | undefined {
55
- if (process.env["INIT_CWD"] != null) {
56
- return process.env["INIT_CWD"];
57
- }
58
-
59
- const sep = path.sep;
60
- const marker = sep + "node_modules" + sep;
61
- const idx = dirname.indexOf(marker);
62
- return idx !== -1 ? dirname.substring(0, idx) : undefined;
63
- }
64
-
65
- /** Checks if this is the simplysm monorepo with the same major version. */
66
- function isSimplysmMonorepoSameMajor(projectRoot: string, pkgRoot: string): boolean {
67
- const projectPkgPath = path.join(projectRoot, "package.json");
68
- if (!fs.existsSync(projectPkgPath)) return false;
69
-
70
- const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, "utf-8")) as {
71
- name?: string;
72
- version?: string;
73
- };
74
- if (projectPkg.name !== "simplysm") return false;
75
-
76
- const sdClaudePkgPath = path.join(pkgRoot, "package.json");
77
- if (!fs.existsSync(sdClaudePkgPath)) return false;
78
-
79
- const sdClaudePkg = JSON.parse(fs.readFileSync(sdClaudePkgPath, "utf-8")) as {
80
- version?: string;
81
- };
82
-
83
- const projectMajor = projectPkg.version?.split(".")[0];
84
- const sdClaudeMajor = sdClaudePkg.version?.split(".")[0];
85
- return projectMajor != null && projectMajor === sdClaudeMajor;
86
- }
87
-
88
- /** Recursively collects sd-* entries. */
89
- function collectSdEntries(sourceDir: string): string[] {
90
- const entries: string[] = [];
91
-
92
- // Root level: sd-*
93
- for (const name of fs.readdirSync(sourceDir)) {
94
- if (name.startsWith("sd-")) {
95
- entries.push(name);
96
- }
97
- }
98
-
99
- // Subdirectories: */sd-*
100
- for (const dirent of fs.readdirSync(sourceDir, { withFileTypes: true })) {
101
- if (!dirent.isDirectory() || dirent.name.startsWith("sd-")) continue;
102
- const subPath = path.join(sourceDir, dirent.name);
103
- for (const name of fs.readdirSync(subPath)) {
104
- if (name.startsWith("sd-")) {
105
- entries.push(path.join(dirent.name, name));
106
- }
107
- }
108
- }
109
-
110
- return entries;
111
- }
112
-
113
- /** Removes existing sd-* entries. */
114
- function cleanSdEntries(targetDir: string): void {
115
- if (!fs.existsSync(targetDir)) return;
116
-
117
- // Root level sd-*
118
- for (const name of fs.readdirSync(targetDir)) {
119
- if (name.startsWith("sd-")) {
120
- fs.rmSync(path.join(targetDir, name), { recursive: true });
121
- }
122
- }
123
-
124
- // Subdirectories */sd-*
125
- for (const dirent of fs.readdirSync(targetDir, { withFileTypes: true })) {
126
- if (!dirent.isDirectory() || dirent.name.startsWith("sd-")) continue;
127
- const subPath = path.join(targetDir, dirent.name);
128
- for (const name of fs.readdirSync(subPath)) {
129
- if (name.startsWith("sd-")) {
130
- fs.rmSync(path.join(subPath, name), { recursive: true });
131
- }
132
- }
133
- }
134
- }
135
-
136
- /** Copies sd-* entries. */
137
- function copySdEntries(sourceDir: string, targetDir: string, entries: string[]): void {
138
- fs.mkdirSync(targetDir, { recursive: true });
139
- for (const entry of entries) {
140
- const src = path.join(sourceDir, entry);
141
- const dest = path.join(targetDir, entry);
142
- fs.mkdirSync(path.dirname(dest), { recursive: true });
143
- fs.cpSync(src, dest, { recursive: true });
144
- }
145
- }
146
-
147
- /** Ensures statusLine and SessionStart hooks are configured in settings.json. */
148
- function setupSettings(targetDir: string): void {
149
- const settingsPath = path.join(targetDir, "settings.json");
150
-
151
- let settings: Record<string, unknown> = {};
152
- if (fs.existsSync(settingsPath)) {
153
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")) as Record<string, unknown>;
154
- }
155
-
156
- // statusLine: always overwrite
157
- settings["statusLine"] = { type: "command", command: "python .claude/sd-statusline.py" };
158
-
159
- // SessionStart: ensure sd-session-start hook exists with correct config
160
- const sdSessionEntry = {
161
- matcher: "startup|resume|clear|compact",
162
- hooks: [{ type: "command", command: "bash .claude/sd-session-start.sh" }],
163
- };
164
-
165
- const sessionStart = settings["SessionStart"] as
166
- | Array<{ matcher?: string; hooks?: Array<{ type: string; command: string }> }>
167
- | undefined;
168
-
169
- if (sessionStart == null) {
170
- settings["SessionStart"] = [sdSessionEntry];
171
- } else {
172
- const idx = sessionStart.findIndex((entry) =>
173
- entry.hooks?.some((hook) => hook.command.includes("sd-session-start")),
174
- );
175
- if (idx >= 0) {
176
- sessionStart[idx] = sdSessionEntry;
177
- } else {
178
- sessionStart.push(sdSessionEntry);
179
- }
180
- }
181
-
182
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
183
- }
package/src/index.ts DELETED
@@ -1,7 +0,0 @@
1
- // Commands
2
- export * from "./commands/install.js";
3
- export * from "./commands/auth-utils.js";
4
- export * from "./commands/auth-add.js";
5
- export * from "./commands/auth-use.js";
6
- export * from "./commands/auth-list.js";
7
- export * from "./commands/auth-remove.js";
package/src/sd-claude.ts DELETED
@@ -1,98 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import yargs from "yargs";
4
- import { hideBin } from "yargs/helpers";
5
- import { runInstall } from "./commands/install.js";
6
- import { runAuthAdd } from "./commands/auth-add.js";
7
- import { runAuthUse } from "./commands/auth-use.js";
8
- import { runAuthList } from "./commands/auth-list.js";
9
- import { runAuthRemove } from "./commands/auth-remove.js";
10
-
11
- await yargs(hideBin(process.argv))
12
- .help("help", "Help")
13
- .alias("help", "h")
14
- .command(
15
- "install",
16
- "Installs Claude Code assets to the project.",
17
- (cmd) => cmd.version(false).hide("help"),
18
- () => {
19
- runInstall();
20
- },
21
- )
22
- .command("auth", "Manages Claude account profiles.", (cmd) =>
23
- cmd
24
- .version(false)
25
- .hide("help")
26
- .command(
27
- "add <name>",
28
- "Saves the currently logged-in account",
29
- (sub) =>
30
- sub.positional("name", {
31
- type: "string",
32
- demandOption: true,
33
- }),
34
- (argv) => {
35
- try {
36
- runAuthAdd(argv.name);
37
- } catch (err) {
38
- // eslint-disable-next-line no-console
39
- console.error((err as Error).message);
40
- process.exit(1);
41
- }
42
- },
43
- )
44
- .command(
45
- "use <name>",
46
- "Switches to a saved account",
47
- (sub) =>
48
- sub.positional("name", {
49
- type: "string",
50
- demandOption: true,
51
- }),
52
- (argv) => {
53
- try {
54
- runAuthUse(argv.name);
55
- } catch (err) {
56
- // eslint-disable-next-line no-console
57
- console.error((err as Error).message);
58
- process.exit(1);
59
- }
60
- },
61
- )
62
- .command(
63
- "list",
64
- "Displays the list of saved accounts",
65
- (sub) => sub,
66
- async () => {
67
- try {
68
- await runAuthList();
69
- } catch (err) {
70
- // eslint-disable-next-line no-console
71
- console.error((err as Error).message);
72
- process.exit(1);
73
- }
74
- },
75
- )
76
- .command(
77
- "remove <name>",
78
- "Removes a saved account",
79
- (sub) =>
80
- sub.positional("name", {
81
- type: "string",
82
- demandOption: true,
83
- }),
84
- (argv) => {
85
- try {
86
- runAuthRemove(argv.name);
87
- } catch (err) {
88
- // eslint-disable-next-line no-console
89
- console.error((err as Error).message);
90
- process.exit(1);
91
- }
92
- },
93
- )
94
- .demandCommand(1, "Please specify an auth subcommand."),
95
- )
96
- .demandCommand(1, "Please specify a command.")
97
- .strict()
98
- .parse();
@@ -1,74 +0,0 @@
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 { runAuthAdd } from "../src/commands/auth-add";
6
-
7
- describe("runAuthAdd", () => {
8
- let tmpDir: string;
9
-
10
- beforeEach(() => {
11
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-claude-auth-add-test-"));
12
-
13
- // Set up .claude.json with oauthAccount and userID
14
- const claudeJson = {
15
- oauthAccount: { emailAddress: "test@example.com", token: "oauth-token" },
16
- userID: "user-123",
17
- };
18
- fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify(claudeJson));
19
-
20
- // Set up .claude/.credentials.json
21
- const claudeDir = path.join(tmpDir, ".claude");
22
- fs.mkdirSync(claudeDir, { recursive: true });
23
- const credentials = { accessToken: "access-abc", refreshToken: "refresh-xyz" };
24
- fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(credentials));
25
- });
26
-
27
- afterEach(() => {
28
- fs.rmSync(tmpDir, { recursive: true });
29
- });
30
-
31
- test("saves auth.json and credentials.json correctly", () => {
32
- runAuthAdd("work", tmpDir);
33
-
34
- const profileDir = path.join(tmpDir, ".sd-claude", "auth", "work");
35
-
36
- // Verify auth.json
37
- const authJson = JSON.parse(
38
- fs.readFileSync(path.join(profileDir, "auth.json"), "utf-8"),
39
- ) as Record<string, unknown>;
40
- expect(authJson).toEqual({
41
- oauthAccount: { emailAddress: "test@example.com", token: "oauth-token" },
42
- userID: "user-123",
43
- });
44
-
45
- // Verify credentials.json
46
- const credJson = JSON.parse(
47
- fs.readFileSync(path.join(profileDir, "credentials.json"), "utf-8"),
48
- ) as Record<string, unknown>;
49
- expect(credJson).toEqual({
50
- accessToken: "access-abc",
51
- refreshToken: "refresh-xyz",
52
- });
53
- });
54
-
55
- test("throws when profile already exists", () => {
56
- const profileDir = path.join(tmpDir, ".sd-claude", "auth", "work");
57
- fs.mkdirSync(profileDir, { recursive: true });
58
-
59
- expect(() => runAuthAdd("work", tmpDir)).toThrow(
60
- "Profile 'work' already exists. Remove it first with: sd-claude auth remove work",
61
- );
62
- });
63
-
64
- test("throws with invalid name", () => {
65
- expect(() => runAuthAdd("BAD NAME!", tmpDir)).toThrow("Invalid name");
66
- });
67
-
68
- test("throws when not logged in", () => {
69
- // Overwrite .claude.json without oauthAccount/userID
70
- fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ someField: "value" }));
71
-
72
- expect(() => runAuthAdd("work", tmpDir)).toThrow("Not logged in");
73
- });
74
- });
@@ -1,198 +0,0 @@
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 { runAuthList } from "../src/commands/auth-list";
6
-
7
- // Mock global fetch to prevent real API calls
8
- vi.stubGlobal(
9
- "fetch",
10
- vi.fn().mockResolvedValue({
11
- ok: false,
12
- json: () => Promise.resolve({}),
13
- }),
14
- );
15
-
16
- describe("runAuthList", () => {
17
- let tmpDir: string;
18
-
19
- beforeEach(() => {
20
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-claude-auth-list-test-"));
21
- });
22
-
23
- afterEach(() => {
24
- fs.rmSync(tmpDir, { recursive: true });
25
- vi.restoreAllMocks();
26
- });
27
-
28
- test("outputs 'No saved profiles.' when no profiles exist", async () => {
29
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
30
-
31
- await runAuthList(tmpDir);
32
-
33
- expect(spy).toHaveBeenCalledWith("No saved profiles.");
34
- });
35
-
36
- test("outputs profiles sorted alphabetically with active marker", async () => {
37
- const authDir = path.join(tmpDir, ".sd-claude", "auth");
38
-
39
- // Create profile "beta"
40
- const betaDir = path.join(authDir, "beta");
41
- fs.mkdirSync(betaDir, { recursive: true });
42
- fs.writeFileSync(
43
- path.join(betaDir, "auth.json"),
44
- JSON.stringify({
45
- oauthAccount: { emailAddress: "beta@example.com", organizationName: "BetaCorp" },
46
- userID: "user-beta",
47
- }),
48
- );
49
- fs.writeFileSync(
50
- path.join(betaDir, "credentials.json"),
51
- JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-06-20").getTime() } }),
52
- );
53
-
54
- // Create profile "alpha"
55
- const alphaDir = path.join(authDir, "alpha");
56
- fs.mkdirSync(alphaDir, { recursive: true });
57
- fs.writeFileSync(
58
- path.join(alphaDir, "auth.json"),
59
- JSON.stringify({
60
- oauthAccount: { emailAddress: "alpha@example.com", organizationName: "AlphaCorp" },
61
- userID: "user-alpha",
62
- }),
63
- );
64
- fs.writeFileSync(
65
- path.join(alphaDir, "credentials.json"),
66
- JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-06-25").getTime() } }),
67
- );
68
-
69
- // Set current userID to "user-alpha"
70
- fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-alpha" }));
71
-
72
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
73
-
74
- await runAuthList(tmpDir);
75
-
76
- expect(spy).toHaveBeenCalledTimes(2);
77
- // alpha comes first (alphabetical), and is active; usage shows ? when fetch fails
78
- expect(spy).toHaveBeenNthCalledWith(
79
- 1,
80
- "* alpha (alpha@example.com) expires: 2025-06-25 │ 5h: ? │ 7d: ?",
81
- );
82
- // beta is not active
83
- expect(spy).toHaveBeenNthCalledWith(
84
- 2,
85
- " beta (beta@example.com) expires: 2025-06-20 │ 5h: ? │ 7d: ?",
86
- );
87
- });
88
-
89
- test("shows email even when organizationName is missing", async () => {
90
- const authDir = path.join(tmpDir, ".sd-claude", "auth");
91
-
92
- const profileDir = path.join(authDir, "personal");
93
- fs.mkdirSync(profileDir, { recursive: true });
94
- fs.writeFileSync(
95
- path.join(profileDir, "auth.json"),
96
- JSON.stringify({
97
- oauthAccount: { emailAddress: "user@gmail.com" },
98
- userID: "user-personal",
99
- }),
100
- );
101
- fs.writeFileSync(
102
- path.join(profileDir, "credentials.json"),
103
- JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-07-01").getTime() } }),
104
- );
105
-
106
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
107
-
108
- await runAuthList(tmpDir);
109
-
110
- expect(spy).toHaveBeenCalledWith(
111
- " personal (user@gmail.com) expires: 2025-07-01 │ 5h: ? │ 7d: ?",
112
- );
113
- });
114
-
115
- test("shows 'unknown' when expiresAt is missing", async () => {
116
- const authDir = path.join(tmpDir, ".sd-claude", "auth");
117
-
118
- const profileDir = path.join(authDir, "noexpiry");
119
- fs.mkdirSync(profileDir, { recursive: true });
120
- fs.writeFileSync(
121
- path.join(profileDir, "auth.json"),
122
- JSON.stringify({
123
- oauthAccount: { emailAddress: "noexp@example.com", organizationName: "SomeCorp" },
124
- userID: "user-noexp",
125
- }),
126
- );
127
- fs.writeFileSync(path.join(profileDir, "credentials.json"), JSON.stringify({}));
128
-
129
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
130
-
131
- await runAuthList(tmpDir);
132
-
133
- expect(spy).toHaveBeenCalledWith(
134
- " noexpiry (noexp@example.com) expires: unknown │ 5h: ? │ 7d: ?",
135
- );
136
- });
137
-
138
- test("marks active profile with * when userID matches", async () => {
139
- const authDir = path.join(tmpDir, ".sd-claude", "auth");
140
-
141
- const profileDir = path.join(authDir, "work");
142
- fs.mkdirSync(profileDir, { recursive: true });
143
- fs.writeFileSync(
144
- path.join(profileDir, "auth.json"),
145
- JSON.stringify({
146
- oauthAccount: { emailAddress: "work@company.com", organizationName: "WorkCorp" },
147
- userID: "user-work",
148
- }),
149
- );
150
- fs.writeFileSync(
151
- path.join(profileDir, "credentials.json"),
152
- JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-12-31").getTime() } }),
153
- );
154
-
155
- // Set current userID to match
156
- fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-work" }));
157
-
158
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
159
-
160
- await runAuthList(tmpDir);
161
-
162
- expect(spy).toHaveBeenCalledWith(
163
- "* work (work@company.com) expires: 2025-12-31 │ 5h: ? │ 7d: ?",
164
- );
165
- });
166
-
167
- test("non-active profile has space prefix instead of *", async () => {
168
- const authDir = path.join(tmpDir, ".sd-claude", "auth");
169
-
170
- const profileDir = path.join(authDir, "other");
171
- fs.mkdirSync(profileDir, { recursive: true });
172
- fs.writeFileSync(
173
- path.join(profileDir, "auth.json"),
174
- JSON.stringify({
175
- oauthAccount: { emailAddress: "other@example.com", organizationName: "OtherCorp" },
176
- userID: "user-other",
177
- }),
178
- );
179
- fs.writeFileSync(
180
- path.join(profileDir, "credentials.json"),
181
- JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-08-15").getTime() } }),
182
- );
183
-
184
- // Set current userID to something different
185
- fs.writeFileSync(
186
- path.join(tmpDir, ".claude.json"),
187
- JSON.stringify({ userID: "user-different" }),
188
- );
189
-
190
- const spy = vi.spyOn(console, "log").mockImplementation(() => {});
191
-
192
- await runAuthList(tmpDir);
193
-
194
- expect(spy).toHaveBeenCalledWith(
195
- " other (other@example.com) expires: 2025-08-15 │ 5h: ? │ 7d: ?",
196
- );
197
- });
198
- });
@@ -1,74 +0,0 @@
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
- });