@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.
@@ -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
+ ];
@@ -0,0 +1,8 @@
1
+ import { basename } from 'node:path';
2
+ const ALIASES = new Set(['cc-env', 'ccenv']);
3
+ export function getCliName() {
4
+ const name = basename(process.argv[1] ?? '');
5
+ if (ALIASES.has(name))
6
+ return name;
7
+ return 'cc-env';
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 (selectedKeys.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
  }
@@ -0,0 +1,7 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function TextInputDisplay({ value, cursorPos }) {
4
+ const before = value.slice(0, cursorPos);
5
+ const after = value.slice(cursorPos);
6
+ return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ['>', " "] }), _jsx(Text, { color: "cyan", children: before }), _jsx(Text, { dimColor: true, children: "\u2588" }), _jsx(Text, { color: "cyan", children: after })] }));
7
+ }
@@ -0,0 +1,71 @@
1
+ import { useState } from 'react';
2
+ export function handleKey(state, input, key, setState) {
3
+ const { value, cursorPos } = state;
4
+ if (key.leftArrow) {
5
+ if (cursorPos > 0)
6
+ setState(value, cursorPos - 1);
7
+ return true;
8
+ }
9
+ if (key.rightArrow) {
10
+ if (cursorPos < value.length)
11
+ setState(value, cursorPos + 1);
12
+ return true;
13
+ }
14
+ if (key.home) {
15
+ setState(value, 0);
16
+ return true;
17
+ }
18
+ if (key.end) {
19
+ setState(value, value.length);
20
+ return true;
21
+ }
22
+ if (key.ctrl && input === 'a') {
23
+ setState(value, 0);
24
+ return true;
25
+ }
26
+ if (key.ctrl && input === 'e') {
27
+ setState(value, value.length);
28
+ return true;
29
+ }
30
+ if (key.ctrl && input === 'u') {
31
+ setState(value.slice(cursorPos), 0);
32
+ return true;
33
+ }
34
+ if (key.ctrl && input === 'k') {
35
+ setState(value.slice(0, cursorPos), cursorPos);
36
+ return true;
37
+ }
38
+ if ((key.ctrl && (key.backspace || key.delete)) || (key.meta && (key.backspace || key.delete))) {
39
+ if (cursorPos > 0) {
40
+ setState(value.slice(cursorPos), 0);
41
+ }
42
+ return true;
43
+ }
44
+ if (key.backspace || key.delete) {
45
+ if (cursorPos > 0) {
46
+ setState(value.slice(0, cursorPos - 1) + value.slice(cursorPos), cursorPos - 1);
47
+ }
48
+ return true;
49
+ }
50
+ if (input && !key.ctrl && !key.meta) {
51
+ setState(value.slice(0, cursorPos) + input + value.slice(cursorPos), cursorPos + 1);
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+ export function useTextInput() {
57
+ const [value, setValue] = useState('');
58
+ const [cursorPos, setCursorPos] = useState(0);
59
+ const setState = (newValue, newCursor) => {
60
+ setValue(newValue);
61
+ setCursorPos(newCursor);
62
+ };
63
+ const onKey = (input, key) => {
64
+ return handleKey({ value, cursorPos }, input, key, setState);
65
+ };
66
+ const reset = (newValue = '') => {
67
+ setValue(newValue);
68
+ setCursorPos(newValue.length);
69
+ };
70
+ return { value, cursorPos, handleKey: onKey, setValue, reset };
71
+ }
@@ -1,8 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useApp, useInput } from 'ink';
4
+ import { useTextInput } from './hooks/use-text-input.js';
5
+ import { TextInputDisplay } from './components/text-input.js';
4
6
  import { advancePresetCreateFlow, createPresetCreateFlowState, } from '../flows/preset-create-flow.js';
5
7
  import { EnvSummary } from './summary.js';
8
+ function DetectedPromptStep({ cursor }) {
9
+ const options = ['Generate from detected config', 'Choose another source'];
10
+ 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))) })] }));
11
+ }
12
+ function DetectedKeysStep({ keys, selectedKeys, requiredKeys, cursor, }) {
13
+ 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) => {
14
+ const isSelected = selectedKeys.includes(key);
15
+ const isRequired = requiredKeys.includes(key);
16
+ 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));
17
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "! required key \u00B7 q cancel" }) })] }));
18
+ }
6
19
  function SourceStep({ cursor }) {
7
20
  const options = [
8
21
  { label: 'File import', value: 'file' },
@@ -10,8 +23,8 @@ function SourceStep({ cursor }) {
10
23
  ];
11
24
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env source" }), _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))) })] }));
12
25
  }
13
- function FilePathStep({ value, error }) {
14
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter file path (.yaml/.yml/.json)" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ['>', " "] }), _jsx(Text, { color: "cyan", children: value }), _jsx(Text, { dimColor: true, children: "\u2588" })] }), error ? _jsx(Text, { color: "red", children: error }) : null] }));
26
+ function FilePathStep({ value, cursorPos, error }) {
27
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter file path (.yaml/.yml/.json)" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInputDisplay, { value: value, cursorPos: cursorPos }) }), error ? _jsx(Text, { color: "red", children: error }) : null] }));
15
28
  }
16
29
  function KeysStep({ keys, selectedKeys, cursor, }) {
17
30
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env keys to import" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle \u00B7 enter confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
@@ -19,11 +32,11 @@ function KeysStep({ keys, selectedKeys, cursor, }) {
19
32
  return (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: isSelected ? '[x]' : '[ ]' }), _jsxs(Text, { children: [" ", key] })] }, key));
20
33
  }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [selectedKeys.length, " of ", keys.length, " selected"] }) })] }));
21
34
  }
22
- function ManualInputStep({ entries, value, error, }) {
23
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter KEY=VALUE pairs (press q when done)" }), entries.length > 0 ? (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: entries.map(([key, val]) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u2022 " }), _jsx(Text, { color: "magenta", children: key }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: val })] }, key))) })) : null, _jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ['>', " "] }), _jsx(Text, { color: "cyan", children: value }), _jsx(Text, { dimColor: true, children: "\u2588" })] }), error ? _jsx(Text, { color: "red", children: error }) : null] }));
35
+ function ManualInputStep({ entries, value, cursorPos, error, }) {
36
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter KEY=VALUE pairs (press q when done)" }), entries.length > 0 ? (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: entries.map(([key, val]) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u2022 " }), _jsx(Text, { color: "magenta", children: key }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: val })] }, key))) })) : null, _jsx(TextInputDisplay, { value: value, cursorPos: cursorPos }), error ? _jsx(Text, { color: "red", children: error }) : null] }));
24
37
  }
25
- function NameStep({ value }) {
26
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter preset name" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ['>', " "] }), _jsx(Text, { color: "cyan", children: value }), _jsx(Text, { dimColor: true, children: "\u2588" })] })] }));
38
+ function NameStep({ value, cursorPos }) {
39
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter preset name" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInputDisplay, { value: value, cursorPos: cursorPos }) })] }));
27
40
  }
28
41
  function DestinationStep({ cursor }) {
29
42
  const options = [
@@ -32,10 +45,12 @@ function DestinationStep({ cursor }) {
32
45
  ];
33
46
  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
47
  }
35
- export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectEnvPath, }) {
48
+ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectEnvPath, detectedEnv, requiredKeys, }) {
36
49
  const { exit } = useApp();
37
- const [state, setState] = useState(createPresetCreateFlowState);
38
- const [textInput, setTextInput] = useState('');
50
+ const [state, setState] = useState(() => createPresetCreateFlowState(detectedEnv
51
+ ? (requiredKeys ? { detectedEnv, requiredKeys } : { detectedEnv })
52
+ : undefined));
53
+ const textInput = useTextInput();
39
54
  const [listCursor, setListCursor] = useState(0);
40
55
  const [allKeys, setAllKeys] = useState([]);
41
56
  const [fileEnv, setFileEnv] = useState({});
@@ -44,6 +59,58 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
44
59
  exit();
45
60
  return;
46
61
  }
62
+ if (state.step === 'detectedPrompt') {
63
+ if (input === 'q') {
64
+ exit();
65
+ return;
66
+ }
67
+ if (key.upArrow || input === 'k') {
68
+ setListCursor((c) => Math.max(0, c - 1));
69
+ return;
70
+ }
71
+ if (key.downArrow || input === 'j') {
72
+ setListCursor((c) => Math.min(1, c + 1));
73
+ return;
74
+ }
75
+ if (key.return) {
76
+ setState((s) => advancePresetCreateFlow(s, listCursor === 0
77
+ ? { type: 'accept-detected-prompt' }
78
+ : { type: 'reject-detected-prompt' }));
79
+ setListCursor(0);
80
+ textInput.reset();
81
+ return;
82
+ }
83
+ }
84
+ if (state.step === 'detected') {
85
+ if (input === 'q') {
86
+ exit();
87
+ return;
88
+ }
89
+ if (key.upArrow || input === 'k') {
90
+ setListCursor((c) => Math.max(0, c - 1));
91
+ return;
92
+ }
93
+ if (key.downArrow || input === 'j') {
94
+ setListCursor((c) => Math.min(state.allKeys.length - 1, c + 1));
95
+ return;
96
+ }
97
+ if (input === ' ') {
98
+ const targetKey = state.allKeys[listCursor];
99
+ if (targetKey) {
100
+ setState((s) => advancePresetCreateFlow(s, {
101
+ type: 'toggle-detected-key',
102
+ key: targetKey,
103
+ }));
104
+ }
105
+ return;
106
+ }
107
+ if (key.return) {
108
+ setState((s) => advancePresetCreateFlow(s, { type: 'confirm-detected-keys' }));
109
+ setListCursor(0);
110
+ textInput.reset();
111
+ return;
112
+ }
113
+ }
47
114
  if (state.step === 'source') {
48
115
  if (input === 'q') {
49
116
  exit();
@@ -61,7 +128,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
61
128
  const source = listCursor === 0 ? 'file' : 'manual';
62
129
  setState((s) => advancePresetCreateFlow(s, { type: 'select-source', source }));
63
130
  setListCursor(0);
64
- setTextInput('');
131
+ textInput.reset();
65
132
  return;
66
133
  }
67
134
  }
@@ -70,14 +137,10 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
70
137
  exit();
71
138
  return;
72
139
  }
73
- if (key.backspace || key.delete) {
74
- setTextInput((v) => v.slice(0, -1));
75
- return;
76
- }
77
140
  if (key.return) {
78
141
  void (async () => {
79
142
  try {
80
- const result = await readFile(textInput);
143
+ const result = await readFile(textInput.value);
81
144
  if (result.allKeys.length === 0) {
82
145
  setState((s) => advancePresetCreateFlow(s, {
83
146
  type: 'set-error',
@@ -89,7 +152,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
89
152
  setFileEnv(result.env);
90
153
  setState((s) => advancePresetCreateFlow(s, {
91
154
  type: 'set-file-path',
92
- filePath: textInput,
155
+ filePath: textInput.value,
93
156
  }));
94
157
  setListCursor(0);
95
158
  }
@@ -103,10 +166,8 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
103
166
  })();
104
167
  return;
105
168
  }
106
- if (input && !key.ctrl && !key.meta) {
107
- setTextInput((v) => v + input);
169
+ if (textInput.handleKey(input, key))
108
170
  return;
109
- }
110
171
  }
111
172
  if (state.step === 'keys') {
112
173
  if (input === 'q') {
@@ -141,12 +202,12 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
141
202
  keys: state.selectedKeys,
142
203
  env: selectedEnv,
143
204
  }));
144
- setTextInput('');
205
+ textInput.reset();
145
206
  return;
146
207
  }
147
208
  }
148
209
  if (state.step === 'manualInput') {
149
- if (input === 'q' && textInput === '') {
210
+ if (input === 'q' && textInput.value === '') {
150
211
  if (state.selectedKeys.length === 0) {
151
212
  setState((s) => advancePresetCreateFlow(s, {
152
213
  type: 'set-error',
@@ -155,15 +216,11 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
155
216
  return;
156
217
  }
157
218
  setState((s) => advancePresetCreateFlow(s, { type: 'finish-manual-input' }));
158
- setTextInput('');
159
- return;
160
- }
161
- if (key.backspace || key.delete) {
162
- setTextInput((v) => v.slice(0, -1));
219
+ textInput.reset();
163
220
  return;
164
221
  }
165
222
  if (key.return) {
166
- const separatorIndex = textInput.indexOf('=');
223
+ const separatorIndex = textInput.value.indexOf('=');
167
224
  if (separatorIndex <= 0) {
168
225
  setState((s) => advancePresetCreateFlow(s, {
169
226
  type: 'set-error',
@@ -171,8 +228,8 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
171
228
  }));
172
229
  return;
173
230
  }
174
- const k = textInput.slice(0, separatorIndex);
175
- const v = textInput.slice(separatorIndex + 1);
231
+ const k = textInput.value.slice(0, separatorIndex);
232
+ const v = textInput.value.slice(separatorIndex + 1);
176
233
  if (!/^[A-Z0-9_]+$/.test(k)) {
177
234
  setState((s) => advancePresetCreateFlow(s, {
178
235
  type: 'set-error',
@@ -185,35 +242,27 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
185
242
  key: k,
186
243
  value: v,
187
244
  }));
188
- setTextInput('');
245
+ textInput.reset();
189
246
  return;
190
247
  }
191
- if (input && !key.ctrl && !key.meta) {
192
- setTextInput((v) => v + input);
248
+ if (textInput.handleKey(input, key))
193
249
  return;
194
- }
195
250
  }
196
251
  if (state.step === 'name') {
197
252
  if (input === 'q') {
198
253
  exit();
199
254
  return;
200
255
  }
201
- if (key.backspace || key.delete) {
202
- setTextInput((v) => v.slice(0, -1));
203
- return;
204
- }
205
- if (key.return && textInput.trim().length > 0) {
256
+ if (key.return && textInput.value.trim().length > 0) {
206
257
  setState((s) => advancePresetCreateFlow(s, {
207
258
  type: 'set-name',
208
- name: textInput.trim(),
259
+ name: textInput.value.trim(),
209
260
  }));
210
261
  setListCursor(0);
211
262
  return;
212
263
  }
213
- if (input && !key.ctrl && !key.meta) {
214
- setTextInput((v) => v + input);
264
+ if (textInput.handleKey(input, key))
215
265
  return;
216
- }
217
266
  }
218
267
  if (state.step === 'destination') {
219
268
  if (input === 'q') {
@@ -261,7 +310,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
261
310
  if (state.step === 'done') {
262
311
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "Done" }) }));
263
312
  }
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)
313
+ 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.value, cursorPos: textInput.cursorPos, ...(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.value, cursorPos: textInput.cursorPos, ...(state.error ? { error: state.error } : {}) })), state.step === 'name' && _jsx(NameStep, { value: textInput.value, cursorPos: textInput.cursorPos }), 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
314
  .filter(([k]) => state.selectedKeys.includes(k))
266
315
  .sort(([a], [b]) => a.localeCompare(b)), mask: true, ...(state.filePath ? { fromFiles: [state.filePath] } : {}), toFiles: [
267
316
  state.destination === 'global'