@synth1s/cloak 1.0.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/LICENSE +21 -0
- package/README.md +103 -0
- package/package.json +41 -0
- package/src/cli.js +70 -0
- package/src/commands/create.js +77 -0
- package/src/commands/delete.js +48 -0
- package/src/commands/init.js +30 -0
- package/src/commands/launch.js +29 -0
- package/src/commands/list.js +29 -0
- package/src/commands/rename.js +40 -0
- package/src/commands/switch.js +37 -0
- package/src/commands/whoami.js +12 -0
- package/src/lib/paths.js +69 -0
- package/src/lib/validate.js +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 synth1s
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @synth1s/cloak
|
|
2
|
+
|
|
3
|
+
> Cloak your Claude. Switch identities in seconds.
|
|
4
|
+
|
|
5
|
+
Every developer wears a different cloak. One for work, one for personal projects, one for that freelance gig. **Cloak** lets you dress your Claude Code in the right identity — and switch between them without breaking a sweat.
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
Claude Code stores your session in `~/.claude.json`. There's no built-in support for multiple accounts, so switching between personal and work means running `/logout` and `/login` every time — losing your session state in the process.
|
|
10
|
+
|
|
11
|
+
## The solution
|
|
12
|
+
|
|
13
|
+
Cloak gives each account its own isolated directory using Claude Code's official [`CLAUDE_CONFIG_DIR`](https://code.claude.com/docs/en/env-vars) environment variable. Each identity stays separate. No file conflicts. No data loss. Full support for concurrent sessions.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @synth1s/cloak
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add to your `.bashrc` or `.zshrc`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
eval "$(cloak init)"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Cloak your Claude with your work identity
|
|
31
|
+
claude account create work
|
|
32
|
+
|
|
33
|
+
# Log out, log in with another account, then:
|
|
34
|
+
claude account create home
|
|
35
|
+
|
|
36
|
+
# Throw on a cloak and go
|
|
37
|
+
claude -a work
|
|
38
|
+
claude -a home
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
### Wardrobe management
|
|
44
|
+
|
|
45
|
+
| Command | Alias | Description |
|
|
46
|
+
|---------|-------|-------------|
|
|
47
|
+
| `claude account create [name]` | | Save current session as a new cloak |
|
|
48
|
+
| `claude account switch <name>` | `use` | Wear a different cloak |
|
|
49
|
+
| `claude account list` | `ls` | See all cloaks in your wardrobe |
|
|
50
|
+
| `claude account delete <name>` | `rm` | Discard a cloak |
|
|
51
|
+
| `claude account whoami` | | Which cloak are you wearing? |
|
|
52
|
+
| `claude account rename <old> <new>` | | Rename a cloak |
|
|
53
|
+
|
|
54
|
+
### Shortcut
|
|
55
|
+
|
|
56
|
+
| Command | Description |
|
|
57
|
+
|---------|-------------|
|
|
58
|
+
| `claude -a <name>` | Throw on a cloak and launch Claude |
|
|
59
|
+
| `claude -a <name> [args...]` | Throw on a cloak and launch with arguments |
|
|
60
|
+
|
|
61
|
+
## Concurrent sessions
|
|
62
|
+
|
|
63
|
+
Different terminal, different cloak. No conflicts.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Terminal A — wearing the work cloak:
|
|
67
|
+
claude -a work
|
|
68
|
+
|
|
69
|
+
# Terminal B — wearing the home cloak:
|
|
70
|
+
claude -a home
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
Each cloak is an isolated directory that acts as a [`CLAUDE_CONFIG_DIR`](https://code.claude.com/docs/en/env-vars):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
~/.cloak/
|
|
79
|
+
└── profiles/
|
|
80
|
+
├── work/ # Work identity
|
|
81
|
+
│ ├── .claude.json
|
|
82
|
+
│ ├── settings.json
|
|
83
|
+
│ └── ...
|
|
84
|
+
└── home/ # Personal identity
|
|
85
|
+
├── .claude.json
|
|
86
|
+
└── ...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
When you run `claude -a work`, Cloak sets `CLAUDE_CONFIG_DIR=~/.cloak/profiles/work` and launches Claude Code. That's it. Each terminal gets its own environment, so you can wear different cloaks simultaneously.
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Node.js >= 18
|
|
94
|
+
- bash or zsh (for shell integration)
|
|
95
|
+
|
|
96
|
+
## Documentation
|
|
97
|
+
|
|
98
|
+
- [Requirements & use cases](docs/requirements.md)
|
|
99
|
+
- [Technical specification](docs/technical-spec.md)
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synth1s/cloak",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cloak your Claude. Switch identities in seconds.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cloak": "./src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test tests/"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"anthropic",
|
|
16
|
+
"account",
|
|
17
|
+
"profile",
|
|
18
|
+
"cli",
|
|
19
|
+
"switch",
|
|
20
|
+
"multi-account",
|
|
21
|
+
"cloak"
|
|
22
|
+
],
|
|
23
|
+
"author": "synth1s",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^12.1.0",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"inquirer": "^10.1.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/synth1s/cloak.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/synth1s/cloak#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/synth1s/cloak/issues"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander'
|
|
4
|
+
import { readFileSync } from 'fs'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { dirname, join } from 'path'
|
|
7
|
+
|
|
8
|
+
import { createAccount } from './commands/create.js'
|
|
9
|
+
import { switchAccount } from './commands/switch.js'
|
|
10
|
+
import { listAccounts } from './commands/list.js'
|
|
11
|
+
import { deleteAccount } from './commands/delete.js'
|
|
12
|
+
import { whoami } from './commands/whoami.js'
|
|
13
|
+
import { renameAccount } from './commands/rename.js'
|
|
14
|
+
import { launchAccount } from './commands/launch.js'
|
|
15
|
+
import { initShell } from './commands/init.js'
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.name('cloak')
|
|
22
|
+
.description('Cloak your Claude. Switch identities in seconds.')
|
|
23
|
+
.version(pkg.version)
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('create [name]')
|
|
27
|
+
.description('Save current session as a new cloak')
|
|
28
|
+
.action(createAccount)
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('switch <name>')
|
|
32
|
+
.alias('use')
|
|
33
|
+
.description('Wear a different cloak')
|
|
34
|
+
.option('--print-env', 'Output export command for eval (used by shell integration)')
|
|
35
|
+
.action((name, opts) => switchAccount(name, { printEnv: opts.printEnv }))
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('list')
|
|
39
|
+
.alias('ls')
|
|
40
|
+
.description('See all cloaks in your wardrobe')
|
|
41
|
+
.action(listAccounts)
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('delete <name>')
|
|
45
|
+
.alias('rm')
|
|
46
|
+
.description('Discard a cloak')
|
|
47
|
+
.action((name) => deleteAccount(name))
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command('whoami')
|
|
51
|
+
.description('Which cloak are you wearing?')
|
|
52
|
+
.action(whoami)
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command('rename <old> <new>')
|
|
56
|
+
.description('Rename a cloak')
|
|
57
|
+
.action(renameAccount)
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('launch <name>')
|
|
61
|
+
.description('Wear a cloak and launch Claude')
|
|
62
|
+
.argument('[args...]', 'Arguments to pass to claude')
|
|
63
|
+
.action((name, args) => launchAccount(name, args))
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('init')
|
|
67
|
+
.description('Output shell integration code')
|
|
68
|
+
.action(initShell)
|
|
69
|
+
|
|
70
|
+
program.parse()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, copyFileSync, mkdirSync } from 'fs'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import {
|
|
5
|
+
claudeAuthPath,
|
|
6
|
+
claudeSettingsPath,
|
|
7
|
+
profileDir,
|
|
8
|
+
profileAuthPath,
|
|
9
|
+
profileSettingsPath,
|
|
10
|
+
profileExists,
|
|
11
|
+
ensureProfilesDir,
|
|
12
|
+
} from '../lib/paths.js'
|
|
13
|
+
import { validateAccountName } from '../lib/validate.js'
|
|
14
|
+
|
|
15
|
+
export async function createAccount(name, options = {}) {
|
|
16
|
+
// Interactive prompt if no name given
|
|
17
|
+
if (!name) {
|
|
18
|
+
const answer = await inquirer.prompt([{
|
|
19
|
+
type: 'input',
|
|
20
|
+
name: 'accountName',
|
|
21
|
+
message: 'Account name:',
|
|
22
|
+
validate: (v) => {
|
|
23
|
+
const result = validateAccountName(v.trim())
|
|
24
|
+
return result.valid || result.error
|
|
25
|
+
},
|
|
26
|
+
}])
|
|
27
|
+
name = answer.accountName.trim()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const validation = validateAccountName(name)
|
|
31
|
+
if (!validation.valid) {
|
|
32
|
+
console.error(chalk.red(`✖ ${validation.error}`))
|
|
33
|
+
process.exit(1)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const authSource = claudeAuthPath()
|
|
38
|
+
if (!existsSync(authSource)) {
|
|
39
|
+
console.error(chalk.red('✖ No active Claude Code session found.'))
|
|
40
|
+
console.log(chalk.dim(' Open Claude Code and log in first.'))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (profileExists(name)) {
|
|
46
|
+
if (options.confirm === false) {
|
|
47
|
+
console.log(chalk.dim('Cancelled.'))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (options.confirm === undefined) {
|
|
51
|
+
const { overwrite } = await inquirer.prompt([{
|
|
52
|
+
type: 'confirm',
|
|
53
|
+
name: 'overwrite',
|
|
54
|
+
message: `Cloak "${name}" already exists. Overwrite?`,
|
|
55
|
+
default: false,
|
|
56
|
+
}])
|
|
57
|
+
if (!overwrite) {
|
|
58
|
+
console.log(chalk.dim('Cancelled.'))
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// options.confirm === true → proceed
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ensureProfilesDir()
|
|
66
|
+
const dir = profileDir(name)
|
|
67
|
+
mkdirSync(dir, { recursive: true })
|
|
68
|
+
|
|
69
|
+
copyFileSync(authSource, profileAuthPath(name))
|
|
70
|
+
|
|
71
|
+
const settingsSource = claudeSettingsPath()
|
|
72
|
+
if (existsSync(settingsSource)) {
|
|
73
|
+
copyFileSync(settingsSource, profileSettingsPath(name))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(chalk.green(`✔ Cloak "${name}" created.`))
|
|
77
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { rmSync } from 'fs'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import { profileDir, profileExists, getActiveProfile } from '../lib/paths.js'
|
|
5
|
+
import { validateAccountName } from '../lib/validate.js'
|
|
6
|
+
|
|
7
|
+
export async function deleteAccount(name, options = {}) {
|
|
8
|
+
const validation = validateAccountName(name)
|
|
9
|
+
if (!validation.valid) {
|
|
10
|
+
console.error(chalk.red(`✖ ${validation.error}`))
|
|
11
|
+
process.exit(1)
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!profileExists(name)) {
|
|
16
|
+
console.error(chalk.red(`✖ Account "${name}" not found.`))
|
|
17
|
+
process.exit(1)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (getActiveProfile() === name) {
|
|
22
|
+
console.error(chalk.red(`✖ Can't discard a cloak you're wearing.`))
|
|
23
|
+
console.log(chalk.dim(' Switch to another account first.'))
|
|
24
|
+
process.exit(1)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.confirm === false) {
|
|
29
|
+
console.log(chalk.dim('Cancelled.'))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.confirm === undefined) {
|
|
34
|
+
const { confirm } = await inquirer.prompt([{
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'confirm',
|
|
37
|
+
message: `Delete cloak "${name}"?`,
|
|
38
|
+
default: false,
|
|
39
|
+
}])
|
|
40
|
+
if (!confirm) {
|
|
41
|
+
console.log(chalk.dim('Cancelled.'))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
rmSync(profileDir(name), { recursive: true, force: true })
|
|
47
|
+
console.log(chalk.green(`✔ Cloak "${name}" discarded.`))
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function getInitScript() {
|
|
2
|
+
/* eslint-disable no-template-curly-in-string */
|
|
3
|
+
const lines = [
|
|
4
|
+
'claude() {',
|
|
5
|
+
' if [ "$1" = "account" ]; then',
|
|
6
|
+
' local subcmd="$2"',
|
|
7
|
+
' shift 2',
|
|
8
|
+
' if [ "$subcmd" = "switch" ] || [ "$subcmd" = "use" ]; then',
|
|
9
|
+
' local output',
|
|
10
|
+
' output=$(command cloak switch --print-env "$@")',
|
|
11
|
+
' local exit_code=$?',
|
|
12
|
+
' if [ $exit_code -eq 0 ]; then',
|
|
13
|
+
' eval "$output"',
|
|
14
|
+
' fi',
|
|
15
|
+
' else',
|
|
16
|
+
' command cloak "$subcmd" "$@"',
|
|
17
|
+
' fi',
|
|
18
|
+
' elif [ "$1" = "-a" ] && [ -n "$2" ]; then',
|
|
19
|
+
' command cloak launch "${@:2}"',
|
|
20
|
+
' else',
|
|
21
|
+
' command claude "$@"',
|
|
22
|
+
' fi',
|
|
23
|
+
'}',
|
|
24
|
+
]
|
|
25
|
+
return lines.join('\n') + '\n'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function initShell() {
|
|
29
|
+
process.stdout.write(getInitScript())
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from 'child_process'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { profileDir, profileExists } from '../lib/paths.js'
|
|
4
|
+
import { validateAccountName } from '../lib/validate.js'
|
|
5
|
+
|
|
6
|
+
export function launchAccount(name, extraArgs = [], spawner = defaultSpawn) {
|
|
7
|
+
const validation = validateAccountName(name)
|
|
8
|
+
if (!validation.valid) {
|
|
9
|
+
return Promise.reject(new Error(validation.error))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!profileExists(name)) {
|
|
13
|
+
return Promise.reject(new Error(`Account "${name}" not found. Run: claude account create ${name}`))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
process.env.CLAUDE_CONFIG_DIR = profileDir(name)
|
|
17
|
+
console.log(chalk.green(`✔ Now wearing cloak "${name}".`))
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const child = spawner('claude', extraArgs, {
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
env: process.env,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
child.on('close', (code) => {
|
|
26
|
+
resolve(code)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { listProfileNames, getActiveProfile } from '../lib/paths.js'
|
|
3
|
+
|
|
4
|
+
export function listAccounts() {
|
|
5
|
+
const names = listProfileNames().sort()
|
|
6
|
+
const active = getActiveProfile()
|
|
7
|
+
|
|
8
|
+
const accounts = names.map(name => ({
|
|
9
|
+
name,
|
|
10
|
+
active: name === active,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
if (accounts.length === 0) {
|
|
14
|
+
console.log(chalk.dim('No cloaks in your wardrobe yet.'))
|
|
15
|
+
console.log(chalk.dim('Run: claude account create <name>'))
|
|
16
|
+
return accounts
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(chalk.bold('\nClaude Code Accounts\n'))
|
|
20
|
+
accounts.forEach(({ name, active: isActive }) => {
|
|
21
|
+
const marker = isActive ? chalk.green('● ') : chalk.dim('○ ')
|
|
22
|
+
const label = isActive ? chalk.green.bold(name) : chalk.white(name)
|
|
23
|
+
const tag = isActive ? chalk.green(' (active)') : ''
|
|
24
|
+
console.log(` ${marker}${label}${tag}`)
|
|
25
|
+
})
|
|
26
|
+
console.log()
|
|
27
|
+
|
|
28
|
+
return accounts
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { renameSync } from 'fs'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { profileDir, profileExists, getActiveProfile } from '../lib/paths.js'
|
|
4
|
+
import { validateAccountName } from '../lib/validate.js'
|
|
5
|
+
|
|
6
|
+
export async function renameAccount(oldName, newName) {
|
|
7
|
+
const oldValidation = validateAccountName(oldName)
|
|
8
|
+
if (!oldValidation.valid) {
|
|
9
|
+
console.error(chalk.red(`✖ ${oldValidation.error}`))
|
|
10
|
+
process.exit(1)
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const newValidation = validateAccountName(newName)
|
|
15
|
+
if (!newValidation.valid) {
|
|
16
|
+
console.error(chalk.red(`✖ ${newValidation.error}`))
|
|
17
|
+
process.exit(1)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!profileExists(oldName)) {
|
|
22
|
+
console.error(chalk.red(`✖ Account "${oldName}" not found.`))
|
|
23
|
+
process.exit(1)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (profileExists(newName)) {
|
|
28
|
+
console.error(chalk.red(`✖ Account "${newName}" is already in use.`))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
renameSync(profileDir(oldName), profileDir(newName))
|
|
34
|
+
|
|
35
|
+
if (getActiveProfile() === oldName) {
|
|
36
|
+
console.log(chalk.yellow(`⚠ Run \`claude account switch ${newName}\` to update your session.`))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.green(`✔ Cloak "${oldName}" renamed to "${newName}".`))
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { profileDir, profileExists, getActiveProfile } from '../lib/paths.js'
|
|
3
|
+
import { validateAccountName } from '../lib/validate.js'
|
|
4
|
+
|
|
5
|
+
export async function switchAccount(name, options = {}) {
|
|
6
|
+
const validation = validateAccountName(name)
|
|
7
|
+
if (!validation.valid) {
|
|
8
|
+
console.error(chalk.red(`✖ ${validation.error}`))
|
|
9
|
+
process.exit(1)
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!profileExists(name)) {
|
|
14
|
+
console.error(chalk.red(`✖ Account "${name}" not found.`))
|
|
15
|
+
console.log(chalk.dim(` Run: claude account create ${name}`))
|
|
16
|
+
process.exit(1)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const active = getActiveProfile()
|
|
21
|
+
if (active === name) {
|
|
22
|
+
console.log(chalk.yellow(`⚡ Already wearing cloak "${name}".`))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const dir = profileDir(name)
|
|
27
|
+
|
|
28
|
+
if (options.printEnv) {
|
|
29
|
+
process.stdout.write(`export CLAUDE_CONFIG_DIR=${dir}\n`)
|
|
30
|
+
process.stdout.write(`echo "${chalk.green(`✔ Now wearing cloak "${name}".`)}"\n`)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Manual instructions (no shell integration)
|
|
35
|
+
console.log(chalk.dim('Run this command to switch:'))
|
|
36
|
+
console.log(`\n export CLAUDE_CONFIG_DIR=${dir}\n`)
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { getActiveProfile } from '../lib/paths.js'
|
|
3
|
+
|
|
4
|
+
export function whoami() {
|
|
5
|
+
const active = getActiveProfile()
|
|
6
|
+
if (!active) {
|
|
7
|
+
console.log(chalk.dim('No cloak. Using default Claude Code config.'))
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
console.log(active)
|
|
11
|
+
return active
|
|
12
|
+
}
|
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { homedir } from 'os'
|
|
2
|
+
import { join, resolve, sep } from 'path'
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync } from 'fs'
|
|
4
|
+
|
|
5
|
+
function getHome() {
|
|
6
|
+
return process.env.HOME || homedir()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const HOME = getHome()
|
|
10
|
+
export const CLOAK_DIR = join(HOME, '.cloak')
|
|
11
|
+
export const PROFILES_DIR = join(CLOAK_DIR, 'profiles')
|
|
12
|
+
|
|
13
|
+
export function claudeAuthPath() {
|
|
14
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR
|
|
15
|
+
if (configDir) {
|
|
16
|
+
return join(configDir, '.claude.json')
|
|
17
|
+
}
|
|
18
|
+
return join(HOME, '.claude.json')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function claudeSettingsPath() {
|
|
22
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR
|
|
23
|
+
if (configDir) {
|
|
24
|
+
return join(configDir, 'settings.json')
|
|
25
|
+
}
|
|
26
|
+
return join(HOME, '.claude', 'settings.json')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function profileDir(name) {
|
|
30
|
+
return join(PROFILES_DIR, name)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function profileAuthPath(name) {
|
|
34
|
+
return join(PROFILES_DIR, name, '.claude.json')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function profileSettingsPath(name) {
|
|
38
|
+
return join(PROFILES_DIR, name, 'settings.json')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ensureProfilesDir() {
|
|
42
|
+
if (!existsSync(PROFILES_DIR)) {
|
|
43
|
+
mkdirSync(PROFILES_DIR, { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function profileExists(name) {
|
|
48
|
+
return existsSync(profileDir(name))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function listProfileNames() {
|
|
52
|
+
ensureProfilesDir()
|
|
53
|
+
return readdirSync(PROFILES_DIR, { withFileTypes: true })
|
|
54
|
+
.filter(d => d.isDirectory())
|
|
55
|
+
.map(d => d.name)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getActiveProfile() {
|
|
59
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR
|
|
60
|
+
if (!configDir) return null
|
|
61
|
+
|
|
62
|
+
const resolved = resolve(configDir)
|
|
63
|
+
const profilesResolved = resolve(PROFILES_DIR)
|
|
64
|
+
|
|
65
|
+
if (!resolved.startsWith(profilesResolved + sep)) return null
|
|
66
|
+
|
|
67
|
+
const name = resolved.slice(profilesResolved.length + 1).split(sep)[0]
|
|
68
|
+
return name || null
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/
|
|
2
|
+
|
|
3
|
+
export function validateAccountName(name) {
|
|
4
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
5
|
+
return { valid: false, error: 'Account name is required.' }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (name.length > 64) {
|
|
9
|
+
return { valid: false, error: 'Account name must be at most 64 characters.' }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!/^[a-zA-Z0-9]/.test(name)) {
|
|
13
|
+
return { valid: false, error: 'Account name must start with a letter or number.' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!NAME_PATTERN.test(name)) {
|
|
17
|
+
return { valid: false, error: 'Account name can only contain letters, numbers, hyphens and underscores.' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { valid: true }
|
|
21
|
+
}
|