claude-switch-profile 1.4.2 → 1.4.3
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/.cocoindex_code/cocoindex.db/mdb/lock.mdb +0 -0
- package/CHANGELOG.md +8 -0
- package/README.md +38 -17
- package/bin/csp.js +21 -1
- package/package.json +1 -1
- package/src/commands/launch.js +158 -125
- package/src/isolated-launch-context.js +100 -0
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,14 @@ All notable changes to `claude-switch-profile` are documented here.
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [1.4.2] - 2026-03-31
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Re-released the previous patch after an interrupted publish flow (OTP step).
|
|
16
|
+
- No code changes from `1.4.1`; this version reflects successful npm publication and release metadata synchronization.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
12
20
|
## [1.4.1] - 2026-03-31
|
|
13
21
|
|
|
14
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -550,6 +550,40 @@ Set `CSP_DEBUG_LAUNCH_ENV=1` to print extended launch diagnostics. Do not use in
|
|
|
550
550
|
|
|
551
551
|
---
|
|
552
552
|
|
|
553
|
+
### exec
|
|
554
|
+
|
|
555
|
+
Run an arbitrary command inside isolated profile runtime env. This does **not** change global active profile.
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
csp exec <name> -- <command> [args...]
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Examples:**
|
|
562
|
+
|
|
563
|
+
Run any CLI tool with profile runtime env:
|
|
564
|
+
```bash
|
|
565
|
+
csp exec work -- env
|
|
566
|
+
csp exec work -- node scripts/check-env.js
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Run a shell function/alias defined by your interactive shell:
|
|
570
|
+
```bash
|
|
571
|
+
csp exec hd -- claude-hd2
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Behavior:**
|
|
575
|
+
1. Validates target profile exists
|
|
576
|
+
2. Ensures the profile snapshot exists; for legacy installs missing `default/`, guarded backfill only runs when the active profile is `default` or no active profile is set
|
|
577
|
+
3. Prepares per-profile runtime under `~/.claude-profiles/.runtime/<name>`
|
|
578
|
+
4. Resolves effective allowlisted `ANTHROPIC_*` launch env (`ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_MODEL`) with precedence: `settings.json env` > profile `.env` allowlist > parent process env
|
|
579
|
+
5. Strips inherited `CLAUDECODE`, inherited `CLAUDE_CONFIG_DIR`, inherited `ANTHROPIC_*`, and Claude session env vars before applying resolved allowlisted values
|
|
580
|
+
6. On Unix-like systems, runs the command through your interactive shell so shell functions/aliases can resolve like a normal terminal command; on Windows, keeps direct spawn behavior with `.cmd` / `.bat` wrapper detection
|
|
581
|
+
7. Reasserts `CLAUDE_CONFIG_DIR` and allowlisted `ANTHROPIC_*` after shell init so profile isolation wins over shell startup overrides
|
|
582
|
+
8. Inherits stdin/stdout/stderr and forwards child exit code
|
|
583
|
+
9. Keeps `.active` unchanged and never mutates global `~/.claude`
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
553
587
|
### uninstall
|
|
554
588
|
|
|
555
589
|
Remove all profiles and restore Claude Code to its pre-CSP state.
|
|
@@ -688,23 +722,6 @@ Another csp operation is running (PID: 12345).
|
|
|
688
722
|
Remove ~/.claude-profiles/.lock if stale.
|
|
689
723
|
```
|
|
690
724
|
|
|
691
|
-
### Automatic Backups
|
|
692
|
-
|
|
693
|
-
Every profile switch creates a timestamped backup:
|
|
694
|
-
|
|
695
|
-
```
|
|
696
|
-
~/.claude-profiles/.backup/
|
|
697
|
-
├── 2026-03-11T14-30-45-123Z/
|
|
698
|
-
│ ├── source.json
|
|
699
|
-
│ ├── settings.json
|
|
700
|
-
│ ├── .env
|
|
701
|
-
│ └── ...
|
|
702
|
-
└── 2026-03-11T15-45-22-456Z/
|
|
703
|
-
└── ...
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
Backups are pruned automatically; CSP keeps only the 2 most recent backups. You can manually restore by copying from backup directory.
|
|
707
|
-
|
|
708
725
|
### Claude Process Detection
|
|
709
726
|
|
|
710
727
|
When switching profiles, CSP detects if Claude Code is running:
|
|
@@ -943,6 +960,10 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and migration guidance.
|
|
|
943
960
|
└── package.json
|
|
944
961
|
```
|
|
945
962
|
|
|
963
|
+
## Planned / Future Features
|
|
964
|
+
|
|
965
|
+
- Automatic backups during profile switch flow (`csp use`) with backup retention/pruning.
|
|
966
|
+
|
|
946
967
|
## License
|
|
947
968
|
|
|
948
969
|
MIT
|
package/bin/csp.js
CHANGED
|
@@ -16,7 +16,7 @@ import { importCommand } from '../src/commands/import.js';
|
|
|
16
16
|
import { diffCommand } from '../src/commands/diff.js';
|
|
17
17
|
import { initCommand } from '../src/commands/init.js';
|
|
18
18
|
import { uninstallCommand } from '../src/commands/uninstall.js';
|
|
19
|
-
import { launchCommand } from '../src/commands/launch.js';
|
|
19
|
+
import { launchCommand, execCommand } from '../src/commands/launch.js';
|
|
20
20
|
import { deactivateCommand } from '../src/commands/deactivate.js';
|
|
21
21
|
import { toggleCommand } from '../src/commands/toggle.js';
|
|
22
22
|
import { statusCommand } from '../src/commands/status.js';
|
|
@@ -130,6 +130,26 @@ program
|
|
|
130
130
|
launchCommand(name, claudeArgs, options);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
program
|
|
134
|
+
.command('exec <name> [args...]')
|
|
135
|
+
.description('Run arbitrary command inside isolated profile runtime environment')
|
|
136
|
+
.allowUnknownOption(true)
|
|
137
|
+
.enablePositionalOptions(true)
|
|
138
|
+
.passThroughOptions(true)
|
|
139
|
+
.action((name, args, _options, cmd) => {
|
|
140
|
+
const passthrough = [...(args || [])];
|
|
141
|
+
const unknown = cmd.args.filter((a) => a !== name && !passthrough.includes(a));
|
|
142
|
+
const tokens = [...passthrough, ...unknown];
|
|
143
|
+
|
|
144
|
+
while (tokens[0] === '--') {
|
|
145
|
+
tokens.shift();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const command = tokens[0];
|
|
149
|
+
const commandArgs = tokens.slice(1);
|
|
150
|
+
execCommand(name, command, commandArgs);
|
|
151
|
+
});
|
|
152
|
+
|
|
133
153
|
program
|
|
134
154
|
.command('uninstall')
|
|
135
155
|
.description('Remove all profiles and restore Claude Code to pre-CSP state')
|
package/package.json
CHANGED
package/src/commands/launch.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { accessSync, constants as fsConstants, existsSync } from 'node:fs';
|
|
4
|
+
import { info, error } from '../output-helpers.js';
|
|
5
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
6
|
import { isWindows } from '../platform.js';
|
|
10
|
-
import { LAUNCH_CONFIG_ENV
|
|
11
|
-
import {
|
|
7
|
+
import { LAUNCH_CONFIG_ENV } from '../constants.js';
|
|
8
|
+
import {
|
|
9
|
+
ensureLaunchProfileReady,
|
|
10
|
+
formatLaunchEnvDiagnostics,
|
|
11
|
+
resolveIsolatedLaunchContext,
|
|
12
|
+
stripInheritedLaunchEnv,
|
|
13
|
+
} from '../isolated-launch-context.js';
|
|
14
|
+
|
|
15
|
+
export { ensureLaunchProfileReady, formatLaunchEnvDiagnostics, resolveIsolatedLaunchContext, stripInheritedLaunchEnv };
|
|
16
|
+
|
|
17
|
+
const SHELL_REASSERT_ENV_KEYS = [LAUNCH_CONFIG_ENV, 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL'];
|
|
18
|
+
const SAFE_SHELL_COMMAND = /^[A-Za-z0-9_./:@%+-]+$/;
|
|
12
19
|
|
|
13
20
|
const isTruthyDebugValue = (value) => {
|
|
14
21
|
if (value === undefined || value === null) return false;
|
|
@@ -61,161 +68,187 @@ export const resolveClaudeLaunchTarget = (env = process.env, dependencies = {})
|
|
|
61
68
|
return { command: 'claude', shell: true };
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
const requiresShell = /\.(cmd|bat)$/i.test(resolvedPath);
|
|
65
|
-
|
|
66
71
|
return {
|
|
67
|
-
command:
|
|
68
|
-
shell:
|
|
72
|
+
command: /\.(cmd|bat)$/i.test(resolvedPath) ? `"${resolvedPath}"` : resolvedPath,
|
|
73
|
+
shell: /\.(cmd|bat)$/i.test(resolvedPath),
|
|
69
74
|
};
|
|
70
75
|
};
|
|
71
76
|
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
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 {};
|
|
77
|
+
export const resolveExecTarget = (command, env = process.env, dependencies = {}) => {
|
|
78
|
+
const windows = dependencies.isWindows ?? isWindows;
|
|
79
|
+
if (!windows) {
|
|
80
|
+
return { command, shell: false };
|
|
82
81
|
}
|
|
83
|
-
};
|
|
84
82
|
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
83
|
+
const execRunner = dependencies.execFileSync || execFileSync;
|
|
84
|
+
const fileName = basename(command || '').toLowerCase();
|
|
85
|
+
if (/\.(cmd|bat)$/i.test(command || '') || fileName === 'cmd' || fileName === 'cmd.exe') {
|
|
86
|
+
return {
|
|
87
|
+
command: /\s/.test(command || '') ? `"${command}"` : command,
|
|
88
|
+
shell: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
88
91
|
|
|
89
92
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
93
|
+
const resolvedPath = execRunner('where.exe', [command], {
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
96
|
+
env,
|
|
97
|
+
})
|
|
98
|
+
.trim()
|
|
99
|
+
.split(/\r?\n/)
|
|
100
|
+
.map((entry) => entry.trim())
|
|
101
|
+
.find(Boolean);
|
|
102
|
+
|
|
103
|
+
if (resolvedPath && /\.(cmd|bat)$/i.test(resolvedPath)) {
|
|
104
|
+
return { command, shell: true };
|
|
105
|
+
}
|
|
92
106
|
} catch {
|
|
93
|
-
|
|
94
|
-
return {};
|
|
107
|
+
// Fall back to default non-shell spawn behavior.
|
|
95
108
|
}
|
|
109
|
+
|
|
110
|
+
return { command, shell: false };
|
|
96
111
|
};
|
|
97
112
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
const runSpawnedCommand = ({ command, args = [], env, shell = false, errorLabel }) => {
|
|
114
|
+
const child = spawn(command, args, {
|
|
115
|
+
stdio: 'inherit',
|
|
116
|
+
shell,
|
|
117
|
+
detached: false,
|
|
118
|
+
env,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const forwardSignal = (sig) => {
|
|
122
|
+
try {
|
|
123
|
+
child.kill(sig);
|
|
124
|
+
} catch {
|
|
125
|
+
// Child may have already exited
|
|
126
|
+
}
|
|
102
127
|
};
|
|
103
|
-
};
|
|
104
128
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const sources = diagnostics.anthropicKeySources || {};
|
|
129
|
+
process.on('SIGINT', forwardSignal);
|
|
130
|
+
process.on('SIGTERM', forwardSignal);
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
child.on('error', (err) => {
|
|
133
|
+
error(`${errorLabel}: ${err.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
child.on('exit', (code) => {
|
|
138
|
+
process.removeListener('SIGINT', forwardSignal);
|
|
139
|
+
process.removeListener('SIGTERM', forwardSignal);
|
|
140
|
+
process.exit(code || 0);
|
|
141
|
+
});
|
|
112
142
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.join(', ');
|
|
143
|
+
return new Promise(() => {});
|
|
144
|
+
};
|
|
116
145
|
|
|
117
|
-
|
|
146
|
+
const formatCommand = (command, args = []) => {
|
|
147
|
+
return [command, ...args].filter((part) => part !== undefined && part !== null && String(part).length > 0).join(' ');
|
|
118
148
|
};
|
|
119
149
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
150
|
+
const quoteShellToken = (value) => `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
151
|
+
const formatShellCommandToken = (value) => (SAFE_SHELL_COMMAND.test(String(value || '')) ? String(value) : quoteShellToken(value));
|
|
152
|
+
const isExecutableFile = (filePath) => {
|
|
153
|
+
if (!filePath) return false;
|
|
154
|
+
try {
|
|
155
|
+
accessSync(filePath, fsConstants.X_OK);
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
126
159
|
}
|
|
127
|
-
|
|
160
|
+
};
|
|
161
|
+
const getInteractiveShellPath = (env = process.env) => {
|
|
162
|
+
if (isExecutableFile(env.SHELL)) return env.SHELL;
|
|
163
|
+
return '/bin/sh';
|
|
128
164
|
};
|
|
129
165
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
166
|
+
const buildShellEvalCommand = (command, args = []) => {
|
|
167
|
+
return [formatShellCommandToken(command), ...args.map((arg) => quoteShellToken(arg))].join(' ');
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const buildShellEnvReassertion = (env) => {
|
|
171
|
+
return SHELL_REASSERT_ENV_KEYS.map((key) => {
|
|
172
|
+
if (Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined) {
|
|
173
|
+
return `export ${key}=${quoteShellToken(env[key])}`;
|
|
137
174
|
}
|
|
138
|
-
|
|
175
|
+
return `unset ${key}`;
|
|
176
|
+
}).join('; ');
|
|
177
|
+
};
|
|
139
178
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
179
|
+
const executeViaInteractiveShell = ({ command, args = [], launchEnv, errorLabel }) => {
|
|
180
|
+
const shellPath = getInteractiveShellPath(launchEnv);
|
|
181
|
+
const shellEnv = { ...launchEnv, CSP_EXEC_CMD: buildShellEvalCommand(command, args) };
|
|
182
|
+
const shellScript = `${buildShellEnvReassertion(launchEnv)}; eval "$CSP_EXEC_CMD"`;
|
|
144
183
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
184
|
+
return runSpawnedCommand({
|
|
185
|
+
command: shellPath,
|
|
186
|
+
args: ['-ic', shellScript],
|
|
187
|
+
env: shellEnv,
|
|
188
|
+
errorLabel,
|
|
189
|
+
});
|
|
190
|
+
};
|
|
150
191
|
|
|
151
|
-
|
|
192
|
+
export const launchCommand = async (name, claudeArgs, options = {}) => {
|
|
193
|
+
ensureLaunchProfileReady(name);
|
|
152
194
|
|
|
153
|
-
|
|
195
|
+
let args = [...(claudeArgs || [])];
|
|
196
|
+
if (options.legacyGlobal || args.includes('--legacy-global')) {
|
|
197
|
+
args = args.filter((arg) => arg !== '--legacy-global');
|
|
154
198
|
await useCommand(name, { save: true, skipClaudeCheck: true });
|
|
155
199
|
info(`Launching legacy/global mode: claude ${args.join(' ')}`.trim());
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const { profileSettingsEnv, profileDotEnvEnv } = readProfileLaunchEnvSources(runtimeDir);
|
|
159
|
-
const { launchEnv: resolvedLaunchEnv, diagnostics } = buildEffectiveLaunchEnv({
|
|
160
|
-
parentEnv: process.env,
|
|
161
|
-
profileSettingsEnv,
|
|
162
|
-
profileDotEnvEnv,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
launchEnv = {
|
|
166
|
-
...resolvedLaunchEnv,
|
|
167
|
-
[LAUNCH_CONFIG_ENV]: runtimeDir,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// CLAUDE_CONFIG_DIR already redirects user-level config lookups to the
|
|
171
|
-
// runtime dir, so the "user" settings source reads {runtimeDir}/settings.json
|
|
172
|
-
// — which is the correct profile-specific settings. We do NOT exclude "user"
|
|
173
|
-
// because Claude Code gates skill/hook discovery on it being enabled.
|
|
200
|
+
return runSpawnedCommand({ command: 'claude', args, env: { ...process.env }, errorLabel: 'Failed to launch Claude' });
|
|
201
|
+
}
|
|
174
202
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
info(`CLAUDE_CONFIG_DIR=${launchEnv[LAUNCH_CONFIG_ENV]}`);
|
|
178
|
-
info(`ANTHROPIC_AUTH_TOKEN=${launchEnv.ANTHROPIC_AUTH_TOKEN ? launchEnv.ANTHROPIC_AUTH_TOKEN.slice(0, 8) + '...' : '(not set)'}`);
|
|
179
|
-
info(`ANTHROPIC_BASE_URL=${launchEnv.ANTHROPIC_BASE_URL || '(not set)'}`);
|
|
203
|
+
const isolated = await resolveIsolatedLaunchContext(name, process.env);
|
|
204
|
+
const launchEnv = isolated.launchEnv;
|
|
180
205
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
206
|
+
info(`Launch env diagnostics (${name}): ${formatLaunchEnvDiagnostics(isolated.diagnostics)}`);
|
|
207
|
+
info(`CLAUDE_CONFIG_DIR=${launchEnv[LAUNCH_CONFIG_ENV]}`);
|
|
208
|
+
info(`ANTHROPIC_AUTH_TOKEN=${launchEnv.ANTHROPIC_AUTH_TOKEN ? `${launchEnv.ANTHROPIC_AUTH_TOKEN.slice(0, 8)}...` : '(not set)'}`);
|
|
209
|
+
info(`ANTHROPIC_BASE_URL=${launchEnv.ANTHROPIC_BASE_URL || '(not set)'}`);
|
|
184
210
|
|
|
185
|
-
|
|
211
|
+
if (isTruthyDebugValue(process.env.CSP_DEBUG_LAUNCH_ENV)) {
|
|
212
|
+
info(`[DEBUG] Full launch env ANTHROPIC keys: ${JSON.stringify(Object.fromEntries(Object.entries(launchEnv).filter(([key]) => key.startsWith('ANTHROPIC_'))))}`);
|
|
186
213
|
}
|
|
187
214
|
|
|
215
|
+
info(`Launching isolated session for profile "${name}": claude ${args.join(' ')}`.trim());
|
|
216
|
+
|
|
188
217
|
const launchTarget = resolveClaudeLaunchTarget(launchEnv);
|
|
218
|
+
return runSpawnedCommand({ command: launchTarget.command, args, env: launchEnv, shell: launchTarget.shell, errorLabel: 'Failed to launch Claude' });
|
|
219
|
+
};
|
|
189
220
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
shell: launchTarget.shell,
|
|
193
|
-
detached: false,
|
|
194
|
-
env: launchEnv,
|
|
195
|
-
});
|
|
221
|
+
export const execCommand = async (name, command, commandArgs = []) => {
|
|
222
|
+
ensureLaunchProfileReady(name);
|
|
196
223
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
child.kill(sig);
|
|
201
|
-
} catch {
|
|
202
|
-
// Child may have already exited
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
process.on('SIGINT', forwardSignal);
|
|
206
|
-
process.on('SIGTERM', forwardSignal);
|
|
207
|
-
|
|
208
|
-
child.on('error', (err) => {
|
|
209
|
-
error(`Failed to launch Claude: ${err.message}`);
|
|
224
|
+
const normalizedCommand = typeof command === 'string' ? command.trim() : '';
|
|
225
|
+
if (!normalizedCommand) {
|
|
226
|
+
error('Missing command. Usage: csp exec <name> -- <cmd> [args...]');
|
|
210
227
|
process.exit(1);
|
|
211
|
-
}
|
|
228
|
+
}
|
|
212
229
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
process.removeListener('SIGTERM', forwardSignal);
|
|
216
|
-
process.exit(code || 0);
|
|
217
|
-
});
|
|
230
|
+
const isolated = await resolveIsolatedLaunchContext(name, process.env);
|
|
231
|
+
const launchEnv = isolated.launchEnv;
|
|
218
232
|
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
info(`Launch env diagnostics (${name}): ${formatLaunchEnvDiagnostics(isolated.diagnostics)}`);
|
|
234
|
+
info(`CLAUDE_CONFIG_DIR=${launchEnv[LAUNCH_CONFIG_ENV]}`);
|
|
235
|
+
info(`Executing isolated command for profile "${name}": ${formatCommand(normalizedCommand, commandArgs)}`);
|
|
236
|
+
|
|
237
|
+
if (!isWindows) {
|
|
238
|
+
return executeViaInteractiveShell({
|
|
239
|
+
command: normalizedCommand,
|
|
240
|
+
args: commandArgs,
|
|
241
|
+
launchEnv,
|
|
242
|
+
errorLabel: `Failed to execute command "${normalizedCommand}"`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const execTarget = resolveExecTarget(normalizedCommand, launchEnv);
|
|
247
|
+
return runSpawnedCommand({
|
|
248
|
+
command: execTarget.command,
|
|
249
|
+
args: commandArgs,
|
|
250
|
+
env: launchEnv,
|
|
251
|
+
shell: execTarget.shell,
|
|
252
|
+
errorLabel: `Failed to execute command "${normalizedCommand}"`,
|
|
253
|
+
});
|
|
221
254
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { profileExists, ensureDefaultProfileSnapshot } from './profile-store.js';
|
|
4
|
+
import { ensureRuntimeInstance } from './runtime-instance-manager.js';
|
|
5
|
+
import { withRuntimeLock } from './safety.js';
|
|
6
|
+
import { error, warn } from './output-helpers.js';
|
|
7
|
+
import { LAUNCH_CONFIG_ENV, DEFAULT_PROFILE } from './constants.js';
|
|
8
|
+
import {
|
|
9
|
+
buildEffectiveLaunchEnv,
|
|
10
|
+
parseDotEnvLaunchEnv,
|
|
11
|
+
parseSettingsLaunchEnv,
|
|
12
|
+
sanitizeInheritedLaunchEnv,
|
|
13
|
+
} from './launch-effective-env-resolver.js';
|
|
14
|
+
|
|
15
|
+
const readProfileSettingsLaunchEnv = (profileDir) => {
|
|
16
|
+
const settingsPath = join(profileDir, 'settings.json');
|
|
17
|
+
if (!existsSync(settingsPath)) return {};
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return parseSettingsLaunchEnv(readFileSync(settingsPath, 'utf-8'));
|
|
21
|
+
} catch {
|
|
22
|
+
warn(`Could not parse launch env from ${settingsPath}; falling back to inherited env.`);
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readProfileDotEnvLaunchEnv = (profileDir) => {
|
|
28
|
+
const dotEnvPath = join(profileDir, '.env');
|
|
29
|
+
if (!existsSync(dotEnvPath)) return {};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return parseDotEnvLaunchEnv(readFileSync(dotEnvPath, 'utf-8'));
|
|
33
|
+
} catch {
|
|
34
|
+
warn(`Could not parse launch env from ${dotEnvPath}; falling back to inherited env.`);
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const readProfileLaunchEnvSources = (runtimeDir) => {
|
|
40
|
+
return {
|
|
41
|
+
profileSettingsEnv: readProfileSettingsLaunchEnv(runtimeDir),
|
|
42
|
+
profileDotEnvEnv: readProfileDotEnvLaunchEnv(runtimeDir),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const formatLaunchEnvDiagnostics = (diagnostics = {}) => {
|
|
47
|
+
const keys = diagnostics.anthropicKeys || [];
|
|
48
|
+
const sources = diagnostics.anthropicKeySources || {};
|
|
49
|
+
|
|
50
|
+
if (!keys.length) {
|
|
51
|
+
return 'ANTHROPIC_* keys: none';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `ANTHROPIC_* keys: ${keys.map((key) => `${key}<=${sources[key] || 'unknown'}`).join(', ')}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const stripInheritedLaunchEnv = (env = process.env) => {
|
|
58
|
+
const sanitized = sanitizeInheritedLaunchEnv(env);
|
|
59
|
+
for (const key of Object.keys(sanitized)) {
|
|
60
|
+
if (key.toUpperCase().startsWith('ANTHROPIC_')) {
|
|
61
|
+
delete sanitized[key];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return sanitized;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const ensureLaunchProfileReady = (name) => {
|
|
68
|
+
if (name === DEFAULT_PROFILE) {
|
|
69
|
+
try {
|
|
70
|
+
ensureDefaultProfileSnapshot();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
error(err.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!profileExists(name)) {
|
|
78
|
+
error(`Profile "${name}" does not exist. Run "csp list" to see available profiles.`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const resolveIsolatedLaunchContext = async (name, parentEnv = process.env) => {
|
|
84
|
+
const runtimeDir = await withRuntimeLock(name, async () => ensureRuntimeInstance(name));
|
|
85
|
+
const { profileSettingsEnv, profileDotEnvEnv } = readProfileLaunchEnvSources(runtimeDir);
|
|
86
|
+
const { launchEnv: resolvedLaunchEnv, diagnostics } = buildEffectiveLaunchEnv({
|
|
87
|
+
parentEnv,
|
|
88
|
+
profileSettingsEnv,
|
|
89
|
+
profileDotEnvEnv,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
runtimeDir,
|
|
94
|
+
diagnostics,
|
|
95
|
+
launchEnv: {
|
|
96
|
+
...resolvedLaunchEnv,
|
|
97
|
+
[LAUNCH_CONFIG_ENV]: runtimeDir,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|