@synth1s/cloak 1.4.1 → 1.5.1

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 CHANGED
@@ -98,6 +98,47 @@ Each cloak is an isolated directory that acts as a [`CLAUDE_CONFIG_DIR`](https:/
98
98
 
99
99
  When you run `claude -a work`, Cloak sets `CLAUDE_CONFIG_DIR=~/.cloak/profiles/work` in your current shell and launches Claude Code. Each terminal gets its own environment, so you can wear different cloaks simultaneously.
100
100
 
101
+ ## FAQ
102
+
103
+ ### Will switching accounts overwrite my settings or preferences?
104
+
105
+ No. Each account is a completely isolated directory. Switching only changes which directory Claude Code reads from — it doesn't copy, move, or overwrite any files. Your settings, MCP servers, and preferences for each account stay exactly where they are.
106
+
107
+ ### Can I lose data when running multiple accounts simultaneously?
108
+
109
+ No, as long as each terminal uses a **different** account. Each account has its own directory (`~/.cloak/profiles/<name>/`), so there's no file overlap. Terminal A writing to `work/` and Terminal B writing to `home/` never interfere.
110
+
111
+ ### What about token renewals? Are they preserved when I switch?
112
+
113
+ Yes. When Claude Code renews your OAuth token during a session, it writes the new token to the active account's directory. When you switch away and back, the renewed token is still there — Cloak never touches those files.
114
+
115
+ ### What happens if I create a new account? Does it affect existing ones?
116
+
117
+ No. `cloak create` copies your current session into a **new** directory. Existing accounts are not modified. It's a snapshot, not a move.
118
+
119
+ ### What if I run the same account in two terminals at once?
120
+
121
+ This is not recommended. Two Claude Code instances writing to the same directory (`~/.cloak/profiles/work/`) can cause token conflicts. This is a Claude Code limitation, not specific to Cloak — the same issue exists without Cloak if you open two `claude` instances normally.
122
+
123
+ ### What happens if I uninstall Cloak?
124
+
125
+ Your account directories remain in `~/.cloak/`. Claude Code continues to work normally with its default config. To use a saved account manually, set the environment variable:
126
+
127
+ ```bash
128
+ export CLAUDE_CONFIG_DIR=~/.cloak/profiles/work
129
+ claude
130
+ ```
131
+
132
+ To clean up completely: `rm -rf ~/.cloak`
133
+
134
+ ### Is my auth token safe?
135
+
136
+ Cloak never transmits, logs, or modifies your tokens. It only copies files during `cloak create` (from Claude Code's config to a profile directory) and changes an environment variable during `cloak switch`. All data stays local on your machine.
137
+
138
+ ### Does Cloak work with Claude Code IDE extensions (VSCode, JetBrains)?
139
+
140
+ IDE extensions may not respect `CLAUDE_CONFIG_DIR` ([known limitation](https://github.com/anthropics/claude-code/issues/4739)). Cloak is designed for terminal-based Claude Code usage.
141
+
101
142
  ## Requirements
102
143
 
103
144
  - Node.js >= 18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synth1s/cloak",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "Cloak your Claude. Switch identities in seconds.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,4 @@
1
1
  import { existsSync, copyFileSync, mkdirSync } from 'fs'
2
- import chalk from 'chalk'
3
2
  import inquirer from 'inquirer'
4
3
  import {
5
4
  claudeAuthPath,
@@ -11,14 +10,14 @@ import {
11
10
  ensureProfilesDir,
12
11
  } from '../lib/paths.js'
13
12
  import { validateAccountName } from '../lib/validate.js'
13
+ import * as msg from '../lib/messages.js'
14
14
 
15
15
  export async function createAccount(name, options = {}) {
16
- // Interactive prompt if no name given
17
16
  if (!name) {
18
17
  const answer = await inquirer.prompt([{
19
18
  type: 'input',
20
19
  name: 'accountName',
21
- message: 'Account name:',
20
+ message: msg.prompts.accountName,
22
21
  validate: (v) => {
23
22
  const result = validateAccountName(v.trim())
24
23
  return result.valid || result.error
@@ -29,37 +28,36 @@ export async function createAccount(name, options = {}) {
29
28
 
30
29
  const validation = validateAccountName(name)
31
30
  if (!validation.valid) {
32
- console.error(chalk.red(`✖ ${validation.error}`))
31
+ console.error(msg.validationError(validation.error))
33
32
  process.exit(1)
34
33
  return
35
34
  }
36
35
 
37
36
  const authSource = claudeAuthPath()
38
37
  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.'))
38
+ console.error(msg.noActiveSession())
39
+ console.log(msg.loginFirst())
41
40
  process.exit(1)
42
41
  return
43
42
  }
44
43
 
45
44
  if (profileExists(name)) {
46
45
  if (options.confirm === false) {
47
- console.log(chalk.dim('Cancelled.'))
46
+ console.log(msg.cancelled())
48
47
  return
49
48
  }
50
49
  if (options.confirm === undefined) {
51
50
  const { overwrite } = await inquirer.prompt([{
52
51
  type: 'confirm',
53
52
  name: 'overwrite',
54
- message: `Cloak "${name}" already exists. Overwrite?`,
53
+ message: msg.prompts.overwriteConfirm(name),
55
54
  default: false,
56
55
  }])
57
56
  if (!overwrite) {
58
- console.log(chalk.dim('Cancelled.'))
57
+ console.log(msg.cancelled())
59
58
  return
60
59
  }
61
60
  }
62
- // options.confirm === true → proceed
63
61
  }
64
62
 
65
63
  ensureProfilesDir()
@@ -73,5 +71,5 @@ export async function createAccount(name, options = {}) {
73
71
  copyFileSync(settingsSource, profileSettingsPath(name))
74
72
  }
75
73
 
76
- console.log(chalk.green(`✔ Cloak "${name}" created.`))
74
+ console.log(msg.cloakCreated(name))
77
75
  }
@@ -1,32 +1,32 @@
1
1
  import { rmSync } from 'fs'
2
- import chalk from 'chalk'
3
2
  import inquirer from 'inquirer'
4
3
  import { profileDir, profileExists, getActiveProfile } from '../lib/paths.js'
5
4
  import { validateAccountName } from '../lib/validate.js'
5
+ import * as msg from '../lib/messages.js'
6
6
 
7
7
  export async function deleteAccount(name, options = {}) {
8
8
  const validation = validateAccountName(name)
9
9
  if (!validation.valid) {
10
- console.error(chalk.red(`✖ ${validation.error}`))
10
+ console.error(msg.validationError(validation.error))
11
11
  process.exit(1)
12
12
  return
13
13
  }
14
14
 
15
15
  if (!profileExists(name)) {
16
- console.error(chalk.red(`✖ Account "${name}" not found.`))
16
+ console.error(msg.accountNotFound(name))
17
17
  process.exit(1)
18
18
  return
19
19
  }
20
20
 
21
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.'))
22
+ console.error(msg.cannotDiscardActive())
23
+ console.log(msg.suggestSwitchFirst())
24
24
  process.exit(1)
25
25
  return
26
26
  }
27
27
 
28
28
  if (options.confirm === false) {
29
- console.log(chalk.dim('Cancelled.'))
29
+ console.log(msg.cancelled())
30
30
  return
31
31
  }
32
32
 
@@ -34,15 +34,15 @@ export async function deleteAccount(name, options = {}) {
34
34
  const { confirm } = await inquirer.prompt([{
35
35
  type: 'confirm',
36
36
  name: 'confirm',
37
- message: `Delete cloak "${name}"?`,
37
+ message: msg.prompts.deleteConfirm(name),
38
38
  default: false,
39
39
  }])
40
40
  if (!confirm) {
41
- console.log(chalk.dim('Cancelled.'))
41
+ console.log(msg.cancelled())
42
42
  return
43
43
  }
44
44
  }
45
45
 
46
46
  rmSync(profileDir(name), { recursive: true, force: true })
47
- console.log(chalk.green(`✔ Cloak "${name}" discarded.`))
47
+ console.log(msg.cloakDiscarded(name))
48
48
  }
@@ -1,5 +1,5 @@
1
- import chalk from 'chalk'
2
1
  import { listProfileNames, getActiveProfile } from '../lib/paths.js'
2
+ import * as msg from '../lib/messages.js'
3
3
 
4
4
  export function listAccounts() {
5
5
  const names = listProfileNames().sort()
@@ -11,17 +11,14 @@ export function listAccounts() {
11
11
  }))
12
12
 
13
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>'))
14
+ console.log(msg.noCloaksYet())
15
+ console.log(msg.suggestCreate())
16
16
  return accounts
17
17
  }
18
18
 
19
- console.log(chalk.bold('\nClaude Code Accounts\n'))
19
+ console.log(msg.accountListHeader())
20
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}`)
21
+ console.log(msg.accountListItem(name, isActive))
25
22
  })
26
23
  console.log()
27
24
 
@@ -1,31 +1,31 @@
1
1
  import { renameSync } from 'fs'
2
- import chalk from 'chalk'
3
2
  import { profileDir, profileExists, getActiveProfile } from '../lib/paths.js'
4
3
  import { validateAccountName } from '../lib/validate.js'
4
+ import * as msg from '../lib/messages.js'
5
5
 
6
6
  export async function renameAccount(oldName, newName) {
7
7
  const oldValidation = validateAccountName(oldName)
8
8
  if (!oldValidation.valid) {
9
- console.error(chalk.red(`✖ ${oldValidation.error}`))
9
+ console.error(msg.validationError(oldValidation.error))
10
10
  process.exit(1)
11
11
  return
12
12
  }
13
13
 
14
14
  const newValidation = validateAccountName(newName)
15
15
  if (!newValidation.valid) {
16
- console.error(chalk.red(`✖ ${newValidation.error}`))
16
+ console.error(msg.validationError(newValidation.error))
17
17
  process.exit(1)
18
18
  return
19
19
  }
20
20
 
21
21
  if (!profileExists(oldName)) {
22
- console.error(chalk.red(`✖ Account "${oldName}" not found.`))
22
+ console.error(msg.accountNotFound(oldName))
23
23
  process.exit(1)
24
24
  return
25
25
  }
26
26
 
27
27
  if (profileExists(newName)) {
28
- console.error(chalk.red(`✖ Account "${newName}" is already in use.`))
28
+ console.error(msg.accountAlreadyInUse(newName))
29
29
  process.exit(1)
30
30
  return
31
31
  }
@@ -33,8 +33,8 @@ export async function renameAccount(oldName, newName) {
33
33
  renameSync(profileDir(oldName), profileDir(newName))
34
34
 
35
35
  if (getActiveProfile() === oldName) {
36
- console.log(chalk.yellow(`⚠ Run \`claude account switch ${newName}\` to update your session.`))
36
+ console.log(msg.updateSessionAfterRename(newName))
37
37
  }
38
38
 
39
- console.log(chalk.green(`✔ Cloak "${oldName}" renamed to "${newName}".`))
39
+ console.log(msg.cloakRenamed(oldName, newName))
40
40
  }
@@ -1,66 +1,66 @@
1
- import chalk from 'chalk'
2
1
  import inquirer from 'inquirer'
3
2
  import { profileDir, profileExists, getActiveProfile } from '../lib/paths.js'
4
3
  import { validateAccountName } from '../lib/validate.js'
5
4
  import { getRcFilePath, isAlreadyInstalled, installToRcFile } from '../lib/setup.js'
5
+ import * as msg from '../lib/messages.js'
6
6
 
7
7
  export async function switchAccount(name, options = {}) {
8
8
  const validation = validateAccountName(name)
9
9
  if (!validation.valid) {
10
- console.error(chalk.red(`✖ ${validation.error}`))
10
+ console.error(msg.validationError(validation.error))
11
11
  process.exit(1)
12
12
  return
13
13
  }
14
14
 
15
15
  if (!profileExists(name)) {
16
- console.error(chalk.red(`✖ Account "${name}" not found.`))
17
- console.log(chalk.dim(` Run: claude account create ${name}`))
16
+ console.error(msg.accountNotFound(name))
17
+ console.log(msg.suggestCreate(name))
18
18
  process.exit(1)
19
19
  return
20
20
  }
21
21
 
22
22
  const active = getActiveProfile()
23
23
  if (active === name) {
24
- console.log(chalk.yellow(`⚡ Already wearing cloak "${name}".`))
24
+ console.log(msg.alreadyWearing(name))
25
25
  return
26
26
  }
27
27
 
28
28
  const dir = profileDir(name)
29
29
 
30
- // Shell integration is active — output for eval
31
30
  if (options.printEnv) {
32
- process.stdout.write(`export CLAUDE_CONFIG_DIR=${dir}\n`)
33
- process.stdout.write(`echo "${chalk.green(`✔ Now wearing cloak "${name}".`)}"\n`)
31
+ process.stdout.write(msg.printEnvExport(dir))
32
+ process.stdout.write(msg.printEnvEcho(name))
34
33
  return
35
34
  }
36
35
 
37
36
  // No shell integration — prompt user to set it up
38
- console.log(chalk.yellow('\n Shell integration is required to switch accounts.\n'))
37
+ console.log('\n' + msg.switchRequired() + '\n')
39
38
 
40
39
  let choice = options.setupChoice
41
40
  if (choice === undefined) {
42
41
  const answer = await inquirer.prompt([{
43
42
  type: 'list',
44
43
  name: 'choice',
45
- message: 'How would you like to proceed?',
44
+ message: msg.prompts.setupChoice,
46
45
  choices: [
47
- { name: 'Set it up now (recommended)', value: 'auto' },
48
- { name: 'Show manual instructions', value: 'manual' },
46
+ { name: msg.prompts.setupAuto, value: 'auto' },
47
+ { name: msg.prompts.setupManual, value: 'manual' },
49
48
  ],
50
49
  }])
51
50
  choice = answer.choice
52
51
  }
53
52
 
53
+ const rcFile = getRcFilePath()
54
+
54
55
  if (choice === 'auto') {
55
- const rcFile = getRcFilePath()
56
56
  if (!isAlreadyInstalled(rcFile)) {
57
57
  installToRcFile(rcFile)
58
- console.log(chalk.green(`✔ Shell integration added to ${rcFile}`))
58
+ console.log(msg.shellIntegrationAdded(rcFile))
59
59
  } else {
60
- console.log(chalk.dim(` Already installed in ${rcFile}`))
60
+ console.log(msg.alreadyInstalled(rcFile))
61
61
  }
62
- console.log(chalk.dim(`\n Run: `) + chalk.white(`source ${rcFile} && cloak switch ${name}\n`))
62
+ console.log(msg.setupRunCommand(rcFile, name))
63
63
  } else {
64
- console.log(chalk.dim(`\n Run: `) + chalk.white(`echo 'eval "$(cloak init)"' >> ${getRcFilePath()} && source ${getRcFilePath()} && cloak switch ${name}\n`))
64
+ console.log(msg.setupManualCommand(rcFile, name))
65
65
  }
66
66
  }
@@ -1,10 +1,10 @@
1
- import chalk from 'chalk'
2
1
  import { getActiveProfile } from '../lib/paths.js'
2
+ import * as msg from '../lib/messages.js'
3
3
 
4
4
  export function whoami() {
5
5
  const active = getActiveProfile()
6
6
  if (!active) {
7
- console.log(chalk.dim('No cloak. Using default Claude Code config.'))
7
+ console.log(msg.noCloak())
8
8
  return null
9
9
  }
10
10
  console.log(active)
@@ -0,0 +1,148 @@
1
+ import chalk from 'chalk'
2
+
3
+ // Icons — consistent across all messages
4
+ const icon = {
5
+ success: chalk.green('✔'),
6
+ error: chalk.red('✖'),
7
+ warning: chalk.yellow('⚠'),
8
+ tip: '💡',
9
+ active: chalk.green('●'),
10
+ inactive: chalk.dim('○'),
11
+ }
12
+
13
+ // --- Success messages ---
14
+
15
+ export function cloakCreated(name) {
16
+ return `${icon.success} Cloak ${chalk.bold(`"${name}"`)} created. Ready to wear!`
17
+ }
18
+
19
+ export function cloakSwitched(name) {
20
+ return `${icon.success} Now wearing cloak ${chalk.bold(`"${name}"`)}.`
21
+ }
22
+
23
+ export function cloakDiscarded(name) {
24
+ return `${icon.success} Cloak ${chalk.bold(`"${name}"`)} discarded.`
25
+ }
26
+
27
+ export function cloakRenamed(oldName, newName) {
28
+ return `${icon.success} Cloak ${chalk.bold(`"${oldName}"`)} is now ${chalk.bold(`"${newName}"`)}.`
29
+ }
30
+
31
+ export function shellIntegrationAdded(rcFile) {
32
+ return `${icon.success} All set! Shell integration added to ${chalk.bold(rcFile)}.`
33
+ }
34
+
35
+ // --- Error messages ---
36
+
37
+ export function validationError(error) {
38
+ return `${icon.error} ${error}`
39
+ }
40
+
41
+ export function accountNotFound(name) {
42
+ return `${icon.error} Couldn't find a cloak named ${chalk.bold(`"${name}"`)}.`
43
+ }
44
+
45
+ export function noActiveSession() {
46
+ return `${icon.error} No active Claude Code session found.`
47
+ }
48
+
49
+ export function cannotDiscardActive() {
50
+ return `${icon.error} You're currently wearing this cloak.`
51
+ }
52
+
53
+ export function accountAlreadyInUse(name) {
54
+ return `${icon.error} A cloak named ${chalk.bold(`"${name}"`)} already exists.`
55
+ }
56
+
57
+ // --- Warning messages ---
58
+
59
+ export function alreadyWearing(name) {
60
+ return `${icon.warning} You're already wearing cloak ${chalk.bold(`"${name}"`)}.`
61
+ }
62
+
63
+ export function switchRequired() {
64
+ return `${icon.warning} Quick setup needed to enable switching.`
65
+ }
66
+
67
+ export function updateSessionAfterRename(newName) {
68
+ return `${icon.warning} To keep using this cloak, run: ${chalk.white(`claude account switch ${newName}`)}`
69
+ }
70
+
71
+ // --- Info / hints ---
72
+
73
+ export function suggestCreate(name) {
74
+ return chalk.dim(` Try: claude account create ${name || '<name>'}`)
75
+ }
76
+
77
+ export function suggestSwitchFirst() {
78
+ return chalk.dim(' Switch to a different cloak first, then try again.')
79
+ }
80
+
81
+ export function loginFirst() {
82
+ return chalk.dim(' Open Claude Code and log in first.')
83
+ }
84
+
85
+ export function cancelled() {
86
+ return chalk.dim('No changes made.')
87
+ }
88
+
89
+ export function noCloak() {
90
+ return chalk.dim('No cloak active — using default Claude Code config.')
91
+ }
92
+
93
+ export function noCloaksYet() {
94
+ return chalk.dim('No cloaks in your wardrobe yet.')
95
+ }
96
+
97
+ export function accountListHeader() {
98
+ return chalk.bold('\nYour Cloaks\n')
99
+ }
100
+
101
+ export function accountListItem(name, isActive) {
102
+ const marker = isActive ? icon.active : icon.inactive
103
+ const label = isActive ? chalk.green.bold(name) : chalk.white(name)
104
+ const tag = isActive ? chalk.green(' (active)') : ''
105
+ return ` ${marker} ${label}${tag}`
106
+ }
107
+
108
+ export function alreadyInstalled(rcFile) {
109
+ return chalk.dim(` Already set up in ${rcFile} — you're good!`)
110
+ }
111
+
112
+ // --- Setup instructions ---
113
+
114
+ export function setupRunCommand(rcFile, name) {
115
+ return chalk.dim('\n Almost there! Run: ') + chalk.white(`source ${rcFile} && cloak switch ${name}\n`)
116
+ }
117
+
118
+ export function setupManualCommand(rcFile, name) {
119
+ return chalk.dim('\n Run: ') + chalk.white(`echo 'eval "$(cloak init)"' >> ${rcFile} && source ${rcFile} && cloak switch ${name}\n`)
120
+ }
121
+
122
+ // --- Tip ---
123
+
124
+ export function shellIntegrationTip() {
125
+ return chalk.dim(`\n${icon.tip} Tip: Enable "claude -a" and "claude account" with:\n`) +
126
+ chalk.dim(' echo \'eval "$(cloak init)"\' >> ~/.bashrc && source ~/.bashrc\n\n')
127
+ }
128
+
129
+ // --- Print-env (stdout, no chalk — evaluated by shell) ---
130
+
131
+ export function printEnvExport(dir) {
132
+ return `export CLAUDE_CONFIG_DIR=${dir}\n`
133
+ }
134
+
135
+ export function printEnvEcho(name) {
136
+ return `echo "${cloakSwitched(name)}"\n`
137
+ }
138
+
139
+ // --- Prompt messages ---
140
+
141
+ export const prompts = {
142
+ accountName: 'Name your cloak:',
143
+ overwriteConfirm: (name) => `Cloak "${name}" already exists. Replace it?`,
144
+ deleteConfirm: (name) => `Remove cloak "${name}"? This can't be undone.`,
145
+ setupChoice: 'How would you like to proceed?',
146
+ setupAuto: 'Set it up now (recommended)',
147
+ setupManual: 'Show me the manual steps',
148
+ }
package/src/lib/tip.js CHANGED
@@ -1,14 +1,11 @@
1
- import chalk from 'chalk'
1
+ import { shellIntegrationTip } from './messages.js'
2
2
 
3
3
  export function showTipIfNeeded() {
4
4
  if (process.env.CLOAK_SHELL_INTEGRATION === '1') return
5
5
  if (process.env.CLOAK_TIP_SHOWN === '1') return
6
6
  if (!process.stdout.isTTY) return
7
7
 
8
- process.stderr.write(
9
- chalk.dim('\n💡 Tip: Run this once to enable "claude -a" and "claude account":\n') +
10
- chalk.dim(' echo \'eval "$(cloak init)"\' >> ~/.bashrc && source ~/.bashrc\n\n')
11
- )
8
+ process.stderr.write(shellIntegrationTip())
12
9
 
13
10
  process.env.CLOAK_TIP_SHOWN = '1'
14
11
  }