@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.
- package/README.md +43 -0
- package/claude/skills/sd-plan/SKILL.md +4 -3
- package/package.json +3 -19
- package/scripts/postinstall.mjs +126 -9
- package/scripts/sd-entries.mjs +33 -0
- package/scripts/sync-claude-assets.mjs +4 -21
- package/dist/commands/auth-add.d.ts +0 -2
- package/dist/commands/auth-add.d.ts.map +0 -1
- package/dist/commands/auth-add.js +0 -32
- package/dist/commands/auth-add.js.map +0 -6
- package/dist/commands/auth-list.d.ts +0 -2
- package/dist/commands/auth-list.d.ts.map +0 -1
- package/dist/commands/auth-list.js +0 -95
- package/dist/commands/auth-list.js.map +0 -6
- package/dist/commands/auth-remove.d.ts +0 -2
- package/dist/commands/auth-remove.d.ts.map +0 -1
- package/dist/commands/auth-remove.js +0 -22
- package/dist/commands/auth-remove.js.map +0 -6
- package/dist/commands/auth-use.d.ts +0 -2
- package/dist/commands/auth-use.d.ts.map +0 -1
- package/dist/commands/auth-use.js +0 -33
- package/dist/commands/auth-use.js.map +0 -6
- package/dist/commands/auth-utils.d.ts +0 -11
- package/dist/commands/auth-utils.d.ts.map +0 -1
- package/dist/commands/auth-utils.js +0 -57
- package/dist/commands/auth-utils.js.map +0 -6
- package/dist/commands/install.d.ts +0 -2
- package/dist/commands/install.d.ts.map +0 -1
- package/dist/commands/install.js +0 -127
- package/dist/commands/install.js.map +0 -6
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -7
- package/dist/index.js.map +0 -6
- package/dist/sd-claude.d.ts +0 -3
- package/dist/sd-claude.d.ts.map +0 -1
- package/dist/sd-claude.js +0 -78
- package/dist/sd-claude.js.map +0 -6
- package/src/commands/auth-add.ts +0 -36
- package/src/commands/auth-list.ts +0 -130
- package/src/commands/auth-remove.ts +0 -26
- package/src/commands/auth-use.ts +0 -53
- package/src/commands/auth-utils.ts +0 -65
- package/src/commands/install.ts +0 -183
- package/src/index.ts +0 -7
- package/src/sd-claude.ts +0 -98
- package/tests/auth-add.spec.ts +0 -74
- package/tests/auth-list.spec.ts +0 -198
- package/tests/auth-remove.spec.ts +0 -74
- package/tests/auth-use.spec.ts +0 -153
- package/tests/auth-utils.spec.ts +0 -173
package/src/commands/install.ts
DELETED
|
@@ -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();
|
package/tests/auth-add.spec.ts
DELETED
|
@@ -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
|
-
});
|
package/tests/auth-list.spec.ts
DELETED
|
@@ -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
|
-
});
|