@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,26 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { validateName, profileExists, getProfileDir, getCurrentUserID } from "./auth-utils.js";
|
|
4
|
+
|
|
5
|
+
export function runAuthRemove(name: string, homeDir?: string): void {
|
|
6
|
+
validateName(name);
|
|
7
|
+
|
|
8
|
+
if (!profileExists(name, homeDir)) {
|
|
9
|
+
throw new Error(`Profile '${name}' not found.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const profileDir = getProfileDir(name, homeDir);
|
|
13
|
+
const authJsonPath = path.join(profileDir, "auth.json");
|
|
14
|
+
const authData = JSON.parse(fs.readFileSync(authJsonPath, "utf-8")) as { userID: string };
|
|
15
|
+
|
|
16
|
+
const currentUserID = getCurrentUserID(homeDir);
|
|
17
|
+
if (currentUserID != null && currentUserID === authData.userID) {
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.warn(`Warning: '${name}' is currently active.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fs.rmSync(profileDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.log(`Removed profile '${name}'`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { validateName, profileExists, getProfileDir } from "./auth-utils.js";
|
|
5
|
+
|
|
6
|
+
export function runAuthUse(name: string, homeDir?: string): void {
|
|
7
|
+
validateName(name);
|
|
8
|
+
|
|
9
|
+
if (!profileExists(name, homeDir)) {
|
|
10
|
+
throw new Error(`Profile '${name}' not found.`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const profileDir = getProfileDir(name, homeDir);
|
|
14
|
+
const home = homeDir ?? os.homedir();
|
|
15
|
+
|
|
16
|
+
// Read saved auth.json and credentials.json from profile directory
|
|
17
|
+
const authJson = JSON.parse(fs.readFileSync(path.join(profileDir, "auth.json"), "utf-8")) as {
|
|
18
|
+
oauthAccount: Record<string, unknown>;
|
|
19
|
+
userID: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const savedCredentials = JSON.parse(
|
|
23
|
+
fs.readFileSync(path.join(profileDir, "credentials.json"), "utf-8"),
|
|
24
|
+
) as Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
// Check token expiry
|
|
27
|
+
const claudeAiOauth = savedCredentials["claudeAiOauth"] as { expiresAt?: number } | undefined;
|
|
28
|
+
if (claudeAiOauth?.expiresAt != null && claudeAiOauth.expiresAt < Date.now()) {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.warn("Warning: Token expired. Run /login after switching.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Read ~/.claude.json, replace ONLY oauthAccount and userID, write back
|
|
34
|
+
const claudeJsonPath = path.join(home, ".claude.json");
|
|
35
|
+
const claudeData = JSON.parse(fs.readFileSync(claudeJsonPath, "utf-8")) as Record<
|
|
36
|
+
string,
|
|
37
|
+
unknown
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
claudeData["oauthAccount"] = authJson.oauthAccount;
|
|
41
|
+
claudeData["userID"] = authJson.userID;
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeData, null, 2));
|
|
44
|
+
|
|
45
|
+
// Replace ~/.claude/.credentials.json entirely with saved credentials
|
|
46
|
+
const credentialsPath = path.join(home, ".claude", ".credentials.json");
|
|
47
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(savedCredentials, null, 2));
|
|
48
|
+
|
|
49
|
+
// Log the switch
|
|
50
|
+
const email = (authJson.oauthAccount["emailAddress"] as string | undefined) ?? "unknown";
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log(`Switched to ${name} (${email})`);
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
|
|
5
|
+
const NAME_PATTERN = /^[a-z0-9_-]+$/;
|
|
6
|
+
|
|
7
|
+
export function validateName(name: string): void {
|
|
8
|
+
if (!NAME_PATTERN.test(name)) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`Invalid name '${name}'. Use only lowercase letters, numbers, hyphens, underscores.`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getProfileDir(name: string, homeDir?: string): string {
|
|
16
|
+
return path.join(homeDir ?? os.homedir(), ".sd-claude", "auth", name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function profileExists(name: string, homeDir?: string): boolean {
|
|
20
|
+
return fs.existsSync(getProfileDir(name, homeDir));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function listProfiles(homeDir?: string): string[] {
|
|
24
|
+
const authDir = path.join(homeDir ?? os.homedir(), ".sd-claude", "auth");
|
|
25
|
+
if (!fs.existsSync(authDir)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return fs
|
|
30
|
+
.readdirSync(authDir, { withFileTypes: true })
|
|
31
|
+
.filter((entry) => entry.isDirectory())
|
|
32
|
+
.map((entry) => entry.name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readCurrentAuth(homeDir?: string): {
|
|
36
|
+
oauthAccount: Record<string, unknown>;
|
|
37
|
+
userID: string;
|
|
38
|
+
} {
|
|
39
|
+
const claudeJsonPath = path.join(homeDir ?? os.homedir(), ".claude.json");
|
|
40
|
+
const data = JSON.parse(fs.readFileSync(claudeJsonPath, "utf-8")) as Record<string, unknown>;
|
|
41
|
+
|
|
42
|
+
const oauthAccount = data["oauthAccount"] as Record<string, unknown> | undefined;
|
|
43
|
+
const userID = data["userID"] as string | undefined;
|
|
44
|
+
|
|
45
|
+
if (oauthAccount == null || userID == null) {
|
|
46
|
+
throw new Error("Not logged in. Run /login first.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { oauthAccount, userID };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readCurrentCredentials(homeDir?: string): Record<string, unknown> {
|
|
53
|
+
const credentialsPath = path.join(homeDir ?? os.homedir(), ".claude", ".credentials.json");
|
|
54
|
+
return JSON.parse(fs.readFileSync(credentialsPath, "utf-8")) as Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getCurrentUserID(homeDir?: string): string | undefined {
|
|
58
|
+
const claudeJsonPath = path.join(homeDir ?? os.homedir(), ".claude.json");
|
|
59
|
+
try {
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(claudeJsonPath, "utf-8")) as Record<string, unknown>;
|
|
61
|
+
return data["userID"] as string | undefined;
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/commands/install.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude Code
|
|
3
|
-
* postinstall
|
|
2
|
+
* Installs Claude Code assets to the project's .claude/ directory.
|
|
3
|
+
* Executed via postinstall script or `sd-claude install`.
|
|
4
4
|
*/
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
@@ -9,23 +9,23 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
export function runInstall(): void {
|
|
10
10
|
try {
|
|
11
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
// dist/commands/ →
|
|
12
|
+
// dist/commands/ → package root
|
|
13
13
|
const pkgRoot = path.resolve(__dirname, "../..");
|
|
14
14
|
const sourceDir = path.join(pkgRoot, "claude");
|
|
15
15
|
|
|
16
16
|
const projectRoot = findProjectRoot(__dirname);
|
|
17
17
|
if (projectRoot == null) {
|
|
18
18
|
// eslint-disable-next-line no-console
|
|
19
|
-
console.log("[@simplysm/sd-claude]
|
|
19
|
+
console.log("[@simplysm/sd-claude] Could not find project root, skipping installation.");
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// simplysm
|
|
23
|
+
// Skip execution if this is the simplysm monorepo with the same major version
|
|
24
24
|
if (isSimplysmMonorepoSameMajor(projectRoot, pkgRoot)) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// Skip if the source directory doesn't exist (claude/ may not exist in monorepo dev environment)
|
|
29
29
|
if (!fs.existsSync(sourceDir)) {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
@@ -42,15 +42,15 @@ export function runInstall(): void {
|
|
|
42
42
|
setupStatusLine(targetDir);
|
|
43
43
|
|
|
44
44
|
// eslint-disable-next-line no-console
|
|
45
|
-
console.log(`[@simplysm/sd-claude] ${sourceEntries.length}
|
|
45
|
+
console.log(`[@simplysm/sd-claude] Installed ${sourceEntries.length} sd-* entries.`);
|
|
46
46
|
} catch (err) {
|
|
47
|
-
// postinstall
|
|
47
|
+
// Ignore errors to prevent postinstall failure from blocking the entire pnpm install
|
|
48
48
|
// eslint-disable-next-line no-console
|
|
49
|
-
console.warn("[@simplysm/sd-claude] postinstall
|
|
49
|
+
console.warn("[@simplysm/sd-claude] postinstall warning:", (err as Error).message);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
53
|
+
/** Finds the project root from INIT_CWD or node_modules path. */
|
|
54
54
|
function findProjectRoot(dirname: string): string | undefined {
|
|
55
55
|
if (process.env["INIT_CWD"] != null) {
|
|
56
56
|
return process.env["INIT_CWD"];
|
|
@@ -62,7 +62,7 @@ function findProjectRoot(dirname: string): string | undefined {
|
|
|
62
62
|
return idx !== -1 ? dirname.substring(0, idx) : undefined;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** simplysm
|
|
65
|
+
/** Checks if this is the simplysm monorepo with the same major version. */
|
|
66
66
|
function isSimplysmMonorepoSameMajor(projectRoot: string, pkgRoot: string): boolean {
|
|
67
67
|
const projectPkgPath = path.join(projectRoot, "package.json");
|
|
68
68
|
if (!fs.existsSync(projectPkgPath)) return false;
|
|
@@ -85,18 +85,18 @@ function isSimplysmMonorepoSameMajor(projectRoot: string, pkgRoot: string): bool
|
|
|
85
85
|
return projectMajor != null && projectMajor === sdClaudeMajor;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/** sd-*
|
|
88
|
+
/** Recursively collects sd-* entries. */
|
|
89
89
|
function collectSdEntries(sourceDir: string): string[] {
|
|
90
90
|
const entries: string[] = [];
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// Root level: sd-*
|
|
93
93
|
for (const name of fs.readdirSync(sourceDir)) {
|
|
94
94
|
if (name.startsWith("sd-")) {
|
|
95
95
|
entries.push(name);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
//
|
|
99
|
+
// Subdirectories: */sd-*
|
|
100
100
|
for (const dirent of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
101
101
|
if (!dirent.isDirectory() || dirent.name.startsWith("sd-")) continue;
|
|
102
102
|
const subPath = path.join(sourceDir, dirent.name);
|
|
@@ -110,18 +110,18 @@ function collectSdEntries(sourceDir: string): string[] {
|
|
|
110
110
|
return entries;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
/**
|
|
113
|
+
/** Removes existing sd-* entries. */
|
|
114
114
|
function cleanSdEntries(targetDir: string): void {
|
|
115
115
|
if (!fs.existsSync(targetDir)) return;
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Root level sd-*
|
|
118
118
|
for (const name of fs.readdirSync(targetDir)) {
|
|
119
119
|
if (name.startsWith("sd-")) {
|
|
120
120
|
fs.rmSync(path.join(targetDir, name), { recursive: true });
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
//
|
|
124
|
+
// Subdirectories */sd-*
|
|
125
125
|
for (const dirent of fs.readdirSync(targetDir, { withFileTypes: true })) {
|
|
126
126
|
if (!dirent.isDirectory() || dirent.name.startsWith("sd-")) continue;
|
|
127
127
|
const subPath = path.join(targetDir, dirent.name);
|
|
@@ -133,7 +133,7 @@ function cleanSdEntries(targetDir: string): void {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** sd-*
|
|
136
|
+
/** Copies sd-* entries. */
|
|
137
137
|
function copySdEntries(sourceDir: string, targetDir: string, entries: string[]): void {
|
|
138
138
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
139
139
|
for (const entry of entries) {
|
|
@@ -144,7 +144,7 @@ function copySdEntries(sourceDir: string, targetDir: string, entries: string[]):
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/**
|
|
147
|
+
/** Adds statusLine configuration to settings.json. */
|
|
148
148
|
function setupStatusLine(targetDir: string): void {
|
|
149
149
|
const settingsPath = path.join(targetDir, "settings.json");
|
|
150
150
|
const sdStatusLineCommand = "node .claude/sd-statusline.js";
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
// Commands
|
|
2
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
CHANGED
|
@@ -3,18 +3,96 @@
|
|
|
3
3
|
import yargs from "yargs";
|
|
4
4
|
import { hideBin } from "yargs/helpers";
|
|
5
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";
|
|
6
10
|
|
|
7
11
|
await yargs(hideBin(process.argv))
|
|
8
|
-
.help("help", "
|
|
12
|
+
.help("help", "Help")
|
|
9
13
|
.alias("help", "h")
|
|
10
14
|
.command(
|
|
11
15
|
"install",
|
|
12
|
-
"Claude Code
|
|
16
|
+
"Installs Claude Code assets to the project.",
|
|
13
17
|
(cmd) => cmd.version(false).hide("help"),
|
|
14
18
|
() => {
|
|
15
19
|
runInstall();
|
|
16
20
|
},
|
|
17
21
|
)
|
|
18
|
-
.
|
|
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
|
+
() => {
|
|
67
|
+
try {
|
|
68
|
+
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.")
|
|
19
97
|
.strict()
|
|
20
98
|
.parse();
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
describe("runAuthList", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-claude-auth-list-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("outputs 'No saved profiles.' when no profiles exist", () => {
|
|
20
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
21
|
+
|
|
22
|
+
runAuthList(tmpDir);
|
|
23
|
+
|
|
24
|
+
expect(spy).toHaveBeenCalledWith("No saved profiles.");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("outputs profiles sorted alphabetically with active marker", () => {
|
|
28
|
+
const authDir = path.join(tmpDir, ".sd-claude", "auth");
|
|
29
|
+
|
|
30
|
+
// Create profile "beta"
|
|
31
|
+
const betaDir = path.join(authDir, "beta");
|
|
32
|
+
fs.mkdirSync(betaDir, { recursive: true });
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
path.join(betaDir, "auth.json"),
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
oauthAccount: { emailAddress: "beta@example.com", organizationName: "BetaCorp" },
|
|
37
|
+
userID: "user-beta",
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(betaDir, "credentials.json"),
|
|
42
|
+
JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-06-20").getTime() } }),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Create profile "alpha"
|
|
46
|
+
const alphaDir = path.join(authDir, "alpha");
|
|
47
|
+
fs.mkdirSync(alphaDir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
path.join(alphaDir, "auth.json"),
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
oauthAccount: { emailAddress: "alpha@example.com", organizationName: "AlphaCorp" },
|
|
52
|
+
userID: "user-alpha",
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(alphaDir, "credentials.json"),
|
|
57
|
+
JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-06-25").getTime() } }),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Set current userID to "user-alpha"
|
|
61
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-alpha" }));
|
|
62
|
+
|
|
63
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
64
|
+
|
|
65
|
+
runAuthList(tmpDir);
|
|
66
|
+
|
|
67
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
68
|
+
// alpha comes first (alphabetical), and is active
|
|
69
|
+
expect(spy).toHaveBeenNthCalledWith(1, "* alpha (alpha@example.com) expires: 2025-06-25");
|
|
70
|
+
// beta is not active
|
|
71
|
+
expect(spy).toHaveBeenNthCalledWith(2, " beta (beta@example.com) expires: 2025-06-20");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("shows email even when organizationName is missing", () => {
|
|
75
|
+
const authDir = path.join(tmpDir, ".sd-claude", "auth");
|
|
76
|
+
|
|
77
|
+
const profileDir = path.join(authDir, "personal");
|
|
78
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(profileDir, "auth.json"),
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
oauthAccount: { emailAddress: "user@gmail.com" },
|
|
83
|
+
userID: "user-personal",
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(profileDir, "credentials.json"),
|
|
88
|
+
JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-07-01").getTime() } }),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
92
|
+
|
|
93
|
+
runAuthList(tmpDir);
|
|
94
|
+
|
|
95
|
+
expect(spy).toHaveBeenCalledWith(" personal (user@gmail.com) expires: 2025-07-01");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("shows 'unknown' when expiresAt is missing", () => {
|
|
99
|
+
const authDir = path.join(tmpDir, ".sd-claude", "auth");
|
|
100
|
+
|
|
101
|
+
const profileDir = path.join(authDir, "noexpiry");
|
|
102
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
path.join(profileDir, "auth.json"),
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
oauthAccount: { emailAddress: "noexp@example.com", organizationName: "SomeCorp" },
|
|
107
|
+
userID: "user-noexp",
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
fs.writeFileSync(path.join(profileDir, "credentials.json"), JSON.stringify({}));
|
|
111
|
+
|
|
112
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
113
|
+
|
|
114
|
+
runAuthList(tmpDir);
|
|
115
|
+
|
|
116
|
+
expect(spy).toHaveBeenCalledWith(" noexpiry (noexp@example.com) expires: unknown");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("marks active profile with * when userID matches", () => {
|
|
120
|
+
const authDir = path.join(tmpDir, ".sd-claude", "auth");
|
|
121
|
+
|
|
122
|
+
const profileDir = path.join(authDir, "work");
|
|
123
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(profileDir, "auth.json"),
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
oauthAccount: { emailAddress: "work@company.com", organizationName: "WorkCorp" },
|
|
128
|
+
userID: "user-work",
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
fs.writeFileSync(
|
|
132
|
+
path.join(profileDir, "credentials.json"),
|
|
133
|
+
JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-12-31").getTime() } }),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Set current userID to match
|
|
137
|
+
fs.writeFileSync(path.join(tmpDir, ".claude.json"), JSON.stringify({ userID: "user-work" }));
|
|
138
|
+
|
|
139
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
140
|
+
|
|
141
|
+
runAuthList(tmpDir);
|
|
142
|
+
|
|
143
|
+
expect(spy).toHaveBeenCalledWith("* work (work@company.com) expires: 2025-12-31");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("non-active profile has space prefix instead of *", () => {
|
|
147
|
+
const authDir = path.join(tmpDir, ".sd-claude", "auth");
|
|
148
|
+
|
|
149
|
+
const profileDir = path.join(authDir, "other");
|
|
150
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
path.join(profileDir, "auth.json"),
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
oauthAccount: { emailAddress: "other@example.com", organizationName: "OtherCorp" },
|
|
155
|
+
userID: "user-other",
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
fs.writeFileSync(
|
|
159
|
+
path.join(profileDir, "credentials.json"),
|
|
160
|
+
JSON.stringify({ claudeAiOauth: { expiresAt: new Date("2025-08-15").getTime() } }),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Set current userID to something different
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
path.join(tmpDir, ".claude.json"),
|
|
166
|
+
JSON.stringify({ userID: "user-different" }),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
170
|
+
|
|
171
|
+
runAuthList(tmpDir);
|
|
172
|
+
|
|
173
|
+
expect(spy).toHaveBeenCalledWith(" other (other@example.com) expires: 2025-08-15");
|
|
174
|
+
});
|
|
175
|
+
});
|