@lkangd/cc-env 1.2.1 → 1.3.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 +4 -0
- package/README.zh.md +4 -0
- package/dist/cli.js +109 -80
- package/dist/commands/completion.js +24 -18
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/init.js +2 -9
- package/dist/commands/preset/create.js +70 -9
- package/dist/commands/restore.js +25 -1
- package/dist/commands/run.js +27 -22
- package/dist/core/claude-required-keys.js +8 -0
- package/dist/core/cli-name.js +8 -0
- package/dist/core/schema.js +10 -0
- package/dist/flows/preset-create-flow.js +49 -1
- package/dist/flows/restore-flow.js +26 -7
- package/dist/ink/components/text-input.js +7 -0
- package/dist/ink/hooks/use-text-input.js +71 -0
- package/dist/ink/preset-create-app.js +92 -43
- package/dist/ink/preset-edit-app.js +12 -16
- package/dist/ink/restore-app.js +24 -9
- package/package.json +3 -2
- package/dist/ink/init-app.js +0 -54
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
`cc-env` is a CLI tool that lets you define, switch, and restore environment variable configurations for Claude Code — per project or via reusable presets. No more manually editing `settings.json` or juggling `.env` files across workspaces.
|
|
22
22
|
|
|
23
|
+
> **Alias:** `ccenv` can be used as a shorthand for `cc-env` everywhere (e.g., `ccenv run`, `ccenv create`).
|
|
24
|
+
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
25
27
|
### via npm
|
|
@@ -75,6 +77,8 @@ cc-env run
|
|
|
75
77
|
|
|
76
78
|
## Shell Completion
|
|
77
79
|
|
|
80
|
+
> Both `cc-env` and `ccenv` are supported in completion scripts.
|
|
81
|
+
|
|
78
82
|
```bash
|
|
79
83
|
# bash
|
|
80
84
|
cc-env completion bash >> ~/.bashrc
|
package/README.zh.md
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
`cc-env` 是一个 CLI 工具,让你可以为 Claude Code 定义、切换和恢复环境变量配置——支持按项目配置或使用可复用的预设。不再需要手动编辑 `settings.json` 或在不同工作区之间切换 `.env` 文件。
|
|
22
22
|
|
|
23
|
+
> **别名:** `ccenv` 可以作为 `cc-env` 的简写使用(例如 `ccenv run`、`ccenv create`)。
|
|
24
|
+
|
|
23
25
|
## 安装
|
|
24
26
|
|
|
25
27
|
### 通过 npm
|
|
@@ -75,6 +77,8 @@ cc-env run
|
|
|
75
77
|
|
|
76
78
|
## Shell 补全
|
|
77
79
|
|
|
80
|
+
> 补全脚本同时支持 `cc-env` 和 `ccenv`。
|
|
81
|
+
|
|
78
82
|
```bash
|
|
79
83
|
# bash
|
|
80
84
|
cc-env completion bash >> ~/.bashrc
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,6 @@ import gradient from 'gradient-string';
|
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import packageJson from '../package.json' with { type: 'json' };
|
|
9
9
|
const h = React.createElement;
|
|
10
|
-
import { createInitCommand } from './commands/init.js';
|
|
11
10
|
import { createPresetCreateCommand } from './commands/preset/create.js';
|
|
12
11
|
import { createDeletePresetCommand } from './commands/preset/delete.js';
|
|
13
12
|
import { createEditPresetCommand } from './commands/preset/edit.js';
|
|
@@ -19,13 +18,13 @@ import { createRestoreCommand } from './commands/restore.js';
|
|
|
19
18
|
import { createRunCommand } from './commands/run.js';
|
|
20
19
|
import { runDoctorCommand } from './commands/doctor.js';
|
|
21
20
|
import { findClaudeExecutable } from './core/find-claude.js';
|
|
22
|
-
import { InitApp } from './ink/init-app.js';
|
|
23
21
|
import { renderEnvSummary } from './ink/summary.js';
|
|
24
22
|
import { PresetCreateApp } from './ink/preset-create-app.js';
|
|
25
23
|
import { PresetShowApp } from './ink/preset-show-app.js';
|
|
26
24
|
import { RunPresetSelectApp } from './ink/run-preset-select-app.js';
|
|
27
25
|
import { advanceRestoreFlow, createRestoreFlowState } from './flows/restore-flow.js';
|
|
28
26
|
import { RestoreApp } from './ink/restore-app.js';
|
|
27
|
+
import { getCliName } from './core/cli-name.js';
|
|
29
28
|
import { CliError } from './core/errors.js';
|
|
30
29
|
import { resolveGlobalRoot } from './core/paths.js';
|
|
31
30
|
import { spawnCommand } from './core/spawn.js';
|
|
@@ -38,7 +37,7 @@ import { createSettingsEnvService } from './services/settings-env-service.js';
|
|
|
38
37
|
import { createShellEnvService } from './services/shell-env-service.js';
|
|
39
38
|
const program = new Command();
|
|
40
39
|
program
|
|
41
|
-
.name(
|
|
40
|
+
.name(getCliName())
|
|
42
41
|
.description('Manage runtime environment variables for Claude Code')
|
|
43
42
|
.version(packageJson.version)
|
|
44
43
|
.option('--verbose', 'Enable verbose output')
|
|
@@ -54,8 +53,80 @@ const shellEnvService = createShellEnvService({ homeDir });
|
|
|
54
53
|
const projectEnvService = createProjectEnvService({ cwd });
|
|
55
54
|
const presetService = createPresetService(globalRoot);
|
|
56
55
|
const historyService = createHistoryService(globalRoot);
|
|
56
|
+
const projectStateService = createProjectStateService(globalRoot);
|
|
57
|
+
async function runPresetCreateFlow({ detectedEnv, requiredKeys }) {
|
|
58
|
+
let result;
|
|
59
|
+
const app = render(h(PresetCreateApp, {
|
|
60
|
+
onSubmit: value => {
|
|
61
|
+
result = value;
|
|
62
|
+
},
|
|
63
|
+
readFile: async (filePath) => {
|
|
64
|
+
const { readEnvFile } = await import('./commands/preset/create.js');
|
|
65
|
+
return readEnvFile(filePath);
|
|
66
|
+
},
|
|
67
|
+
globalPresetPath: name => presetService.getPath(name),
|
|
68
|
+
projectEnvPath: join(cwd, '.cc-env', 'env.json'),
|
|
69
|
+
detectedEnv,
|
|
70
|
+
requiredKeys,
|
|
71
|
+
}));
|
|
72
|
+
await app.waitUntilExit();
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
async function runWithBootstrap({ args = [], dryRun = false, yes = false, json = false, skipDetect = false, }) {
|
|
76
|
+
const result = await createRunCommand({
|
|
77
|
+
claudeSettingsEnvService,
|
|
78
|
+
presetService,
|
|
79
|
+
projectEnvService,
|
|
80
|
+
projectStateService,
|
|
81
|
+
findClaude: findClaudeExecutable,
|
|
82
|
+
renderPresetSelect: async ({ presets, defaultIndex }) => {
|
|
83
|
+
let selected;
|
|
84
|
+
const app = render(h(RunPresetSelectApp, {
|
|
85
|
+
presets,
|
|
86
|
+
defaultIndex,
|
|
87
|
+
onSubmit: preset => {
|
|
88
|
+
selected = preset;
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
await app.waitUntilExit();
|
|
92
|
+
return selected;
|
|
93
|
+
},
|
|
94
|
+
spawnCommand
|
|
95
|
+
})({
|
|
96
|
+
args,
|
|
97
|
+
dryRun,
|
|
98
|
+
yes,
|
|
99
|
+
json,
|
|
100
|
+
skipDetect,
|
|
101
|
+
cwd,
|
|
102
|
+
});
|
|
103
|
+
if (!result || result.status === 'executed') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (Object.keys(result.detectedEnv).length === 0) {
|
|
107
|
+
throw new CliError('No presets found and no migratable Claude settings were detected.');
|
|
108
|
+
}
|
|
109
|
+
const createdPreset = await createPresetCreateCommand({
|
|
110
|
+
presetService,
|
|
111
|
+
projectEnvService,
|
|
112
|
+
claudeSettingsEnvService,
|
|
113
|
+
historyService,
|
|
114
|
+
renderFlow: runPresetCreateFlow,
|
|
115
|
+
})({ cwd });
|
|
116
|
+
if (!createdPreset) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await projectStateService.saveLastPreset(cwd, createdPreset);
|
|
120
|
+
await runWithBootstrap({
|
|
121
|
+
args,
|
|
122
|
+
dryRun,
|
|
123
|
+
yes: true,
|
|
124
|
+
json,
|
|
125
|
+
skipDetect: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
57
128
|
async function runRestoreFlow(context) {
|
|
58
|
-
const state = createRestoreFlowState(context.records);
|
|
129
|
+
const state = createRestoreFlowState(context.records, cwd);
|
|
59
130
|
const firstRecord = context.records[0];
|
|
60
131
|
if (!firstRecord) {
|
|
61
132
|
render(h(RestoreApp, { state }));
|
|
@@ -66,16 +137,19 @@ async function runRestoreFlow(context) {
|
|
|
66
137
|
type: 'select-record',
|
|
67
138
|
timestamp: firstRecord.timestamp
|
|
68
139
|
});
|
|
69
|
-
if (firstRecord.action === 'init') {
|
|
140
|
+
if (firstRecord.action === 'init' || firstRecord.action === 'preset-create') {
|
|
70
141
|
const doneState = advanceRestoreFlow(selectedRecordState, { type: 'confirm' });
|
|
71
142
|
if (doneState.step !== 'done') {
|
|
72
143
|
return undefined;
|
|
73
144
|
}
|
|
74
145
|
return {
|
|
75
146
|
confirmed: true,
|
|
76
|
-
timestamp: firstRecord.timestamp
|
|
147
|
+
timestamp: firstRecord.timestamp,
|
|
77
148
|
};
|
|
78
149
|
}
|
|
150
|
+
if (firstRecord.action !== 'restore') {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
79
153
|
const confirmState = advanceRestoreFlow(selectedRecordState, {
|
|
80
154
|
type: 'select-target',
|
|
81
155
|
targetType: firstRecord.targetType,
|
|
@@ -94,7 +168,7 @@ async function runRestoreFlow(context) {
|
|
|
94
168
|
return {
|
|
95
169
|
confirmed: true,
|
|
96
170
|
timestamp: doneState.selectedTimestamp,
|
|
97
|
-
targetType: doneState.targetType
|
|
171
|
+
...(doneState.targetType ? { targetType: doneState.targetType } : {}),
|
|
98
172
|
};
|
|
99
173
|
}
|
|
100
174
|
return undefined;
|
|
@@ -125,62 +199,13 @@ program
|
|
|
125
199
|
.option('--json', 'Output as JSON (only with --dry-run)')
|
|
126
200
|
.action((args, options) => {
|
|
127
201
|
const rawArgs = args ?? [];
|
|
128
|
-
return
|
|
129
|
-
claudeSettingsEnvService,
|
|
130
|
-
presetService,
|
|
131
|
-
projectEnvService,
|
|
132
|
-
projectStateService: createProjectStateService(globalRoot),
|
|
133
|
-
findClaude: findClaudeExecutable,
|
|
134
|
-
renderPresetSelect: async ({ presets, defaultIndex }) => {
|
|
135
|
-
let result;
|
|
136
|
-
const app = render(h(RunPresetSelectApp, {
|
|
137
|
-
presets,
|
|
138
|
-
defaultIndex,
|
|
139
|
-
onSubmit: preset => {
|
|
140
|
-
result = preset;
|
|
141
|
-
}
|
|
142
|
-
}));
|
|
143
|
-
await app.waitUntilExit();
|
|
144
|
-
return result;
|
|
145
|
-
},
|
|
146
|
-
spawnCommand
|
|
147
|
-
})({
|
|
202
|
+
return runWithBootstrap({
|
|
148
203
|
args: rawArgs,
|
|
149
204
|
dryRun: options.dryRun ?? false,
|
|
150
205
|
yes: options.yes ?? false,
|
|
151
206
|
json: options.json ?? false,
|
|
152
|
-
cwd
|
|
153
207
|
});
|
|
154
208
|
});
|
|
155
|
-
program
|
|
156
|
-
.command('init')
|
|
157
|
-
.description('Initialize cc-env for the current project')
|
|
158
|
-
.option('-y, --yes', 'Accept all defaults without interactive prompts')
|
|
159
|
-
.action(options => createInitCommand({
|
|
160
|
-
claudeSettingsEnvService,
|
|
161
|
-
shellEnvService,
|
|
162
|
-
historyService,
|
|
163
|
-
renderEnvSummary,
|
|
164
|
-
renderFlow: async (context) => {
|
|
165
|
-
if (context.yes) {
|
|
166
|
-
return {
|
|
167
|
-
selectedKeys: context.requiredKeys,
|
|
168
|
-
confirmed: true
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
let result;
|
|
172
|
-
const app = render(h(InitApp, {
|
|
173
|
-
...context,
|
|
174
|
-
onSubmit: value => {
|
|
175
|
-
result = value;
|
|
176
|
-
}
|
|
177
|
-
}));
|
|
178
|
-
await app.waitUntilExit();
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
})({
|
|
182
|
-
yes: options.yes
|
|
183
|
-
}));
|
|
184
209
|
program
|
|
185
210
|
.command('restore')
|
|
186
211
|
.description('Restore environment variables from a previous snapshot')
|
|
@@ -233,26 +258,15 @@ program
|
|
|
233
258
|
program
|
|
234
259
|
.command('create')
|
|
235
260
|
.description('Create a new environment preset')
|
|
236
|
-
.action(() =>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
readFile: async (filePath) => {
|
|
246
|
-
const { readEnvFile } = await import('./commands/preset/create.js');
|
|
247
|
-
return readEnvFile(filePath);
|
|
248
|
-
},
|
|
249
|
-
globalPresetPath: name => presetService.getPath(name),
|
|
250
|
-
projectEnvPath: join(cwd, '.cc-env', 'env.json')
|
|
251
|
-
}));
|
|
252
|
-
await app.waitUntilExit();
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
})({ cwd }));
|
|
261
|
+
.action(async () => {
|
|
262
|
+
await createPresetCreateCommand({
|
|
263
|
+
presetService,
|
|
264
|
+
projectEnvService,
|
|
265
|
+
claudeSettingsEnvService,
|
|
266
|
+
historyService,
|
|
267
|
+
renderFlow: runPresetCreateFlow,
|
|
268
|
+
})({ cwd });
|
|
269
|
+
});
|
|
256
270
|
program
|
|
257
271
|
.command('doctor')
|
|
258
272
|
.description('Check system health and configuration')
|
|
@@ -286,7 +300,7 @@ program
|
|
|
286
300
|
.option('--shell <shell>', 'Shell type (bash, zsh, fish)', 'bash')
|
|
287
301
|
.action(async (options) => {
|
|
288
302
|
const { generateCompletion } = await import('./commands/completion.js');
|
|
289
|
-
process.stdout.write(generateCompletion(options.shell));
|
|
303
|
+
process.stdout.write(generateCompletion(options.shell, getCliName()));
|
|
290
304
|
});
|
|
291
305
|
function printBanner() {
|
|
292
306
|
const banner = figlet.textSync('CC ENV', { font: 'ANSI Shadow' });
|
|
@@ -306,7 +320,22 @@ program.hook('preAction', (thisCommand) => {
|
|
|
306
320
|
thisCommand.setOptionValue('yes', true);
|
|
307
321
|
}
|
|
308
322
|
});
|
|
309
|
-
|
|
323
|
+
async function main() {
|
|
324
|
+
const args = process.argv.slice(2);
|
|
325
|
+
if (args.length === 0) {
|
|
326
|
+
const hasGlobalPreset = (await presetService.listNames()).length > 0;
|
|
327
|
+
const { env: projectEnv } = await projectEnvService.readWithMeta();
|
|
328
|
+
if (hasGlobalPreset || Object.keys(projectEnv).length > 0) {
|
|
329
|
+
await runWithBootstrap({ args: [], yes: !process.stdin.isTTY });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
program.outputHelp();
|
|
333
|
+
process.exitCode = 0;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
await program.parseAsync(process.argv);
|
|
337
|
+
}
|
|
338
|
+
main().catch((error) => {
|
|
310
339
|
if (error instanceof CliError) {
|
|
311
340
|
process.stderr.write(`\n Error: ${error.message}\n\n`);
|
|
312
341
|
process.exitCode = error.exitCode;
|
|
@@ -318,7 +347,7 @@ program.parseAsync(process.argv).catch((error) => {
|
|
|
318
347
|
process.exitCode = 0;
|
|
319
348
|
return;
|
|
320
349
|
}
|
|
321
|
-
const hint = ` Run "
|
|
350
|
+
const hint = ` Run "${getCliName()} --help" to see available commands and options.\n`;
|
|
322
351
|
const formatted = message?.replace(/^error:\s*/i, '') ?? 'Unknown error';
|
|
323
352
|
process.stderr.write(`\n Error: ${formatted}\n\n${hint}\n`);
|
|
324
353
|
process.exitCode = 1;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
const COMMANDS = ['run', 'init', 'restore', 'show', 'delete', 'create', 'doctor', 'completion', '--help', '--version'];
|
|
2
|
-
export function generateCompletion(shell) {
|
|
2
|
+
export function generateCompletion(shell, cliName = 'cc-env') {
|
|
3
3
|
switch (shell) {
|
|
4
4
|
case 'zsh':
|
|
5
|
-
return generateZsh();
|
|
5
|
+
return generateZsh(cliName);
|
|
6
6
|
case 'fish':
|
|
7
|
-
return generateFish();
|
|
7
|
+
return generateFish(cliName);
|
|
8
8
|
default:
|
|
9
|
-
return generateBash();
|
|
9
|
+
return generateBash(cliName);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
function generateBash() {
|
|
13
|
-
return `#
|
|
12
|
+
function generateBash(cliName) {
|
|
13
|
+
return `# ${cliName} bash completion
|
|
14
14
|
# Add to ~/.bashrc: eval "$(cc-env completion --shell bash)"
|
|
15
15
|
_cc_env_completions() {
|
|
16
16
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
@@ -18,12 +18,13 @@ _cc_env_completions() {
|
|
|
18
18
|
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
19
19
|
}
|
|
20
20
|
complete -F _cc_env_completions cc-env
|
|
21
|
+
complete -F _cc_env_completions ccenv
|
|
21
22
|
`;
|
|
22
23
|
}
|
|
23
|
-
function generateZsh() {
|
|
24
|
+
function generateZsh(cliName) {
|
|
24
25
|
const cmds = COMMANDS.filter((c) => !c.startsWith('-'));
|
|
25
26
|
const cmdList = cmds.map((c) => ` '${c}'`).join('\n');
|
|
26
|
-
return `#
|
|
27
|
+
return `# ${cliName} zsh completion
|
|
27
28
|
# Add to ~/.zshrc: eval "$(cc-env completion --shell zsh)"
|
|
28
29
|
_cc_env() {
|
|
29
30
|
local -a commands
|
|
@@ -33,9 +34,10 @@ ${cmdList}
|
|
|
33
34
|
_describe 'command' commands
|
|
34
35
|
}
|
|
35
36
|
compdef _cc_env cc-env
|
|
37
|
+
compdef _cc_env ccenv
|
|
36
38
|
`;
|
|
37
39
|
}
|
|
38
|
-
function generateFish() {
|
|
40
|
+
function generateFish(cliName) {
|
|
39
41
|
const cmds = [
|
|
40
42
|
['run', 'Run claude with merged environment variables'],
|
|
41
43
|
['init', 'Initialize cc-env for the current project'],
|
|
@@ -46,15 +48,19 @@ function generateFish() {
|
|
|
46
48
|
['doctor', 'Check system health and configuration'],
|
|
47
49
|
['completion', 'Generate shell completion script'],
|
|
48
50
|
];
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
+
const names = ['cc-env', 'ccenv'];
|
|
52
|
+
const subcommandLines = names.flatMap(name => cmds.map(([cmd, desc]) => `complete -c ${name} -f -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`));
|
|
53
|
+
const flagLines = names.flatMap(name => [
|
|
54
|
+
`complete -c ${name} -l help -d 'Show help'`,
|
|
55
|
+
`complete -c ${name} -l version -d 'Show version'`,
|
|
56
|
+
`complete -c ${name} -l json -d 'Output as JSON'`,
|
|
57
|
+
`complete -c ${name} -l quiet -d 'Suppress non-essential output'`,
|
|
58
|
+
`complete -c ${name} -l verbose -d 'Enable verbose output'`,
|
|
59
|
+
`complete -c ${name} -l no-interactive -d 'Disable interactive prompts'`,
|
|
60
|
+
]);
|
|
61
|
+
return `# ${cliName} fish completion
|
|
51
62
|
# Add to fish config: cc-env completion --shell fish | source
|
|
52
|
-
${
|
|
53
|
-
|
|
54
|
-
complete -c cc-env -l version -d 'Show version'
|
|
55
|
-
complete -c cc-env -l json -d 'Output as JSON'
|
|
56
|
-
complete -c cc-env -l quiet -d 'Suppress non-essential output'
|
|
57
|
-
complete -c cc-env -l verbose -d 'Enable verbose output'
|
|
58
|
-
complete -c cc-env -l no-interactive -d 'Disable interactive prompts'
|
|
63
|
+
${subcommandLines.join('\n')}
|
|
64
|
+
${flagLines.join('\n')}
|
|
59
65
|
`;
|
|
60
66
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { getCliName } from '../core/cli-name.js';
|
|
3
4
|
import { findClaudeExecutable } from '../core/find-claude.js';
|
|
4
5
|
import { resolveGlobalRoot } from '../core/paths.js';
|
|
5
6
|
async function exists(p) {
|
|
@@ -36,7 +37,7 @@ async function checkClaudeExecutable() {
|
|
|
36
37
|
async function checkProjectEnv(cwd) {
|
|
37
38
|
const path = join(cwd, '.cc-env', 'env.json');
|
|
38
39
|
const ok = await exists(path);
|
|
39
|
-
return { label: 'Project env (.cc-env/env.json)', ok, detail: ok ? path :
|
|
40
|
+
return { label: 'Project env (.cc-env/env.json)', ok, detail: ok ? path : `not initialized — run: ${getCliName()} init` };
|
|
40
41
|
}
|
|
41
42
|
function renderCheck(result, json) {
|
|
42
43
|
if (json)
|
package/dist/commands/init.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { requiredClaudeKeys } from '../core/claude-required-keys.js';
|
|
3
4
|
import { CliError } from '../core/errors.js';
|
|
4
5
|
import { envMapSchema } from '../core/schema.js';
|
|
5
6
|
const h = React.createElement;
|
|
6
|
-
const requiredInitKeys = [
|
|
7
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
8
|
-
'ANTHROPIC_BASE_URL',
|
|
9
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
10
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
11
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
12
|
-
'ANTHROPIC_REASONING_MODEL',
|
|
13
|
-
];
|
|
14
7
|
function omitKeys(env, keys) {
|
|
15
8
|
return envMapSchema.parse(Object.fromEntries(Object.entries(env).filter(([key]) => !keys.includes(key))));
|
|
16
9
|
}
|
|
@@ -22,7 +15,7 @@ export function createInitCommand({ claudeSettingsEnvService, shellEnvService, h
|
|
|
22
15
|
}
|
|
23
16
|
const effectiveEnv = envMapSchema.parse(sources.reduce((acc, source) => ({ ...acc, ...source.env }), {}));
|
|
24
17
|
const keys = Object.keys(effectiveEnv).sort();
|
|
25
|
-
const requiredKeys =
|
|
18
|
+
const requiredKeys = requiredClaudeKeys.filter((key) => key in effectiveEnv);
|
|
26
19
|
const sourceFiles = sources.map((s) => s.path);
|
|
27
20
|
const result = await renderFlow({ keys, requiredKeys, yes, sourceFiles });
|
|
28
21
|
if (!result?.confirmed) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { requiredClaudeKeys } from '../../core/claude-required-keys.js';
|
|
4
5
|
import { CliError } from '../../core/errors.js';
|
|
5
6
|
import { ensureGitignoreEntry } from '../../core/gitignore.js';
|
|
7
|
+
import { envMapSchema } from '../../core/schema.js';
|
|
6
8
|
import { toProcessEnvMap } from '../../core/process-env.js';
|
|
7
9
|
export async function readEnvFile(filePath) {
|
|
8
10
|
try {
|
|
@@ -36,9 +38,44 @@ export async function readEnvFile(filePath) {
|
|
|
36
38
|
throw new CliError(`Failed to read env file: ${filePath}`, 2);
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
|
|
41
|
+
function getDetectedEnv(sources) {
|
|
42
|
+
return toProcessEnvMap(sources.reduce((acc, source) => ({ ...acc, ...source.env }), {}));
|
|
43
|
+
}
|
|
44
|
+
function omitKeys(env, keys) {
|
|
45
|
+
return envMapSchema.parse(Object.fromEntries(Object.entries(env).filter(([key]) => !keys.includes(key))));
|
|
46
|
+
}
|
|
47
|
+
function buildSourceBackups(sources, selectedKeys, selectedEnv) {
|
|
48
|
+
const backups = new Map();
|
|
49
|
+
for (const source of sources) {
|
|
50
|
+
backups.set(source.path, envMapSchema.parse({}));
|
|
51
|
+
}
|
|
52
|
+
for (const key of selectedKeys) {
|
|
53
|
+
for (const source of [...sources].reverse()) {
|
|
54
|
+
if (!(key in source.env)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (source.env[key] !== selectedEnv[key]) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const current = backups.get(source.path) ?? envMapSchema.parse({});
|
|
61
|
+
backups.set(source.path, envMapSchema.parse({
|
|
62
|
+
...current,
|
|
63
|
+
[key]: source.env[key],
|
|
64
|
+
}));
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return sources.map((source) => ({
|
|
69
|
+
file: source.path,
|
|
70
|
+
backup: backups.get(source.path) ?? envMapSchema.parse({}),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
export function createPresetCreateCommand({ presetService, projectEnvService, claudeSettingsEnvService, historyService, renderFlow, ensureGitignore = (dir, entry) => ensureGitignoreEntry(dir, entry), }) {
|
|
40
74
|
return async function createPreset({ cwd }) {
|
|
41
|
-
const
|
|
75
|
+
const sources = claudeSettingsEnvService ? await claudeSettingsEnvService.read() : [];
|
|
76
|
+
const detectedEnv = claudeSettingsEnvService ? getDetectedEnv(sources) : {};
|
|
77
|
+
const requiredKeys = requiredClaudeKeys.filter((key) => key in detectedEnv);
|
|
78
|
+
const result = await renderFlow({ detectedEnv, requiredKeys });
|
|
42
79
|
if (!result)
|
|
43
80
|
return;
|
|
44
81
|
const selectedEnv = {};
|
|
@@ -46,16 +83,40 @@ export function createPresetCreateCommand({ presetService, projectEnvService, re
|
|
|
46
83
|
selectedEnv[key] = result.env[key] ?? '';
|
|
47
84
|
}
|
|
48
85
|
const timestamp = new Date().toISOString();
|
|
86
|
+
const selectedKeys = result.selectedKeys;
|
|
87
|
+
const sourceBackups = result.source === 'detected'
|
|
88
|
+
? buildSourceBackups(sources, selectedKeys, selectedEnv)
|
|
89
|
+
: [];
|
|
49
90
|
if (result.destination === 'project') {
|
|
50
91
|
await projectEnvService.write(selectedEnv, { name: result.presetName, createdAt: timestamp, updatedAt: timestamp });
|
|
51
92
|
await ensureGitignore(cwd, '.cc-env');
|
|
52
|
-
return;
|
|
53
93
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
else {
|
|
95
|
+
await presetService.write({
|
|
96
|
+
name: result.presetName,
|
|
97
|
+
createdAt: timestamp,
|
|
98
|
+
updatedAt: timestamp,
|
|
99
|
+
env: selectedEnv,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (result.source === 'detected' && claudeSettingsEnvService && historyService) {
|
|
103
|
+
await historyService.write({
|
|
104
|
+
timestamp,
|
|
105
|
+
action: 'preset-create',
|
|
106
|
+
projectPath: cwd,
|
|
107
|
+
presetName: result.presetName,
|
|
108
|
+
destination: result.destination,
|
|
109
|
+
migratedKeys: selectedKeys,
|
|
110
|
+
sources: sourceBackups,
|
|
111
|
+
});
|
|
112
|
+
await claudeSettingsEnvService.write(sources.map((source) => ({
|
|
113
|
+
path: source.path,
|
|
114
|
+
env: omitKeys(source.env, Object.keys(sourceBackups.find((entry) => entry.file === source.path)?.backup ?? {})),
|
|
115
|
+
})));
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
presetName: result.presetName,
|
|
119
|
+
source: result.destination,
|
|
120
|
+
};
|
|
60
121
|
};
|
|
61
122
|
}
|
package/dist/commands/restore.js
CHANGED
|
@@ -2,9 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { CliError } from '../core/errors.js';
|
|
4
4
|
const h = React.createElement;
|
|
5
|
+
function isRestorableRecord(record) {
|
|
6
|
+
return record.action === 'init' || record.action === 'restore' || record.action === 'preset-create';
|
|
7
|
+
}
|
|
5
8
|
export function createRestoreCommand({ historyService, claudeSettingsEnvService, shellEnvService, settingsEnvService, presetService, renderFlow, renderEnvSummary, }) {
|
|
6
9
|
return async function restore({ yes = false } = {}) {
|
|
7
|
-
const records = await historyService.list();
|
|
10
|
+
const records = (await historyService.list()).filter(isRestorableRecord);
|
|
8
11
|
const result = await renderFlow({ records, yes });
|
|
9
12
|
if (!result?.confirmed) {
|
|
10
13
|
return;
|
|
@@ -33,6 +36,27 @@ export function createRestoreCommand({ historyService, claudeSettingsEnvService,
|
|
|
33
36
|
});
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
39
|
+
if (record.action === 'preset-create') {
|
|
40
|
+
const mergedBackup = Object.fromEntries(record.sources.flatMap((source) => Object.entries(source.backup)));
|
|
41
|
+
const current = await claudeSettingsEnvService.read();
|
|
42
|
+
await claudeSettingsEnvService.write(current.map((source) => ({
|
|
43
|
+
path: source.path,
|
|
44
|
+
env: {
|
|
45
|
+
...source.env,
|
|
46
|
+
...(record.sources.find((entry) => entry.file === source.path)?.backup ?? {}),
|
|
47
|
+
},
|
|
48
|
+
})));
|
|
49
|
+
await renderEnvSummary({
|
|
50
|
+
title: `Restored from detected preset ${record.presetName}`,
|
|
51
|
+
env: mergedBackup,
|
|
52
|
+
toFiles: record.sources.map((source) => source.file),
|
|
53
|
+
footer: h(Box, { flexDirection: 'column' }, h(Text, { color: 'green' }, 'Restore complete'), h(Text, { bold: true, color: 'green' }, 'Please restart your terminal for the restored environment variables to take effect.')),
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (record.action !== 'restore') {
|
|
58
|
+
throw new CliError('Restore record type is not supported');
|
|
59
|
+
}
|
|
36
60
|
if (result.targetType === 'settings') {
|
|
37
61
|
const currentSettings = await settingsEnvService.read();
|
|
38
62
|
await settingsEnvService.write({
|