@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 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('cc-env')
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 createRunCommand({
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(() => createPresetCreateCommand({
237
- presetService,
238
- projectEnvService,
239
- renderFlow: async () => {
240
- let result;
241
- const app = render(h(PresetCreateApp, {
242
- onSubmit: value => {
243
- result = value;
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
- program.parseAsync(process.argv).catch((error) => {
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 "cc-env --help" to see available commands and options.\n`;
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 `# cc-env bash completion
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 `# cc-env zsh completion
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 lines = cmds.map(([cmd, desc]) => `complete -c cc-env -f -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`);
50
- return `# cc-env fish completion
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
- ${lines.join('\n')}
53
- complete -c cc-env -l help -d 'Show help'
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
  }
@@ -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 : 'not initialized — run: cc-env init' };
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)
@@ -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 = requiredInitKeys.filter((key) => key in effectiveEnv);
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
- export function createPresetCreateCommand({ presetService, projectEnvService, renderFlow, ensureGitignore = (dir, entry) => ensureGitignoreEntry(dir, entry), }) {
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 result = await renderFlow();
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
- await presetService.write({
55
- name: result.presetName,
56
- createdAt: timestamp,
57
- updatedAt: timestamp,
58
- env: selectedEnv,
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
  }
@@ -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({