claude-switch-profile 1.0.2 → 1.4.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.
@@ -1,8 +1,8 @@
1
- import { mkdirSync, cpSync, existsSync, writeFileSync, readdirSync } from 'node:fs';
1
+ import { mkdirSync, cpSync, existsSync, statSync, writeFileSync, copyFileSync, readdirSync, lstatSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { addProfile, getActive, setActive, profileExists, getProfileDir } from '../profile-store.js';
4
- import { saveFiles } from '../file-operations.js';
5
- import { SYMLINK_ITEMS, SYMLINK_DIRS, SOURCE_FILE } from '../constants.js';
4
+ import { saveFiles, updateSettingsPaths } from '../file-operations.js';
5
+ import { CLAUDE_DIR, MANAGED_ITEMS, MANAGED_DIRS, SOURCE_FILE, NEVER_CLONE } from '../constants.js';
6
6
  import { success, error, info, warn } from '../output-helpers.js';
7
7
 
8
8
  export const createCommand = (name, options) => {
@@ -32,44 +32,102 @@ export const createCommand = (name, options) => {
32
32
 
33
33
  mkdirSync(profileDir, { recursive: true });
34
34
 
35
- // Build source.json from the kit directory
35
+ // Copy items from source kit into profile dir
36
36
  const sourceMap = {};
37
- for (const item of SYMLINK_ITEMS) {
37
+ for (const item of MANAGED_ITEMS) {
38
38
  const target = join(sourcePath, item);
39
39
  if (existsSync(target)) {
40
- sourceMap[item] = target;
40
+ const dest = join(profileDir, item);
41
+ try {
42
+ if (statSync(target).isDirectory()) {
43
+ cpSync(target, dest, { recursive: true });
44
+ } else {
45
+ copyFileSync(target, dest);
46
+ }
47
+ sourceMap[item] = dest;
48
+ } catch { /* skip */ }
41
49
  }
42
50
  }
43
51
 
44
52
  if (Object.keys(sourceMap).length === 0) {
45
- warn(`No recognized items found in "${sourcePath}". Expected: ${SYMLINK_ITEMS.join(', ')}`);
53
+ warn(`No recognized items found in "${sourcePath}". Expected: ${MANAGED_ITEMS.join(', ')}`);
54
+ }
55
+
56
+ // Inherit missing items from current ~/.claude state
57
+ for (const item of MANAGED_ITEMS) {
58
+ if (sourceMap[item]) continue;
59
+ const src = join(CLAUDE_DIR, item);
60
+ if (!existsSync(src)) continue;
61
+ try {
62
+ const dest = join(profileDir, item);
63
+ if (statSync(src).isDirectory()) {
64
+ cpSync(src, dest, { recursive: true });
65
+ } else {
66
+ copyFileSync(src, dest);
67
+ }
68
+ sourceMap[item] = dest;
69
+ } catch { /* skip */ }
46
70
  }
47
71
 
48
72
  writeFileSync(join(profileDir, SOURCE_FILE), JSON.stringify(sourceMap, null, 2) + '\n');
49
- info(`Linked to kit at ${sourcePath}`);
73
+ info(`Copied from kit at ${sourcePath}`);
50
74
  info(`Items found: ${Object.keys(sourceMap).join(', ') || 'none'}`);
51
75
 
52
76
  // Also copy current mutable files
53
77
  saveFiles(profileDir);
54
78
  } else {
55
- // Create brand new independent profile — everything fresh, nothing copied
79
+ // Create new profile — full clone of ~/.claude/ (blacklist approach)
56
80
  mkdirSync(profileDir, { recursive: true });
57
81
 
58
- // Create self-contained directories for each symlink directory item
59
82
  const sourceMap = {};
60
- for (const item of SYMLINK_DIRS) {
61
- const itemDir = join(profileDir, item);
62
- mkdirSync(itemDir, { recursive: true });
63
- sourceMap[item] = itemDir;
83
+ let entries;
84
+ try {
85
+ entries = readdirSync(CLAUDE_DIR);
86
+ } catch {
87
+ entries = [];
88
+ }
89
+
90
+ for (const entry of entries) {
91
+ if (NEVER_CLONE.includes(entry)) continue;
92
+
93
+ const src = join(CLAUDE_DIR, entry);
94
+ try {
95
+ const stat = lstatSync(src);
96
+
97
+ if (stat.isDirectory()) {
98
+ const dest = join(profileDir, entry);
99
+ cpSync(src, dest, { recursive: true });
100
+ if (MANAGED_ITEMS.includes(entry)) {
101
+ sourceMap[entry] = dest;
102
+ }
103
+ } else if (stat.isFile()) {
104
+ const dest = join(profileDir, entry);
105
+ copyFileSync(src, dest);
106
+ if (MANAGED_ITEMS.includes(entry)) {
107
+ sourceMap[entry] = dest;
108
+ }
109
+ }
110
+ } catch { /* skip unreadable items */ }
111
+ }
112
+
113
+ // Ensure empty dirs for any missing MANAGED_DIRS
114
+ for (const item of MANAGED_DIRS) {
115
+ if (!sourceMap[item]) {
116
+ const itemDir = join(profileDir, item);
117
+ mkdirSync(itemDir, { recursive: true });
118
+ sourceMap[item] = itemDir;
119
+ }
64
120
  }
65
121
 
66
122
  writeFileSync(join(profileDir, SOURCE_FILE), JSON.stringify(sourceMap, null, 2) + '\n');
67
- // Do NOT call saveFiles() — new profile should start clean
68
- // without inheriting settings.json, .env, .ck*, etc. from current state
69
- info('Created new independent profile (clean state)');
123
+
124
+ // Update absolute paths in settings.json
125
+ updateSettingsPaths(profileDir, 'save');
126
+
127
+ info('Created new profile (full clone of current state)');
70
128
  }
71
129
 
72
- addProfile(name, { description: options.description || '' });
130
+ addProfile(name, { description: options.description || '', mode: 'account-session' });
73
131
 
74
132
  // Set as active if first profile
75
133
  if (!getActive()) {
@@ -1,5 +1,4 @@
1
- import { getActive } from '../profile-store.js';
2
- import { getProfileDir } from '../profile-store.js';
1
+ import { getActive, getProfileDir, getProfileMeta } from '../profile-store.js';
3
2
  import { success, info, warn } from '../output-helpers.js';
4
3
 
5
4
  export const currentCommand = () => {
@@ -8,6 +7,12 @@ export const currentCommand = () => {
8
7
  warn('No active profile. Run "csp create <name>" to create one.');
9
8
  return;
10
9
  }
11
- success(`Active profile: ${active}`);
10
+ success(`Active legacy profile: ${active}`);
12
11
  info(`Location: ${getProfileDir(active)}`);
12
+
13
+ const meta = getProfileMeta(active);
14
+ if (meta?.lastLaunchAt) {
15
+ info(`Last isolated launch: ${meta.lastLaunchAt}`);
16
+ if (meta.runtimeDir) info(`Isolated runtime: ${meta.runtimeDir}`);
17
+ }
13
18
  };
@@ -0,0 +1,22 @@
1
+ import { getActive, setActive } from '../profile-store.js';
2
+ import { success, info } from '../output-helpers.js';
3
+ import { DEFAULT_PROFILE } from '../constants.js';
4
+
5
+ export const deactivateCommand = async () => {
6
+ const active = getActive();
7
+ if (!active) {
8
+ info('No active profile to deactivate.');
9
+ return;
10
+ }
11
+
12
+ if (active === DEFAULT_PROFILE) {
13
+ info('Default profile uses ~/.claude directly. Nothing to deactivate.');
14
+ return;
15
+ }
16
+
17
+ // Just reset to default — never touch ~/.claude
18
+ setActive(DEFAULT_PROFILE);
19
+
20
+ success(`Profile "${active}" deactivated. Reset to default.`);
21
+ info('~/.claude was not modified.');
22
+ };
@@ -1,7 +1,8 @@
1
1
  import { rmSync } from 'node:fs';
2
2
  import { createInterface } from 'node:readline';
3
- import { getActive, removeProfile, profileExists, getProfileDir } from '../profile-store.js';
4
- import { success, error, warn } from '../output-helpers.js';
3
+ import { getActive, clearActive, removeProfile, profileExists, getProfileDir } from '../profile-store.js';
4
+ import { success, error, warn, info } from '../output-helpers.js';
5
+ import { DEFAULT_PROFILE } from '../constants.js';
5
6
 
6
7
  const confirm = (question) => {
7
8
  return new Promise((resolve) => {
@@ -14,14 +15,13 @@ const confirm = (question) => {
14
15
  };
15
16
 
16
17
  export const deleteCommand = async (name, options) => {
17
- if (!profileExists(name)) {
18
- error(`Profile "${name}" does not exist.`);
18
+ if (name === DEFAULT_PROFILE) {
19
+ error('Cannot delete the default profile.');
19
20
  process.exit(1);
20
21
  }
21
22
 
22
- const active = getActive();
23
- if (active === name) {
24
- error(`Cannot delete active profile "${name}". Switch to another profile first.`);
23
+ if (!profileExists(name)) {
24
+ error(`Profile "${name}" does not exist.`);
25
25
  process.exit(1);
26
26
  }
27
27
 
@@ -33,8 +33,20 @@ export const deleteCommand = async (name, options) => {
33
33
  }
34
34
  }
35
35
 
36
+ const active = getActive();
36
37
  const profileDir = getProfileDir(name);
38
+
39
+ // Delete profile directory
37
40
  rmSync(profileDir, { recursive: true, force: true });
38
41
  removeProfile(name);
42
+
43
+ // If deleting the active profile, just clear the marker
44
+ // NEVER touch ~/.claude — leave symlinks/files as-is
45
+ if (active === name) {
46
+ clearActive();
47
+ info('Was active profile — active marker cleared.');
48
+ info('Symlinks in ~/.claude may now be dangling. Run "csp use <profile>" to switch.');
49
+ }
50
+
39
51
  success(`Profile "${name}" deleted.`);
40
52
  };
@@ -1,8 +1,8 @@
1
1
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import chalk from 'chalk';
4
- import { profileExists, getProfileDir, getActive } from '../profile-store.js';
5
- import { SOURCE_FILE } from '../constants.js';
4
+ import { profileExists, getActive, getEffectiveDir } from '../profile-store.js';
5
+ import { SOURCE_FILE, ALL_MANAGED } from '../constants.js';
6
6
  import { error, info } from '../output-helpers.js';
7
7
 
8
8
  export const diffCommand = (profileA, profileB) => {
@@ -31,19 +31,29 @@ export const diffCommand = (profileA, profileB) => {
31
31
  process.exit(1);
32
32
  }
33
33
 
34
- const dirA = getProfileDir(nameA);
35
- const dirB = getProfileDir(nameB);
34
+ const dirA = getEffectiveDir(nameA);
35
+ const dirB = getEffectiveDir(nameB);
36
36
 
37
37
  console.log(`\n${chalk.bold('Comparing:')} ${chalk.cyan(nameA)} ↔ ${chalk.cyan(nameB)}\n`);
38
38
 
39
- // Compare source.json (symlink targets)
39
+ // Compare source.json (managed item sources)
40
40
  const sourceA = readJsonSafe(join(dirA, SOURCE_FILE));
41
41
  const sourceB = readJsonSafe(join(dirB, SOURCE_FILE));
42
- diffObject('Symlink targets (source.json)', sourceA, sourceB, nameA, nameB);
42
+ diffObject('Managed item sources (source.json)', sourceA, sourceB, nameA, nameB);
43
43
 
44
44
  // Compare files that exist in either profile
45
- const filesA = new Set(readdirSync(dirA).filter((f) => f !== SOURCE_FILE));
46
- const filesB = new Set(readdirSync(dirB).filter((f) => f !== SOURCE_FILE));
45
+ // When reading from CLAUDE_DIR (active/default profile), filter to only managed items
46
+ const active = getActive();
47
+ const managedSet = new Set(ALL_MANAGED);
48
+ const listManagedFiles = (dir) => {
49
+ const all = readdirSync(dir).filter((f) => f !== SOURCE_FILE);
50
+ if ((dir === dirA && active === nameA) || (dir === dirB && active === nameB)) {
51
+ return new Set(all.filter((f) => managedSet.has(f)));
52
+ }
53
+ return new Set(all);
54
+ };
55
+ const filesA = listManagedFiles(dirA, nameA);
56
+ const filesB = listManagedFiles(dirB, nameB);
47
57
 
48
58
  const allFiles = new Set([...filesA, ...filesB]);
49
59
  const diffs = [];
@@ -73,7 +83,7 @@ export const diffCommand = (profileA, profileB) => {
73
83
  }
74
84
 
75
85
  if (diffs.length === 0) {
76
- info('Profiles are identical (excluding symlink targets).');
86
+ info('Profiles are identical (excluding source paths).');
77
87
  } else {
78
88
  console.log(chalk.bold('File differences:'));
79
89
  for (const d of diffs) {
@@ -1,10 +1,18 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
2
  import { resolve } from 'node:path';
4
- import { profileExists, getProfileDir } from '../profile-store.js';
5
- import { success, error } from '../output-helpers.js';
3
+ import { profileExists, getProfileDir, getActive } from '../profile-store.js';
4
+ import { isWindows } from '../platform.js';
5
+ import { saveItems } from '../item-manager.js';
6
+ import { saveFiles, updateSettingsPaths } from '../file-operations.js';
7
+ import { success, error, info } from '../output-helpers.js';
8
+ import { DEFAULT_PROFILE } from '../constants.js';
6
9
 
7
10
  export const exportCommand = (name, options) => {
11
+ if (name === DEFAULT_PROFILE) {
12
+ error('Cannot export the default profile (it uses ~/.claude directly).');
13
+ process.exit(1);
14
+ }
15
+
8
16
  if (!profileExists(name)) {
9
17
  error(`Profile "${name}" does not exist.`);
10
18
  process.exit(1);
@@ -14,11 +22,28 @@ export const exportCommand = (name, options) => {
14
22
  const output = options.output || `./${name}.csp.tar.gz`;
15
23
  const outputPath = resolve(output);
16
24
 
25
+ // If exporting active profile, save a copy first (non-destructive)
26
+ const active = getActive();
27
+ if (active === name) {
28
+ saveItems(profileDir);
29
+ saveFiles(profileDir);
30
+ updateSettingsPaths(profileDir, 'save');
31
+ info('Saved current state before export.');
32
+ }
33
+
34
+ const tarArgs = isWindows
35
+ ? ['--force-local', '-czf', outputPath, '-C', profileDir.replace(/\\/g, '/'), '.']
36
+ : ['-czf', outputPath, '-C', profileDir, '.'];
37
+
17
38
  try {
18
- execFileSync('tar', ['-czf', outputPath, '-C', profileDir, '.'], { stdio: 'pipe' });
39
+ execFileSync(isWindows ? 'tar.exe' : 'tar', tarArgs, { stdio: 'pipe' });
19
40
  success(`Profile "${name}" exported to ${outputPath}`);
20
41
  } catch (err) {
21
- error(`Failed to export: ${err.message}`);
42
+ if (err.code === 'ENOENT') {
43
+ error('tar command not found. On Windows, tar is available on Windows 10+.');
44
+ } else {
45
+ error(`Failed to export: ${err.message}`);
46
+ }
22
47
  process.exit(1);
23
48
  }
24
49
  };
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
2
2
  import { existsSync, mkdirSync } from 'node:fs';
3
3
  import { resolve, basename } from 'node:path';
4
4
  import { addProfile, profileExists, getProfileDir, ensureProfilesDir } from '../profile-store.js';
5
+ import { isWindows } from '../platform.js';
5
6
  import { validateProfile } from '../profile-validator.js';
6
7
  import { success, error, warn } from '../output-helpers.js';
7
8
 
@@ -24,10 +25,18 @@ export const importCommand = (file, options) => {
24
25
  const profileDir = getProfileDir(name);
25
26
  mkdirSync(profileDir, { recursive: true });
26
27
 
28
+ const tarArgs = isWindows
29
+ ? ['--force-local', '-xzf', filePath, '-C', profileDir.replace(/\\/g, '/')]
30
+ : ['-xzf', filePath, '-C', profileDir];
31
+
27
32
  try {
28
- execFileSync('tar', ['-xzf', filePath, '-C', profileDir], { stdio: 'pipe' });
33
+ execFileSync(isWindows ? 'tar.exe' : 'tar', tarArgs, { stdio: 'pipe' });
29
34
  } catch (err) {
30
- error(`Failed to extract: ${err.message}`);
35
+ if (err.code === 'ENOENT') {
36
+ error('tar command not found. On Windows, tar is available on Windows 10+.');
37
+ } else {
38
+ error(`Failed to extract: ${err.message}`);
39
+ }
31
40
  process.exit(1);
32
41
  }
33
42
 
@@ -1,12 +1,11 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { ensureProfilesDir, getActive, profileExists } from '../profile-store.js';
3
- import { createCommand } from './create.js';
4
- import { success, info, warn } from '../output-helpers.js';
2
+ import { ensureProfilesDir, getActive, addProfile, setActive } from '../profile-store.js';
3
+ import { success, info } from '../output-helpers.js';
5
4
  import { PROFILES_DIR } from '../constants.js';
6
5
 
7
6
  export const initCommand = () => {
8
- if (existsSync(PROFILES_DIR) && getActive()) {
9
- const active = getActive();
7
+ const active = getActive();
8
+ if (existsSync(PROFILES_DIR) && active) {
10
9
  info(`Already initialized. Active profile: "${active}"`);
11
10
  info(`Profiles directory: ${PROFILES_DIR}`);
12
11
  return;
@@ -15,7 +14,10 @@ export const initCommand = () => {
15
14
  ensureProfilesDir();
16
15
  info('Initializing Claude Switch Profile...');
17
16
 
18
- // Create default profile from current state
19
- createCommand('default', { description: 'Default profile (initial capture)' });
20
- success('Initialization complete. Your current setup is saved as "default".');
17
+ addProfile('default', { description: 'Vanilla Claude defaults', mode: 'legacy' });
18
+ setActive('default');
19
+
20
+ success('Initialization complete.');
21
+ info('"default" profile uses ~/.claude directly — no copy/restore needed.');
22
+ info('Run "csp create <name>" to capture your current setup into a new profile.');
21
23
  };
@@ -0,0 +1,216 @@
1
+ import { execFileSync, spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { profileExists } from '../profile-store.js';
5
+ import { useCommand } from './use.js';
6
+ import { ensureRuntimeInstance } from '../runtime-instance-manager.js';
7
+ import { withRuntimeLock } from '../safety.js';
8
+ import { error, info, warn } from '../output-helpers.js';
9
+ import { isWindows } from '../platform.js';
10
+ import { LAUNCH_CONFIG_ENV, DEFAULT_PROFILE } from '../constants.js';
11
+ import { buildEffectiveLaunchEnv, parseDotEnvLaunchEnv, parseSettingsLaunchEnv, sanitizeInheritedLaunchEnv } from '../launch-effective-env-resolver.js';
12
+
13
+ const isTruthyDebugValue = (value) => {
14
+ if (value === undefined || value === null) return false;
15
+ const normalized = String(value).trim().toLowerCase();
16
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
17
+ };
18
+
19
+ const findWindowsClaudePath = (env, dependencies = {}) => {
20
+ const execRunner = dependencies.execFileSync || execFileSync;
21
+ const pathExists = dependencies.existsSync || existsSync;
22
+
23
+ const pathMatches = (() => {
24
+ try {
25
+ return execRunner('where.exe', ['claude'], {
26
+ encoding: 'utf-8',
27
+ stdio: ['ignore', 'pipe', 'ignore'],
28
+ env,
29
+ })
30
+ .trim()
31
+ .split(/\r?\n/)
32
+ .map((entry) => entry.trim())
33
+ .filter(Boolean);
34
+ } catch {
35
+ return [];
36
+ }
37
+ })();
38
+
39
+ const resolvedFromPath = pathMatches.find((entry) => /\.(exe|cmd|bat)$/i.test(entry)) || pathMatches[0];
40
+ if (resolvedFromPath && pathExists(resolvedFromPath)) {
41
+ return resolvedFromPath;
42
+ }
43
+
44
+ const fallbackCandidates = [
45
+ env.USERPROFILE ? join(env.USERPROFILE, '.local', 'bin', 'claude.exe') : null,
46
+ env.APPDATA ? join(env.APPDATA, 'npm', 'claude.cmd') : null,
47
+ env.USERPROFILE ? join(env.USERPROFILE, '.bun', 'bin', 'claude.exe') : null,
48
+ ].filter(Boolean);
49
+
50
+ return fallbackCandidates.find((candidate) => pathExists(candidate)) || null;
51
+ };
52
+
53
+ export const resolveClaudeLaunchTarget = (env = process.env, dependencies = {}) => {
54
+ const windows = dependencies.isWindows ?? isWindows;
55
+ if (!windows) {
56
+ return { command: 'claude', shell: false };
57
+ }
58
+
59
+ const resolvedPath = findWindowsClaudePath(env, dependencies);
60
+ if (!resolvedPath) {
61
+ return { command: 'claude', shell: true };
62
+ }
63
+
64
+ const requiresShell = /\.(cmd|bat)$/i.test(resolvedPath);
65
+
66
+ return {
67
+ command: requiresShell ? `"${resolvedPath}"` : resolvedPath,
68
+ shell: requiresShell,
69
+ };
70
+ };
71
+
72
+ const readProfileSettingsLaunchEnv = (profileDir) => {
73
+ const settingsPath = join(profileDir, 'settings.json');
74
+ if (!existsSync(settingsPath)) return {};
75
+
76
+ try {
77
+ const rawSettings = readFileSync(settingsPath, 'utf-8');
78
+ return parseSettingsLaunchEnv(rawSettings);
79
+ } catch {
80
+ warn(`Could not parse launch env from ${settingsPath}; falling back to inherited env.`);
81
+ return {};
82
+ }
83
+ };
84
+
85
+ const readProfileDotEnvLaunchEnv = (profileDir) => {
86
+ const dotEnvPath = join(profileDir, '.env');
87
+ if (!existsSync(dotEnvPath)) return {};
88
+
89
+ try {
90
+ const rawDotEnv = readFileSync(dotEnvPath, 'utf-8');
91
+ return parseDotEnvLaunchEnv(rawDotEnv);
92
+ } catch {
93
+ warn(`Could not parse launch env from ${dotEnvPath}; falling back to inherited env.`);
94
+ return {};
95
+ }
96
+ };
97
+
98
+ const readProfileLaunchEnvSources = (runtimeDir) => {
99
+ return {
100
+ profileSettingsEnv: readProfileSettingsLaunchEnv(runtimeDir),
101
+ profileDotEnvEnv: readProfileDotEnvLaunchEnv(runtimeDir),
102
+ };
103
+ };
104
+
105
+ const formatLaunchEnvDiagnostics = (diagnostics = {}) => {
106
+ const keys = diagnostics.anthropicKeys || [];
107
+ const sources = diagnostics.anthropicKeySources || {};
108
+
109
+ if (!keys.length) {
110
+ return 'ANTHROPIC_* keys: none';
111
+ }
112
+
113
+ const keyDetails = keys
114
+ .map((key) => `${key}<=${sources[key] || 'unknown'}`)
115
+ .join(', ');
116
+
117
+ return `ANTHROPIC_* keys: ${keyDetails}`;
118
+ };
119
+
120
+ export const stripInheritedLaunchEnv = (env = process.env) => {
121
+ const sanitized = sanitizeInheritedLaunchEnv(env);
122
+ for (const key of Object.keys(sanitized)) {
123
+ if (key.toUpperCase().startsWith('ANTHROPIC_')) {
124
+ delete sanitized[key];
125
+ }
126
+ }
127
+ return sanitized;
128
+ };
129
+
130
+ export const launchCommand = async (name, claudeArgs, options = {}) => {
131
+ if (!profileExists(name)) {
132
+ error(`Profile "${name}" does not exist. Run "csp list" to see available profiles.`);
133
+ process.exit(1);
134
+ }
135
+
136
+ let args = [...(claudeArgs || [])];
137
+ const legacyFromArgs = args.includes('--legacy-global');
138
+ if (legacyFromArgs) {
139
+ args = args.filter((a) => a !== '--legacy-global');
140
+ }
141
+
142
+ let launchEnv = { ...process.env };
143
+
144
+ if (options.legacyGlobal || legacyFromArgs) {
145
+ // Legacy path: keep historical behavior for compatibility.
146
+ await useCommand(name, { save: true, skipClaudeCheck: true });
147
+ info(`Launching legacy/global mode: claude ${args.join(' ')}`.trim());
148
+ } else if (name === DEFAULT_PROFILE) {
149
+ // Default profile: launch Claude using ~/.claude natively — no runtime override
150
+ info(`Launching with default profile (using ~/.claude directly): claude ${args.join(' ')}`.trim());
151
+ } else {
152
+ const runtimeDir = await withRuntimeLock(name, async () => ensureRuntimeInstance(name));
153
+ const { profileSettingsEnv, profileDotEnvEnv } = readProfileLaunchEnvSources(runtimeDir);
154
+ const { launchEnv: resolvedLaunchEnv, diagnostics } = buildEffectiveLaunchEnv({
155
+ parentEnv: process.env,
156
+ profileSettingsEnv,
157
+ profileDotEnvEnv,
158
+ });
159
+
160
+ launchEnv = {
161
+ ...resolvedLaunchEnv,
162
+ [LAUNCH_CONFIG_ENV]: runtimeDir,
163
+ };
164
+
165
+ // CLAUDE_CONFIG_DIR already redirects user-level config lookups to the
166
+ // runtime dir, so the "user" settings source reads {runtimeDir}/settings.json
167
+ // — which is the correct profile-specific settings. We do NOT exclude "user"
168
+ // because Claude Code gates skill/hook discovery on it being enabled.
169
+
170
+ // Always show launch diagnostics for debugging credential issues
171
+ info(`Launch env diagnostics (${name}): ${formatLaunchEnvDiagnostics(diagnostics)}`);
172
+ info(`CLAUDE_CONFIG_DIR=${launchEnv[LAUNCH_CONFIG_ENV]}`);
173
+ info(`ANTHROPIC_AUTH_TOKEN=${launchEnv.ANTHROPIC_AUTH_TOKEN ? launchEnv.ANTHROPIC_AUTH_TOKEN.slice(0, 8) + '...' : '(not set)'}`);
174
+ info(`ANTHROPIC_BASE_URL=${launchEnv.ANTHROPIC_BASE_URL || '(not set)'}`);
175
+
176
+ if (isTruthyDebugValue(process.env.CSP_DEBUG_LAUNCH_ENV)) {
177
+ info(`[DEBUG] Full launch env ANTHROPIC keys: ${JSON.stringify(Object.fromEntries(Object.entries(launchEnv).filter(([k]) => k.startsWith('ANTHROPIC_'))))}`);
178
+ }
179
+
180
+ info(`Launching isolated session for profile "${name}": claude ${args.join(' ')}`.trim());
181
+ }
182
+
183
+ const launchTarget = resolveClaudeLaunchTarget(launchEnv);
184
+
185
+ const child = spawn(launchTarget.command, args, {
186
+ stdio: 'inherit',
187
+ shell: launchTarget.shell,
188
+ detached: false,
189
+ env: launchEnv,
190
+ });
191
+
192
+ // Forward signals to child instead of killing parent
193
+ const forwardSignal = (sig) => {
194
+ try {
195
+ child.kill(sig);
196
+ } catch {
197
+ // Child may have already exited
198
+ }
199
+ };
200
+ process.on('SIGINT', forwardSignal);
201
+ process.on('SIGTERM', forwardSignal);
202
+
203
+ child.on('error', (err) => {
204
+ error(`Failed to launch Claude: ${err.message}`);
205
+ process.exit(1);
206
+ });
207
+
208
+ child.on('exit', (code) => {
209
+ process.removeListener('SIGINT', forwardSignal);
210
+ process.removeListener('SIGTERM', forwardSignal);
211
+ process.exit(code || 0);
212
+ });
213
+
214
+ // Keep process alive while Claude runs
215
+ return new Promise(() => {});
216
+ };
@@ -20,7 +20,8 @@ export const listCommand = () => {
20
20
  const label = isActive ? chalk.green.bold(name) : name;
21
21
  const desc = meta.description ? chalk.dim(` — ${meta.description}`) : '';
22
22
  const date = meta.created ? chalk.dim(` (${meta.created.split('T')[0]})`) : '';
23
- console.log(`${marker}${label}${desc}${date}`);
23
+ const mode = meta.mode ? chalk.dim(` [${meta.mode}]`) : '';
24
+ console.log(`${marker}${label}${desc}${date}${mode}`);
24
25
  }
25
26
  console.log('');
26
27
  };
@@ -1,7 +1,8 @@
1
1
  import { getActive, profileExists, getProfileDir } from '../profile-store.js';
2
- import { saveSymlinks } from '../symlink-manager.js';
3
- import { saveFiles } from '../file-operations.js';
4
- import { success, error } from '../output-helpers.js';
2
+ import { saveItems } from '../item-manager.js';
3
+ import { saveFiles, updateSettingsPaths } from '../file-operations.js';
4
+ import { success, error, info } from '../output-helpers.js';
5
+ import { DEFAULT_PROFILE } from '../constants.js';
5
6
 
6
7
  export const saveCommand = () => {
7
8
  const active = getActive();
@@ -10,13 +11,19 @@ export const saveCommand = () => {
10
11
  process.exit(1);
11
12
  }
12
13
 
14
+ if (active === DEFAULT_PROFILE) {
15
+ info('Default profile uses ~/.claude directly. No save needed.');
16
+ return;
17
+ }
18
+
13
19
  if (!profileExists(active)) {
14
20
  error(`Active profile "${active}" directory is missing.`);
15
21
  process.exit(1);
16
22
  }
17
23
 
18
24
  const profileDir = getProfileDir(active);
19
- saveSymlinks(profileDir);
25
+ saveItems(profileDir);
20
26
  saveFiles(profileDir);
27
+ updateSettingsPaths(profileDir, 'save');
21
28
  success(`Saved current state to profile "${active}"`);
22
29
  };