cc-swap 0.1.0

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/dist/cli.js ADDED
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import { spawnSync } from "child_process";
6
+ import path3 from "path";
7
+ import { createInterface } from "readline/promises";
8
+ import { stdin as input, stdout as output } from "process";
9
+
10
+ // src/paths.ts
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ var HOME = process.env.CC_SWAP_HOME ?? homedir();
14
+ var CLAUDE_DIR = process.env.CC_SWAP_CLAUDE_DIR ?? join(HOME, ".claude");
15
+ var CC_SWAP_DIR = join(HOME, ".cc-swap");
16
+ var ACCOUNTS_DIR = join(CC_SWAP_DIR, "claude-accounts");
17
+ var CURRENT_FILE = join(CC_SWAP_DIR, ".current");
18
+ var CONFIG_FILE = join(CC_SWAP_DIR, "config.json");
19
+
20
+ // src/accounts.ts
21
+ import fsPromises2 from "fs/promises";
22
+ import path2 from "path";
23
+
24
+ // src/validate.ts
25
+ function validateName(name) {
26
+ if (!name) return "Name cannot be empty";
27
+ if (name.length > 50) return "Name must be 50 characters or fewer";
28
+ if (name.startsWith(".") || name.startsWith("-"))
29
+ return "Name cannot start with '.' or '-'";
30
+ if (!/^[a-zA-Z0-9_-]+$/.test(name))
31
+ return "Name can only contain letters, numbers, hyphens, and underscores";
32
+ return null;
33
+ }
34
+
35
+ // src/symlink.ts
36
+ import fsPromises from "fs/promises";
37
+ import path from "path";
38
+ async function populateAccount(claudeDir, accountDir, syncItems) {
39
+ await fsPromises.mkdir(accountDir, { recursive: true, mode: 448 });
40
+ for (const item of syncItems) {
41
+ const source = path.join(claudeDir, item);
42
+ const link = path.join(accountDir, item);
43
+ const sourceExists = await fsPromises.access(source).then(() => true).catch(() => false);
44
+ if (!sourceExists) continue;
45
+ const linkExists = await fsPromises.access(link).then(() => true).catch(() => false);
46
+ if (linkExists) continue;
47
+ await fsPromises.symlink(source, link);
48
+ }
49
+ }
50
+ async function readCurrent(filePath) {
51
+ try {
52
+ const content = (await fsPromises.readFile(filePath, "utf-8")).trim();
53
+ return content || null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ async function writeCurrent(filePath, name) {
59
+ await fsPromises.writeFile(filePath, name + "\n", { mode: 384 });
60
+ }
61
+
62
+ // src/accounts.ts
63
+ async function listAccounts(accountsDir) {
64
+ try {
65
+ const entries = await fsPromises2.readdir(accountsDir, { withFileTypes: true });
66
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
67
+ } catch {
68
+ return [];
69
+ }
70
+ }
71
+ function nextAccount(current, accounts) {
72
+ if (accounts.length <= 1) return null;
73
+ const idx = accounts.indexOf(current);
74
+ return accounts[(idx + 1) % accounts.length];
75
+ }
76
+ async function addAccount(name, paths) {
77
+ const { claudeDir, ccSwitchDir, accountsDir, currentFile } = paths;
78
+ const nameErr = validateName(name);
79
+ if (nameErr) throw new Error(nameErr);
80
+ const accountsExist = await fsPromises2.access(accountsDir).then(() => true).catch(() => false);
81
+ if (!accountsExist) {
82
+ await fsPromises2.mkdir(accountsDir, { recursive: true, mode: 448 });
83
+ if (ccSwitchDir) {
84
+ await fsPromises2.chmod(ccSwitchDir, 448);
85
+ }
86
+ }
87
+ const target = path2.join(accountsDir, name);
88
+ const exists = await fsPromises2.access(target).then(() => true).catch(() => false);
89
+ if (exists) throw new Error(`Account '${name}' already exists`);
90
+ await populateAccount(claudeDir, target, paths.syncItems ?? []);
91
+ const current = await readCurrent(currentFile);
92
+ if (!current) {
93
+ await writeCurrent(currentFile, name);
94
+ }
95
+ }
96
+ async function switchAccount(name, paths) {
97
+ const nameErr = validateName(name);
98
+ if (nameErr) throw new Error(nameErr);
99
+ const { accountsDir, currentFile } = paths;
100
+ const target = path2.join(accountsDir, name);
101
+ const exists = await fsPromises2.access(target).then(() => true).catch(() => false);
102
+ if (!exists) throw new Error(`Account '${name}' does not exist`);
103
+ const current = await readCurrent(currentFile);
104
+ if (current === name) return `'${name}' is already active`;
105
+ await writeCurrent(currentFile, name);
106
+ return `Switched to: ${name}`;
107
+ }
108
+ async function removeAccount(name, paths) {
109
+ const nameErr = validateName(name);
110
+ if (nameErr) throw new Error(nameErr);
111
+ const { accountsDir, currentFile } = paths;
112
+ const target = path2.join(accountsDir, name);
113
+ const exists = await fsPromises2.access(target).then(() => true).catch(() => false);
114
+ if (!exists) throw new Error(`Account '${name}' does not exist`);
115
+ const current = await readCurrent(currentFile);
116
+ if (current === name) {
117
+ throw new Error(`Cannot remove '${name}' \u2014 it is currently active. Switch to another account first.`);
118
+ }
119
+ const all = await listAccounts(accountsDir);
120
+ if (all.length <= 1) {
121
+ throw new Error(`Cannot remove '${name}' \u2014 it is the last remaining account.`);
122
+ }
123
+ await fsPromises2.rm(target, { recursive: true, force: true });
124
+ }
125
+
126
+ // src/items.ts
127
+ import fsPromises3 from "fs/promises";
128
+ var DEFAULT_ITEMS = [
129
+ "agents",
130
+ "commands",
131
+ "file-history",
132
+ "hooks",
133
+ "plugins",
134
+ "projects",
135
+ "rules",
136
+ "session-env",
137
+ "sessions",
138
+ "settings.json",
139
+ "skills",
140
+ "tasks"
141
+ ];
142
+ var DEFAULT_CONFIG = {
143
+ syncItems: [...DEFAULT_ITEMS],
144
+ autoContinue: true
145
+ };
146
+ async function loadConfig(configFile) {
147
+ try {
148
+ const raw = await fsPromises3.readFile(configFile, "utf-8");
149
+ const parsed = JSON.parse(raw);
150
+ return {
151
+ syncItems: Array.isArray(parsed.syncItems) ? parsed.syncItems : [...DEFAULT_ITEMS],
152
+ autoContinue: parsed.autoContinue !== false
153
+ };
154
+ } catch {
155
+ const config = { ...DEFAULT_CONFIG, syncItems: [...DEFAULT_ITEMS] };
156
+ const dir = configFile.substring(0, configFile.lastIndexOf("/"));
157
+ await fsPromises3.mkdir(dir, { recursive: true, mode: 448 }).catch(() => void 0);
158
+ await saveConfig(configFile, config);
159
+ return config;
160
+ }
161
+ }
162
+ async function saveConfig(configFile, config) {
163
+ await fsPromises3.writeFile(configFile, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
164
+ }
165
+
166
+ // src/cli.ts
167
+ function launchClaude(accountDir, config) {
168
+ if (process.env.CC_SWAP_NO_LAUNCH) return;
169
+ const args = config.autoContinue ? ["--continue"] : [];
170
+ console.log(`Launching Claude Code (config: ${accountDir})...
171
+ `);
172
+ const result = spawnSync("claude", args, {
173
+ stdio: "inherit",
174
+ env: { ...process.env, CLAUDE_CONFIG_DIR: accountDir }
175
+ });
176
+ process.exit(result.status ?? 0);
177
+ }
178
+ var program = new Command();
179
+ program.name("cc-swap").description("Switch between multiple Claude Code accounts").version("0.1.0");
180
+ program.action(async () => {
181
+ const config = await loadConfig(CONFIG_FILE);
182
+ const accounts = await listAccounts(ACCOUNTS_DIR);
183
+ if (accounts.length === 0) {
184
+ console.log("No accounts found. Run 'cc-swap add <name>' to get started.");
185
+ return;
186
+ }
187
+ const current = await readCurrent(CURRENT_FILE);
188
+ if (!current) {
189
+ console.log("No active account. Run 'cc-swap switch <name>' to activate one.");
190
+ return;
191
+ }
192
+ const next = nextAccount(current, accounts);
193
+ if (!next) {
194
+ console.log(`Using account: ${current}`);
195
+ launchClaude(path3.join(ACCOUNTS_DIR, current), config);
196
+ return;
197
+ }
198
+ await switchAccount(next, {
199
+ claudeDir: CLAUDE_DIR,
200
+ accountsDir: ACCOUNTS_DIR,
201
+ currentFile: CURRENT_FILE
202
+ });
203
+ console.log(`Switched: ${current} \u2192 ${next}`);
204
+ launchClaude(path3.join(ACCOUNTS_DIR, next), config);
205
+ });
206
+ program.command("add <name>").description("Add a new account (populated with symlinks to ~/.claude)").action(async (name) => {
207
+ try {
208
+ const config = await loadConfig(CONFIG_FILE);
209
+ await addAccount(name, {
210
+ claudeDir: CLAUDE_DIR,
211
+ ccSwitchDir: CC_SWAP_DIR,
212
+ accountsDir: ACCOUNTS_DIR,
213
+ currentFile: CURRENT_FILE,
214
+ syncItems: config.syncItems
215
+ });
216
+ console.log(`Added account '${name}'.`);
217
+ } catch (err) {
218
+ console.error(`Error: ${err.message}`);
219
+ process.exit(1);
220
+ }
221
+ });
222
+ program.command("switch <name>").description("Switch to a specific account and launch Claude Code").action(async (name) => {
223
+ try {
224
+ const config = await loadConfig(CONFIG_FILE);
225
+ const msg = await switchAccount(name, {
226
+ claudeDir: CLAUDE_DIR,
227
+ accountsDir: ACCOUNTS_DIR,
228
+ currentFile: CURRENT_FILE
229
+ });
230
+ console.log(msg);
231
+ if (!msg.includes("already active")) {
232
+ launchClaude(path3.join(ACCOUNTS_DIR, name), config);
233
+ }
234
+ } catch (err) {
235
+ console.error(`Error: ${err.message}`);
236
+ process.exit(1);
237
+ }
238
+ });
239
+ program.command("list").description("List all accounts").action(async () => {
240
+ const accounts = await listAccounts(ACCOUNTS_DIR);
241
+ if (accounts.length === 0) {
242
+ console.log("No accounts found. Run 'cc-swap add <name>' to get started.");
243
+ return;
244
+ }
245
+ const current = await readCurrent(CURRENT_FILE);
246
+ for (const name of accounts) {
247
+ const marker = name === current ? "* " : " ";
248
+ const label = name === current ? " (active)" : "";
249
+ console.log(`${marker}${name}${label}`);
250
+ }
251
+ });
252
+ program.command("remove <name>").description("Remove an account").option("-f, --force", "Skip confirmation prompt").action(async (name, opts) => {
253
+ try {
254
+ if (!opts.force) {
255
+ const rl = createInterface({ input, output });
256
+ const answer = await rl.question(
257
+ `Remove account '${name}'? This will delete all its data. (y/N) `
258
+ );
259
+ rl.close();
260
+ if (answer.toLowerCase() !== "y") {
261
+ console.log("Cancelled.");
262
+ return;
263
+ }
264
+ }
265
+ await removeAccount(name, {
266
+ accountsDir: ACCOUNTS_DIR,
267
+ currentFile: CURRENT_FILE
268
+ });
269
+ console.log(`Removed account '${name}'.`);
270
+ } catch (err) {
271
+ console.error(`Error: ${err.message}`);
272
+ process.exit(1);
273
+ }
274
+ });
275
+ program.parseAsync();
@@ -0,0 +1,55 @@
1
+ # cc-switch Implementation Plan
2
+
3
+ > **Status: IMPLEMENTED** — All tasks completed.
4
+
5
+ **Goal:** Build a Node.js CLI tool that manages Claude Code accounts. Each account is a directory with symlinks back to `~/.claude` (source of truth). Switching sets `CLAUDE_CONFIG_DIR` and launches `claude --continue`.
6
+
7
+ **Architecture:** Single-binary CLI with Commander.js. Core logic: path constants, name validation, account population (symlinks back to `~/.claude`), account CRUD, and CLI entry point. All async with `fs/promises`. TDD with vitest.
8
+
9
+ **Tech Stack:** Node.js 18+, TypeScript, Commander.js, vitest, tsup (bundler)
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-03-22-cc-switch-design.md`
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ ```
18
+ src/
19
+ ├── cli.ts # Commander.js setup, command definitions, launches claude --continue
20
+ ├── paths.ts # Path constants with env var overrides for testing
21
+ ├── items.ts # SWITCH_ITEMS list (12 items that get symlinked per-account)
22
+ ├── validate.ts # Account name validation (sync - pure string logic)
23
+ ├── symlink.ts # populateAccount (symlinks back to ~/.claude), readCurrent, writeCurrent
24
+ ├── accounts.ts # listAccounts, addAccount, switchAccount, removeAccount, nextAccount
25
+ └── index.ts # Re-exports
26
+
27
+ tests/
28
+ ├── validate.test.ts # 6 tests
29
+ ├── symlink.test.ts # 10 tests (populateAccount, readCurrent, writeCurrent)
30
+ ├── accounts.test.ts # 15 tests (add, switch, remove, list, nextAccount)
31
+ └── cli.test.ts # 6 integration tests
32
+
33
+ package.json
34
+ tsconfig.json
35
+ tsup.config.ts
36
+ ```
37
+
38
+ ## Key Design Decisions
39
+
40
+ 1. **`~/.claude` is NEVER modified** — it is the source of truth
41
+ 2. **Account dirs contain symlinks BACK to `~/.claude`** — not the other way around
42
+ 3. **Custom overrides** — user replaces a symlink in account dir with real file/dir; `populateAccount` skips existing items
43
+ 4. **`CLAUDE_CONFIG_DIR` env var** — used to tell Claude Code to read config from account dir
44
+ 5. **`claude --continue`** — always resumes previous session when launching
45
+ 6. **Single account = launch directly** — `cc-switch` with 1 account just launches it, no error
46
+
47
+ ## Tasks (all completed)
48
+
49
+ - [x] Task 1: Project scaffolding (package.json, tsconfig, tsup, .gitignore)
50
+ - [x] Task 2: Path constants with env var overrides
51
+ - [x] Task 3: Account name validation + tests
52
+ - [x] Task 4: Symlink operations (populateAccount, readCurrent, writeCurrent) + tests
53
+ - [x] Task 5: Account CRUD operations + tests
54
+ - [x] Task 6: CLI entry point with all commands + integration tests
55
+ - [x] Task 7: Build, exports, link
@@ -0,0 +1,117 @@
1
+ # cc-switch - Claude Code Account Switcher
2
+
3
+ ## Overview
4
+
5
+ CLI tool (Node.js/TypeScript) to switch between multiple Claude Code accounts. Each account is a directory containing symlinks back to `~/.claude` (shared config) or custom override files. `~/.claude` is the source of truth and is **never modified** by cc-switch.
6
+
7
+ When launching Claude Code, the tool sets `CLAUDE_CONFIG_DIR` to the account directory, so Claude reads config from there.
8
+
9
+ ## Core Principle
10
+
11
+ ```
12
+ ~/.claude/ # SOURCE OF TRUTH — never modified
13
+ ├── settings.json # real file
14
+ ├── hooks/ # real dir
15
+ ├── plugins/ # real dir
16
+ └── ...
17
+
18
+ ~/.cc-switch/
19
+ ├── .current # active account name
20
+ └── claude-accounts/
21
+ ├── work/
22
+ │ ├── settings.json → ~/.claude/settings.json # symlink (shared)
23
+ │ ├── hooks → ~/.claude/hooks # symlink (shared)
24
+ │ └── plugins/ # REAL dir (custom override)
25
+ └── personal/
26
+ ├── settings.json # REAL file (custom override)
27
+ ├── hooks → ~/.claude/hooks # symlink (shared)
28
+ └── ...
29
+ ```
30
+
31
+ **Shared items** = symlinks back to `~/.claude` (default when adding account)
32
+ **Custom overrides** = replace the symlink with a real file/dir in the account folder
33
+
34
+ ## Switchable Items
35
+
36
+ The following items inside `~/.claude` are symlinked per-account:
37
+
38
+ - `agents/`, `commands/`, `file-history/`, `hooks/`, `plugins/`
39
+ - `projects/`, `rules/`, `session-env/`, `sessions/`
40
+ - `settings.json`, `skills/`, `tasks/`
41
+
42
+ ## CLI Commands
43
+
44
+ ### `cc-switch` (no arguments)
45
+
46
+ Auto switch to the next account (round-robin) and launch Claude Code.
47
+
48
+ 1. Read `.current` to get active account name
49
+ 2. List folders in `claude-accounts/`, sorted alphabetically (case-insensitive)
50
+ 3. If only 1 account → use that account directly
51
+ 4. If 2+ accounts → switch to the next one (wrap to first at end), update `.current`
52
+ 5. Launch `claude --continue` with `CLAUDE_CONFIG_DIR` set to the account directory
53
+
54
+ ### `cc-switch add <name>`
55
+
56
+ Create a new account populated with symlinks back to `~/.claude`.
57
+
58
+ 1. Validate name (alphanumeric, hyphens, underscores; max 50 chars; no `.` or `-` prefix)
59
+ 2. Create `~/.cc-switch/claude-accounts/<name>/`
60
+ 3. For each switchable item that exists in `~/.claude`: create symlink `<name>/<item>` → `~/.claude/<item>`
61
+ 4. If this is the first account, set it as `.current`
62
+ 5. `~/.claude` is NOT modified
63
+
64
+ ### `cc-switch switch <name>`
65
+
66
+ Switch to a specific account and launch Claude Code.
67
+
68
+ 1. Validate `<name>` exists in `claude-accounts/`
69
+ 2. If already active → print message and exit
70
+ 3. Update `.current`
71
+ 4. Launch `claude --continue` with `CLAUDE_CONFIG_DIR=<account-dir>`
72
+
73
+ ### `cc-switch list`
74
+
75
+ List all accounts, marking the active one.
76
+
77
+ ```
78
+ work
79
+ * personal (active)
80
+ ```
81
+
82
+ ### `cc-switch remove <name>`
83
+
84
+ Remove an account.
85
+
86
+ 1. Prevent removing the currently active account
87
+ 2. Prevent removing the last remaining account
88
+ 3. Confirm with user (prompt y/n), support `--force` flag to skip
89
+ 4. Remove `~/.cc-switch/claude-accounts/<name>/`
90
+
91
+ ## How It Works
92
+
93
+ - **Adding**: `cc-switch add work` creates `~/.cc-switch/claude-accounts/work/` with symlinks pointing back to `~/.claude/` for each switchable item
94
+ - **Switching**: `cc-switch switch work` updates `.current` and runs `claude --continue` with `CLAUDE_CONFIG_DIR=~/.cc-switch/claude-accounts/work`
95
+ - **Custom overrides**: User can replace any symlink in the account folder with a real file/dir. `cc-switch` will not overwrite existing items when populating.
96
+ - **`~/.claude` is sacred**: cc-switch never reads from, writes to, or modifies `~/.claude` contents. It only reads to create initial symlinks during `add`.
97
+
98
+ ## Safety Checks
99
+
100
+ - **`~/.claude` is never modified** — only used as symlink target
101
+ - **Validate account names**: alphanumeric, hyphens, underscores only. Max 50 chars. Cannot start with `.` or `-`
102
+ - **Preserve custom overrides**: `populateAccount` skips items that already exist in account dir
103
+ - **Permissions**: create `~/.cc-switch/` with mode `700`
104
+
105
+ ## Tech Stack
106
+
107
+ - Node.js + TypeScript
108
+ - Commander.js for CLI parsing
109
+ - Native `fs/promises` for async filesystem operations
110
+ - `CLAUDE_CONFIG_DIR` env var to direct Claude Code to account folder
111
+ - No heavy dependencies
112
+
113
+ ## Package
114
+
115
+ - Name: `cc-switch`
116
+ - Binary: `cc-switch`
117
+ - Install: `npm install -g cc-switch`
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "cc-swap",
3
+ "version": "0.1.0",
4
+ "description": "Switch between multiple Claude Code accounts using symlinks",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-swap": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "tsup --watch",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "claude-code",
18
+ "account-switcher"
19
+ ],
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "@types/node": "^25.5.0",
23
+ "tsup": "^8.5.1",
24
+ "tsx": "^4.21.0",
25
+ "typescript": "^5.9.3",
26
+ "vitest": "^4.1.0"
27
+ },
28
+ "dependencies": {
29
+ "commander": "^14.0.3"
30
+ }
31
+ }
@@ -0,0 +1,105 @@
1
+ import fsPromises from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { validateName } from "./validate.js";
4
+ import { populateAccount, readCurrent, writeCurrent } from "./symlink.js";
5
+
6
+ interface AccountPaths {
7
+ claudeDir: string;
8
+ ccSwitchDir?: string;
9
+ accountsDir: string;
10
+ currentFile: string;
11
+ syncItems?: string[];
12
+ }
13
+
14
+ export async function listAccounts(accountsDir: string): Promise<string[]> {
15
+ try {
16
+ const entries = await fsPromises.readdir(accountsDir, { withFileTypes: true });
17
+ return entries
18
+ .filter((e) => e.isDirectory())
19
+ .map((e) => e.name)
20
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ export function nextAccount(current: string, accounts: string[]): string | null {
27
+ if (accounts.length <= 1) return null;
28
+ const idx = accounts.indexOf(current);
29
+ return accounts[(idx + 1) % accounts.length];
30
+ }
31
+
32
+ export async function addAccount(name: string, paths: AccountPaths): Promise<void> {
33
+ const { claudeDir, ccSwitchDir, accountsDir, currentFile } = paths;
34
+
35
+ const nameErr = validateName(name);
36
+ if (nameErr) throw new Error(nameErr);
37
+
38
+ const accountsExist = await fsPromises.access(accountsDir).then(() => true).catch(() => false);
39
+
40
+ if (!accountsExist) {
41
+ await fsPromises.mkdir(accountsDir, { recursive: true, mode: 0o700 });
42
+ if (ccSwitchDir) {
43
+ await fsPromises.chmod(ccSwitchDir, 0o700);
44
+ }
45
+ }
46
+
47
+ const target = path.join(accountsDir, name);
48
+ const exists = await fsPromises.access(target).then(() => true).catch(() => false);
49
+ if (exists) throw new Error(`Account '${name}' already exists`);
50
+
51
+ // Create account dir with symlinks pointing back to ~/.claude
52
+ await populateAccount(claudeDir, target, paths.syncItems ?? []);
53
+
54
+ // Set as current if this is the first account
55
+ const current = await readCurrent(currentFile);
56
+ if (!current) {
57
+ await writeCurrent(currentFile, name);
58
+ }
59
+ }
60
+
61
+ export async function switchAccount(
62
+ name: string,
63
+ paths: Pick<AccountPaths, "claudeDir" | "accountsDir" | "currentFile">
64
+ ): Promise<string> {
65
+ const nameErr = validateName(name);
66
+ if (nameErr) throw new Error(nameErr);
67
+
68
+ const { accountsDir, currentFile } = paths;
69
+ const target = path.join(accountsDir, name);
70
+
71
+ const exists = await fsPromises.access(target).then(() => true).catch(() => false);
72
+ if (!exists) throw new Error(`Account '${name}' does not exist`);
73
+
74
+ const current = await readCurrent(currentFile);
75
+ if (current === name) return `'${name}' is already active`;
76
+
77
+ await writeCurrent(currentFile, name);
78
+ return `Switched to: ${name}`;
79
+ }
80
+
81
+ export async function removeAccount(
82
+ name: string,
83
+ paths: Pick<AccountPaths, "accountsDir" | "currentFile">
84
+ ): Promise<void> {
85
+ const nameErr = validateName(name);
86
+ if (nameErr) throw new Error(nameErr);
87
+
88
+ const { accountsDir, currentFile } = paths;
89
+ const target = path.join(accountsDir, name);
90
+
91
+ const exists = await fsPromises.access(target).then(() => true).catch(() => false);
92
+ if (!exists) throw new Error(`Account '${name}' does not exist`);
93
+
94
+ const current = await readCurrent(currentFile);
95
+ if (current === name) {
96
+ throw new Error(`Cannot remove '${name}' — it is currently active. Switch to another account first.`);
97
+ }
98
+
99
+ const all = await listAccounts(accountsDir);
100
+ if (all.length <= 1) {
101
+ throw new Error(`Cannot remove '${name}' — it is the last remaining account.`);
102
+ }
103
+
104
+ await fsPromises.rm(target, { recursive: true, force: true });
105
+ }