@lkangd/cc-env 1.2.1 → 1.3.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.
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,7 +18,6 @@ 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';
@@ -54,8 +52,80 @@ const shellEnvService = createShellEnvService({ homeDir });
54
52
  const projectEnvService = createProjectEnvService({ cwd });
55
53
  const presetService = createPresetService(globalRoot);
56
54
  const historyService = createHistoryService(globalRoot);
55
+ const projectStateService = createProjectStateService(globalRoot);
56
+ async function runPresetCreateFlow({ detectedEnv, requiredKeys }) {
57
+ let result;
58
+ const app = render(h(PresetCreateApp, {
59
+ onSubmit: value => {
60
+ result = value;
61
+ },
62
+ readFile: async (filePath) => {
63
+ const { readEnvFile } = await import('./commands/preset/create.js');
64
+ return readEnvFile(filePath);
65
+ },
66
+ globalPresetPath: name => presetService.getPath(name),
67
+ projectEnvPath: join(cwd, '.cc-env', 'env.json'),
68
+ detectedEnv,
69
+ requiredKeys,
70
+ }));
71
+ await app.waitUntilExit();
72
+ return result;
73
+ }
74
+ async function runWithBootstrap({ args = [], dryRun = false, yes = false, json = false, skipDetect = false, }) {
75
+ const result = await createRunCommand({
76
+ claudeSettingsEnvService,
77
+ presetService,
78
+ projectEnvService,
79
+ projectStateService,
80
+ findClaude: findClaudeExecutable,
81
+ renderPresetSelect: async ({ presets, defaultIndex }) => {
82
+ let selected;
83
+ const app = render(h(RunPresetSelectApp, {
84
+ presets,
85
+ defaultIndex,
86
+ onSubmit: preset => {
87
+ selected = preset;
88
+ }
89
+ }));
90
+ await app.waitUntilExit();
91
+ return selected;
92
+ },
93
+ spawnCommand
94
+ })({
95
+ args,
96
+ dryRun,
97
+ yes,
98
+ json,
99
+ skipDetect,
100
+ cwd,
101
+ });
102
+ if (!result || result.status === 'executed') {
103
+ return;
104
+ }
105
+ if (Object.keys(result.detectedEnv).length === 0) {
106
+ throw new CliError('No presets found and no migratable Claude settings were detected.');
107
+ }
108
+ const createdPreset = await createPresetCreateCommand({
109
+ presetService,
110
+ projectEnvService,
111
+ claudeSettingsEnvService,
112
+ historyService,
113
+ renderFlow: runPresetCreateFlow,
114
+ })({ cwd });
115
+ if (!createdPreset) {
116
+ return;
117
+ }
118
+ await projectStateService.saveLastPreset(cwd, createdPreset);
119
+ await runWithBootstrap({
120
+ args,
121
+ dryRun,
122
+ yes: true,
123
+ json,
124
+ skipDetect: true,
125
+ });
126
+ }
57
127
  async function runRestoreFlow(context) {
58
- const state = createRestoreFlowState(context.records);
128
+ const state = createRestoreFlowState(context.records, cwd);
59
129
  const firstRecord = context.records[0];
60
130
  if (!firstRecord) {
61
131
  render(h(RestoreApp, { state }));
@@ -66,16 +136,19 @@ async function runRestoreFlow(context) {
66
136
  type: 'select-record',
67
137
  timestamp: firstRecord.timestamp
68
138
  });
69
- if (firstRecord.action === 'init') {
139
+ if (firstRecord.action === 'init' || firstRecord.action === 'preset-create') {
70
140
  const doneState = advanceRestoreFlow(selectedRecordState, { type: 'confirm' });
71
141
  if (doneState.step !== 'done') {
72
142
  return undefined;
73
143
  }
74
144
  return {
75
145
  confirmed: true,
76
- timestamp: firstRecord.timestamp
146
+ timestamp: firstRecord.timestamp,
77
147
  };
78
148
  }
149
+ if (firstRecord.action !== 'restore') {
150
+ return undefined;
151
+ }
79
152
  const confirmState = advanceRestoreFlow(selectedRecordState, {
80
153
  type: 'select-target',
81
154
  targetType: firstRecord.targetType,
@@ -94,7 +167,7 @@ async function runRestoreFlow(context) {
94
167
  return {
95
168
  confirmed: true,
96
169
  timestamp: doneState.selectedTimestamp,
97
- targetType: doneState.targetType
170
+ ...(doneState.targetType ? { targetType: doneState.targetType } : {}),
98
171
  };
99
172
  }
100
173
  return undefined;
@@ -125,62 +198,13 @@ program
125
198
  .option('--json', 'Output as JSON (only with --dry-run)')
126
199
  .action((args, options) => {
127
200
  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
- })({
201
+ return runWithBootstrap({
148
202
  args: rawArgs,
149
203
  dryRun: options.dryRun ?? false,
150
204
  yes: options.yes ?? false,
151
205
  json: options.json ?? false,
152
- cwd
153
206
  });
154
207
  });
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
208
  program
185
209
  .command('restore')
186
210
  .description('Restore environment variables from a previous snapshot')
@@ -233,26 +257,15 @@ program
233
257
  program
234
258
  .command('create')
235
259
  .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 }));
260
+ .action(async () => {
261
+ await createPresetCreateCommand({
262
+ presetService,
263
+ projectEnvService,
264
+ claudeSettingsEnvService,
265
+ historyService,
266
+ renderFlow: runPresetCreateFlow,
267
+ })({ cwd });
268
+ });
256
269
  program
257
270
  .command('doctor')
258
271
  .description('Check system health and configuration')
@@ -306,7 +319,22 @@ program.hook('preAction', (thisCommand) => {
306
319
  thisCommand.setOptionValue('yes', true);
307
320
  }
308
321
  });
309
- program.parseAsync(process.argv).catch((error) => {
322
+ async function main() {
323
+ const args = process.argv.slice(2);
324
+ if (args.length === 0) {
325
+ const hasGlobalPreset = (await presetService.listNames()).length > 0;
326
+ const { env: projectEnv } = await projectEnvService.readWithMeta();
327
+ if (hasGlobalPreset || Object.keys(projectEnv).length > 0) {
328
+ await runWithBootstrap({ args: [], yes: !process.stdin.isTTY });
329
+ return;
330
+ }
331
+ program.outputHelp();
332
+ process.exitCode = 0;
333
+ return;
334
+ }
335
+ await program.parseAsync(process.argv);
336
+ }
337
+ main().catch((error) => {
310
338
  if (error instanceof CliError) {
311
339
  process.stderr.write(`\n Error: ${error.message}\n\n`);
312
340
  process.exitCode = error.exitCode;
@@ -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({
@@ -1,38 +1,42 @@
1
- import { CliError } from '../core/errors.js';
2
1
  import { formatRunEnvBlock } from '../core/format.js';
3
- const requiredInitKeys = [
4
- 'ANTHROPIC_AUTH_TOKEN',
5
- 'ANTHROPIC_BASE_URL',
6
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
7
- 'ANTHROPIC_DEFAULT_OPUS_MODEL',
8
- 'ANTHROPIC_DEFAULT_SONNET_MODEL',
9
- 'ANTHROPIC_REASONING_MODEL',
10
- ];
11
- export function createRunCommand({ claudeSettingsEnvService, presetService, projectEnvService, projectStateService, findClaude, renderPresetSelect, spawnCommand, stdout = process.stdout, }) {
12
- return async function run({ args = [], dryRun = false, yes = false, json = false, cwd, }) {
13
- // Step 0: Check settings files for init-managed keys
2
+ import { requiredClaudeKeys } from '../core/claude-required-keys.js';
3
+ const detectTriggerKeys = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'];
4
+ function getDetectedEnv(sources) {
5
+ return sources.reduce((acc, source) => ({ ...acc, ...source.env }), {});
6
+ }
7
+ export function createRunCommand({ claudeSettingsEnvService, presetService, projectEnvService, projectStateService, findClaude, renderPresetSelect, spawnCommand, stdout = process.stdout }) {
8
+ return async function run({ args = [], dryRun = false, yes = false, json = false, skipDetect = false, cwd }) {
14
9
  const sources = await claudeSettingsEnvService.read();
15
- const mergedSettingsEnv = sources.reduce((acc, s) => ({ ...acc, ...s.env }), {});
16
- const staleKeys = requiredInitKeys.filter((k) => k in mergedSettingsEnv);
17
- if (staleKeys.length > 0) {
18
- throw new CliError(`Found init-managed keys in Claude settings:\n\n ${staleKeys.join(', \n ')}. \n\n Run "cc-env init" first.`);
10
+ const detectedEnv = getDetectedEnv(sources);
11
+ const requiredKeys = requiredClaudeKeys.filter((key) => key in detectedEnv);
12
+ const hasDetectTrigger = detectTriggerKeys.some((key) => key in detectedEnv);
13
+ if (!skipDetect && hasDetectTrigger) {
14
+ return {
15
+ status: 'needs-preset',
16
+ detectedEnv,
17
+ requiredKeys,
18
+ };
19
19
  }
20
20
  // Step 1: Collect all presets (project + global)
21
21
  const names = await presetService.listNames();
22
- const globalPresets = await Promise.all(names.map((name) => presetService.read(name).then((p) => ({ name, env: p.env, source: 'global' }))));
22
+ const globalPresets = await Promise.all(names.map(name => presetService.read(name).then(p => ({ name, env: p.env, source: 'global' }))));
23
23
  const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta();
24
24
  const projectPreset = Object.keys(projectEnv).length > 0
25
25
  ? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' }]
26
26
  : [];
27
27
  const presets = [...projectPreset, ...globalPresets];
28
28
  if (presets.length === 0) {
29
- throw new CliError('No presets found. Create one with "cc-env preset create".');
29
+ return {
30
+ status: 'needs-preset',
31
+ detectedEnv,
32
+ requiredKeys,
33
+ };
30
34
  }
31
35
  // Step 2: Determine default selection
32
36
  const savedRef = await projectStateService.getLastPreset(cwd);
33
37
  let defaultIndex = 0;
34
38
  if (savedRef) {
35
- const idx = presets.findIndex((p) => p.name === savedRef.presetName && p.source === savedRef.source);
39
+ const idx = presets.findIndex(p => p.name === savedRef.presetName && p.source === savedRef.source);
36
40
  if (idx >= 0)
37
41
  defaultIndex = idx;
38
42
  }
@@ -52,7 +56,7 @@ export function createRunCommand({ claudeSettingsEnvService, presetService, proj
52
56
  // Step 4: Save selection
53
57
  await projectStateService.saveLastPreset(cwd, {
54
58
  presetName: selected.name,
55
- source: selected.source,
59
+ source: selected.source
56
60
  });
57
61
  // Step 5: Resolve claude command
58
62
  let command;
@@ -72,7 +76,7 @@ export function createRunCommand({ claudeSettingsEnvService, presetService, proj
72
76
  command: [command, ...claudeArgs],
73
77
  env: selected.env
74
78
  }, null, 2) + '\n');
75
- return;
79
+ return { status: 'executed' };
76
80
  }
77
81
  const presetKeys = new Set(Object.keys(selected.env));
78
82
  const envBlock = formatRunEnvBlock(selected.env, presetKeys);
@@ -80,9 +84,10 @@ export function createRunCommand({ claudeSettingsEnvService, presetService, proj
80
84
  if (dryRun) {
81
85
  const preview = [command, ...claudeArgs].join(' ');
82
86
  stdout.write(`Would run: ${preview}\n`);
83
- return;
87
+ return { status: 'executed' };
84
88
  }
85
89
  // Step 7: Spawn
86
90
  await spawnCommand(command, claudeArgs, { ...process.env, ...selected.env });
91
+ return { status: 'executed' };
87
92
  };
88
93
  }
@@ -0,0 +1,8 @@
1
+ export const requiredClaudeKeys = [
2
+ 'ANTHROPIC_AUTH_TOKEN',
3
+ 'ANTHROPIC_BASE_URL',
4
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
5
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
6
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
7
+ 'ANTHROPIC_REASONING_MODEL',
8
+ ];
@@ -32,7 +32,17 @@ const restoreHistorySchema = z.object({
32
32
  targetType: z.enum(['settings', 'preset']),
33
33
  targetName: z.string(),
34
34
  });
35
+ const presetCreateHistorySchema = z.object({
36
+ timestamp: z.string().datetime({ offset: true }),
37
+ action: z.literal('preset-create'),
38
+ projectPath: z.string(),
39
+ presetName: z.string(),
40
+ destination: z.enum(['global', 'project']),
41
+ migratedKeys: z.array(envKeySchema),
42
+ sources: z.array(sourceEntrySchema),
43
+ });
35
44
  export const historySchema = z.discriminatedUnion('action', [
36
45
  initHistorySchema,
37
46
  restoreHistorySchema,
47
+ presetCreateHistorySchema,
38
48
  ]);
@@ -1,14 +1,62 @@
1
- export function createPresetCreateFlowState() {
1
+ export function createPresetCreateFlowState(input) {
2
+ const detectedEnv = input?.detectedEnv ?? {};
3
+ const requiredKeys = input?.requiredKeys ?? [];
4
+ const detectedKeys = Object.keys(detectedEnv).sort();
5
+ const selectedKeys = requiredKeys.filter((key) => key in detectedEnv);
6
+ if (detectedKeys.length > 0) {
7
+ return {
8
+ step: 'detectedPrompt',
9
+ env: detectedEnv,
10
+ allKeys: detectedKeys,
11
+ selectedKeys,
12
+ requiredKeys: selectedKeys,
13
+ presetName: '',
14
+ };
15
+ }
2
16
  return {
3
17
  step: 'source',
4
18
  env: {},
5
19
  allKeys: [],
6
20
  selectedKeys: [],
21
+ requiredKeys: [],
7
22
  presetName: '',
8
23
  };
9
24
  }
10
25
  export function advancePresetCreateFlow(state, action) {
11
26
  switch (state.step) {
27
+ case 'detectedPrompt':
28
+ if (action.type === 'accept-detected-prompt') {
29
+ return {
30
+ ...state,
31
+ step: 'detected',
32
+ };
33
+ }
34
+ if (action.type === 'reject-detected-prompt') {
35
+ const { source: _source, ...rest } = state;
36
+ return {
37
+ ...rest,
38
+ step: 'source',
39
+ };
40
+ }
41
+ return state;
42
+ case 'detected':
43
+ if (action.type === 'toggle-detected-key') {
44
+ if (state.requiredKeys.includes(action.key) || !state.allKeys.includes(action.key)) {
45
+ return state;
46
+ }
47
+ const selectedKeys = state.selectedKeys.includes(action.key)
48
+ ? state.selectedKeys.filter((key) => key !== action.key)
49
+ : [...state.selectedKeys, action.key].sort();
50
+ return { ...state, selectedKeys };
51
+ }
52
+ if (action.type === 'confirm-detected-keys') {
53
+ return {
54
+ ...state,
55
+ step: 'name',
56
+ source: 'detected',
57
+ };
58
+ }
59
+ return state;
12
60
  case 'source':
13
61
  if (action.type !== 'select-source')
14
62
  return state;
@@ -1,7 +1,26 @@
1
- export function createRestoreFlowState(records) {
1
+ export function createRestoreFlowState(records, cwd) {
2
+ const currentProjectRecords = records
3
+ .filter((record) => 'projectPath' in record && record.projectPath === cwd)
4
+ .sort((left, right) => right.timestamp.localeCompare(left.timestamp));
5
+ const otherHistoryRecords = records
6
+ .filter((record) => !('projectPath' in record) || record.projectPath !== cwd)
7
+ .sort((left, right) => right.timestamp.localeCompare(left.timestamp));
8
+ const orderedRecords = [...currentProjectRecords, ...otherHistoryRecords];
9
+ const groups = [];
10
+ if (currentProjectRecords.length > 0) {
11
+ groups.push({ title: 'Current project', start: 0, end: currentProjectRecords.length });
12
+ }
13
+ if (otherHistoryRecords.length > 0) {
14
+ groups.push({
15
+ title: 'Other history',
16
+ start: currentProjectRecords.length,
17
+ end: orderedRecords.length,
18
+ });
19
+ }
2
20
  return {
3
21
  step: 'record',
4
- records,
22
+ records: orderedRecords,
23
+ groups,
5
24
  };
6
25
  }
7
26
  export function advanceRestoreFlow(state, action) {
@@ -14,7 +33,7 @@ export function advanceRestoreFlow(state, action) {
14
33
  if (!selectedRecord) {
15
34
  return state;
16
35
  }
17
- if (selectedRecord.action === 'init') {
36
+ if (selectedRecord.action === 'init' || selectedRecord.action === 'preset-create') {
18
37
  return {
19
38
  ...state,
20
39
  step: 'confirm',
@@ -41,19 +60,18 @@ export function advanceRestoreFlow(state, action) {
41
60
  targetType: 'settings',
42
61
  };
43
62
  }
44
- const targetName = action.targetName;
45
63
  return {
46
64
  ...state,
47
65
  step: 'confirm',
48
66
  targetType: 'preset',
49
- targetName,
67
+ targetName: action.targetName,
50
68
  };
51
- case 'confirm':
69
+ case 'confirm': {
52
70
  if (action.type !== 'confirm' || !state.selectedTimestamp) {
53
71
  return state;
54
72
  }
55
73
  const selectedRecord = state.records.find((record) => record.timestamp === state.selectedTimestamp);
56
- if (selectedRecord?.action === 'init') {
74
+ if (selectedRecord?.action === 'init' || selectedRecord?.action === 'preset-create') {
57
75
  return {
58
76
  ...state,
59
77
  step: 'done',
@@ -69,6 +87,7 @@ export function advanceRestoreFlow(state, action) {
69
87
  ...state,
70
88
  step: 'done',
71
89
  };
90
+ }
72
91
  case 'done':
73
92
  return state;
74
93
  }
@@ -3,6 +3,17 @@ import { useState } from 'react';
3
3
  import { Box, Text, useApp, useInput } from 'ink';
4
4
  import { advancePresetCreateFlow, createPresetCreateFlowState, } from '../flows/preset-create-flow.js';
5
5
  import { EnvSummary } from './summary.js';
6
+ function DetectedPromptStep({ cursor }) {
7
+ const options = ['Generate from detected config', 'Choose another source'];
8
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Detected existing configuration" }), _jsx(Text, { dimColor: true, children: "Use the currently detected settings to generate a preset?" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((label, i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(i === cursor ? { color: 'cyan' } : {}), children: label })] }, label))) })] }));
9
+ }
10
+ function DetectedKeysStep({ keys, selectedKeys, requiredKeys, cursor, }) {
11
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select detected env keys to migrate" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle optional keys \u00B7 enter confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
12
+ const isSelected = selectedKeys.includes(key);
13
+ const isRequired = requiredKeys.includes(key);
14
+ return (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: isSelected ? '[x]' : '[ ]' }), _jsx(Text, { children: isRequired ? ' ! ' : ' ' }), _jsxs(Text, { children: [" ", key] })] }, key));
15
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "! required key \u00B7 q cancel" }) })] }));
16
+ }
6
17
  function SourceStep({ cursor }) {
7
18
  const options = [
8
19
  { label: 'File import', value: 'file' },
@@ -32,9 +43,11 @@ function DestinationStep({ cursor }) {
32
43
  ];
33
44
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select save destination" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(i === cursor ? { color: 'cyan' } : {}), children: opt.label })] }, opt.value))) })] }));
34
45
  }
35
- export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectEnvPath, }) {
46
+ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectEnvPath, detectedEnv, requiredKeys, }) {
36
47
  const { exit } = useApp();
37
- const [state, setState] = useState(createPresetCreateFlowState);
48
+ const [state, setState] = useState(() => createPresetCreateFlowState(detectedEnv
49
+ ? (requiredKeys ? { detectedEnv, requiredKeys } : { detectedEnv })
50
+ : undefined));
38
51
  const [textInput, setTextInput] = useState('');
39
52
  const [listCursor, setListCursor] = useState(0);
40
53
  const [allKeys, setAllKeys] = useState([]);
@@ -44,6 +57,58 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
44
57
  exit();
45
58
  return;
46
59
  }
60
+ if (state.step === 'detectedPrompt') {
61
+ if (input === 'q') {
62
+ exit();
63
+ return;
64
+ }
65
+ if (key.upArrow || input === 'k') {
66
+ setListCursor((c) => Math.max(0, c - 1));
67
+ return;
68
+ }
69
+ if (key.downArrow || input === 'j') {
70
+ setListCursor((c) => Math.min(1, c + 1));
71
+ return;
72
+ }
73
+ if (key.return) {
74
+ setState((s) => advancePresetCreateFlow(s, listCursor === 0
75
+ ? { type: 'accept-detected-prompt' }
76
+ : { type: 'reject-detected-prompt' }));
77
+ setListCursor(0);
78
+ setTextInput('');
79
+ return;
80
+ }
81
+ }
82
+ if (state.step === 'detected') {
83
+ if (input === 'q') {
84
+ exit();
85
+ return;
86
+ }
87
+ if (key.upArrow || input === 'k') {
88
+ setListCursor((c) => Math.max(0, c - 1));
89
+ return;
90
+ }
91
+ if (key.downArrow || input === 'j') {
92
+ setListCursor((c) => Math.min(state.allKeys.length - 1, c + 1));
93
+ return;
94
+ }
95
+ if (input === ' ') {
96
+ const targetKey = state.allKeys[listCursor];
97
+ if (targetKey) {
98
+ setState((s) => advancePresetCreateFlow(s, {
99
+ type: 'toggle-detected-key',
100
+ key: targetKey,
101
+ }));
102
+ }
103
+ return;
104
+ }
105
+ if (key.return) {
106
+ setState((s) => advancePresetCreateFlow(s, { type: 'confirm-detected-keys' }));
107
+ setListCursor(0);
108
+ setTextInput('');
109
+ return;
110
+ }
111
+ }
47
112
  if (state.step === 'source') {
48
113
  if (input === 'q') {
49
114
  exit();
@@ -261,7 +326,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
261
326
  if (state.step === 'done') {
262
327
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "Done" }) }));
263
328
  }
264
- return (_jsxs(Box, { flexDirection: "column", children: [state.step === 'source' && _jsx(SourceStep, { cursor: listCursor }), state.step === 'filePath' && (_jsx(FilePathStep, { value: textInput, ...(state.error ? { error: state.error } : {}) })), state.step === 'keys' && (_jsx(KeysStep, { keys: allKeys, selectedKeys: state.selectedKeys, cursor: listCursor })), state.step === 'manualInput' && (_jsx(ManualInputStep, { entries: state.selectedKeys.map((k) => [k, state.env[k] ?? '']), value: textInput, ...(state.error ? { error: state.error } : {}) })), state.step === 'name' && _jsx(NameStep, { value: textInput }), state.step === 'destination' && _jsx(DestinationStep, { cursor: listCursor }), state.step === 'confirm' && state.destination ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(EnvSummary, { title: `Preset: ${state.presetName}`, entries: Object.entries(state.env)
329
+ return (_jsxs(Box, { flexDirection: "column", children: [state.step === 'detectedPrompt' && _jsx(DetectedPromptStep, { cursor: listCursor }), state.step === 'detected' && (_jsx(DetectedKeysStep, { keys: state.allKeys, selectedKeys: state.selectedKeys, requiredKeys: state.requiredKeys, cursor: listCursor })), state.step === 'source' && _jsx(SourceStep, { cursor: listCursor }), state.step === 'filePath' && (_jsx(FilePathStep, { value: textInput, ...(state.error ? { error: state.error } : {}) })), state.step === 'keys' && (_jsx(KeysStep, { keys: allKeys, selectedKeys: state.selectedKeys, cursor: listCursor })), state.step === 'manualInput' && (_jsx(ManualInputStep, { entries: state.selectedKeys.map((k) => [k, state.env[k] ?? '']), value: textInput, ...(state.error ? { error: state.error } : {}) })), state.step === 'name' && _jsx(NameStep, { value: textInput }), state.step === 'destination' && _jsx(DestinationStep, { cursor: listCursor }), state.step === 'confirm' && state.destination ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(EnvSummary, { title: `Preset: ${state.presetName}`, entries: Object.entries(state.env)
265
330
  .filter(([k]) => state.selectedKeys.includes(k))
266
331
  .sort(([a], [b]) => a.localeCompare(b)), mask: true, ...(state.filePath ? { fromFiles: [state.filePath] } : {}), toFiles: [
267
332
  state.destination === 'global'
@@ -3,6 +3,21 @@ import { useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Text, useApp, useInput } from 'ink';
4
4
  import { advanceRestoreFlow } from '../flows/restore-flow.js';
5
5
  import { EnvEntries, EnvSummary } from './summary.js';
6
+ export function getRestorePreviewSections(record) {
7
+ if (!record) {
8
+ return [];
9
+ }
10
+ if (record.action === 'init' || record.action === 'preset-create') {
11
+ return record.sources
12
+ .filter((source) => Object.keys(source.backup).length > 0)
13
+ .map((source) => ({
14
+ file: source.file,
15
+ entries: Object.entries(source.backup)
16
+ .sort(([left], [right]) => left.localeCompare(right)),
17
+ }));
18
+ }
19
+ return [];
20
+ }
6
21
  export function RestoreApp({ state, onSubmit, }) {
7
22
  const { exit } = useApp();
8
23
  const [currentState, setCurrentState] = useState(state);
@@ -13,9 +28,11 @@ export function RestoreApp({ state, onSubmit, }) {
13
28
  ? recordAtCursor
14
29
  : selectedRecord ?? currentState.records[0];
15
30
  const restoreEntries = useMemo(() => activeRecord
16
- ? Object.entries(activeRecord.action === 'init'
31
+ ? Object.entries(activeRecord.action === 'init' || activeRecord.action === 'preset-create'
17
32
  ? Object.fromEntries(activeRecord.sources.flatMap((s) => Object.entries(s.backup)))
18
- : activeRecord.backup).sort(([left], [right]) => left.localeCompare(right))
33
+ : activeRecord.action === 'restore'
34
+ ? activeRecord.backup
35
+ : {}).sort(([left], [right]) => left.localeCompare(right))
19
36
  : [], [activeRecord]);
20
37
  const fromFiles = useMemo(() => {
21
38
  if (!activeRecord || activeRecord.action !== 'init') {
@@ -23,12 +40,7 @@ export function RestoreApp({ state, onSubmit, }) {
23
40
  }
24
41
  return activeRecord.shellWrites.map((sw) => sw.filePath);
25
42
  }, [activeRecord]);
26
- const toFiles = useMemo(() => {
27
- if (!activeRecord || activeRecord.action !== 'init') {
28
- return [];
29
- }
30
- return activeRecord.sources.map((s) => s.file);
31
- }, [activeRecord]);
43
+ const previewSections = useMemo(() => getRestorePreviewSections(activeRecord), [activeRecord]);
32
44
  useEffect(() => {
33
45
  setCurrentState(state);
34
46
  setCursor(0);
@@ -96,7 +108,10 @@ export function RestoreApp({ state, onSubmit, }) {
96
108
  exit();
97
109
  }
98
110
  });
99
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Restore record" }), currentState.step === 'record' ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "History" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: currentState.records.map((record, index) => (_jsxs(Text, { children: [index === cursor ? '❯ ' : ' ', record.timestamp] }, record.timestamp))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Preview" }), activeRecord?.action === 'init' ? (_jsxs(Box, { flexDirection: "column", children: [fromFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "From:" }), fromFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null, toFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "To:" }), toFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null] })) : (_jsxs(Text, { dimColor: true, children: ["Restore to ", activeRecord?.targetType === 'preset' ? `preset ${activeRecord.targetName}` : activeRecord?.targetType ?? 'settings'] })), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: restoreEntries }) })] })] })] })) : null, currentState.step === 'target' ? (_jsxs(Text, { children: ["Select target for ", selectedRecord?.timestamp ?? 'record', ": settings or preset"] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'init' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp })] }), _jsx(EnvSummary, { title: "Will restore", entries: restoreEntries, ...(fromFiles.length > 0 ? { fromFiles } : {}), ...(toFiles.length > 0 ? { toFiles } : {}) })] })) : null, currentState.step === 'confirm' && selectedRecord?.action !== 'init' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord?.timestamp ?? 'record' }), " to", ' ', _jsx(Text, { color: "green", children: currentState.targetType === 'preset'
111
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Restore record" }), currentState.step === 'record' ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "History" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: currentState.groups.map((group) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: group.title }), currentState.records.slice(group.start, group.end).map((record, index) => {
112
+ const actualIndex = group.start + index;
113
+ return (_jsxs(Text, { children: [actualIndex === cursor ? '❯ ' : ' ', record.timestamp] }, record.timestamp));
114
+ })] }, group.title))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Preview" }), activeRecord?.action === 'restore' ? (_jsxs(Text, { dimColor: true, children: ["Restore to ", activeRecord.targetType === 'preset' ? `preset ${activeRecord.targetName}` : activeRecord.targetType] })) : null, previewSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: section.file }), _jsx(EnvEntries, { entries: section.entries })] }, section.file))), previewSections.length === 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: restoreEntries }) })) : null] })] })] })) : null, currentState.step === 'target' ? (_jsxs(Text, { children: ["Select target for ", selectedRecord?.timestamp ?? 'record', ": settings or preset"] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'init' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp })] }), _jsx(EnvSummary, { title: "Will restore", entries: restoreEntries, ...(fromFiles.length > 0 ? { fromFiles } : {}), toFiles: previewSections.map((section) => section.file) })] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'preset-create' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp })] }), previewSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: section.file }), _jsx(EnvEntries, { entries: section.entries })] }, section.file)))] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'restore' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp }), " to", ' ', _jsx(Text, { color: "green", children: currentState.targetType === 'preset'
100
115
  ? `preset ${currentState.targetName}`
101
116
  : currentState.targetType ?? 'settings' })] }), _jsx(EnvSummary, { title: "Will restore", entries: restoreEntries })] })) : null, currentState.step === 'done' ? (_jsxs(Text, { color: "green", children: ['\n', "Restore complete"] })) : null, currentState.step !== 'done' ? (_jsx(Text, { children: "Press Enter to confirm or q to cancel" })) : null] }));
102
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lkangd/cc-env",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Manage runtime environment variables for Claude Code",
5
5
  "homepage": "https://github.com/lkangd/cc-env#readme",
6
6
  "bugs": {
@@ -1,54 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { Box, Text, useApp, useInput } from 'ink';
4
- import { advanceInitFlow, createInitFlowState, } from '../flows/init-flow.js';
5
- export function InitApp({ keys = [], requiredKeys = [], sourceFiles = [], onSubmit, }) {
6
- const { exit } = useApp();
7
- const [cursor, setCursor] = useState(0);
8
- const [flowState, setFlowState] = useState(() => createInitFlowState(keys, requiredKeys));
9
- useEffect(() => {
10
- if (!onSubmit) {
11
- return;
12
- }
13
- if (keys.length === 0) {
14
- onSubmit({ confirmed: false, selectedKeys: [] });
15
- exit();
16
- }
17
- }, [exit, keys.length, onSubmit]);
18
- useInput((input, key) => {
19
- if (!onSubmit) {
20
- return;
21
- }
22
- if (key.upArrow || input === 'k') {
23
- setCursor((c) => Math.max(0, c - 1));
24
- return;
25
- }
26
- if (key.downArrow || input === 'j') {
27
- setCursor((c) => Math.min(keys.length - 1, c + 1));
28
- return;
29
- }
30
- if (input === ' ') {
31
- const targetKey = keys[cursor];
32
- if (targetKey) {
33
- setFlowState((prev) => advanceInitFlow(prev, { type: 'toggle-key', key: targetKey }));
34
- }
35
- return;
36
- }
37
- if (key.return) {
38
- onSubmit({ confirmed: true, selectedKeys: flowState.selectedKeys });
39
- exit();
40
- return;
41
- }
42
- if (key.escape || input.toLowerCase() === 'q') {
43
- onSubmit({ confirmed: false, selectedKeys: [] });
44
- exit();
45
- }
46
- });
47
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env keys to migrate into managed shell config" }), sourceFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Source:" }), sourceFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null, _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle \u00B7 enter confirm \u00B7 q cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
48
- const isRequired = requiredKeys.includes(key);
49
- const isSelected = flowState.selectedKeys.includes(key);
50
- const isCursor = i === cursor;
51
- const checkbox = isSelected ? '[x]' : '[ ]';
52
- return (_jsxs(Box, { children: [_jsx(Text, { children: isCursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: checkbox }), _jsxs(Text, { children: [" ", key] }), isRequired ? _jsx(Text, { dimColor: true, children: " (required)" }) : null] }, key));
53
- }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [flowState.selectedKeys.length, " of ", keys.length, " selected"] }) })] }));
54
- }