@lkangd/cc-env 1.3.0 → 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
@@ -24,6 +24,7 @@ import { PresetShowApp } from './ink/preset-show-app.js';
24
24
  import { RunPresetSelectApp } from './ink/run-preset-select-app.js';
25
25
  import { advanceRestoreFlow, createRestoreFlowState } from './flows/restore-flow.js';
26
26
  import { RestoreApp } from './ink/restore-app.js';
27
+ import { getCliName } from './core/cli-name.js';
27
28
  import { CliError } from './core/errors.js';
28
29
  import { resolveGlobalRoot } from './core/paths.js';
29
30
  import { spawnCommand } from './core/spawn.js';
@@ -36,7 +37,7 @@ import { createSettingsEnvService } from './services/settings-env-service.js';
36
37
  import { createShellEnvService } from './services/shell-env-service.js';
37
38
  const program = new Command();
38
39
  program
39
- .name('cc-env')
40
+ .name(getCliName())
40
41
  .description('Manage runtime environment variables for Claude Code')
41
42
  .version(packageJson.version)
42
43
  .option('--verbose', 'Enable verbose output')
@@ -299,7 +300,7 @@ program
299
300
  .option('--shell <shell>', 'Shell type (bash, zsh, fish)', 'bash')
300
301
  .action(async (options) => {
301
302
  const { generateCompletion } = await import('./commands/completion.js');
302
- process.stdout.write(generateCompletion(options.shell));
303
+ process.stdout.write(generateCompletion(options.shell, getCliName()));
303
304
  });
304
305
  function printBanner() {
305
306
  const banner = figlet.textSync('CC ENV', { font: 'ANSI Shadow' });
@@ -346,7 +347,7 @@ main().catch((error) => {
346
347
  process.exitCode = 0;
347
348
  return;
348
349
  }
349
- 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`;
350
351
  const formatted = message?.replace(/^error:\s*/i, '') ?? 'Unknown error';
351
352
  process.stderr.write(`\n Error: ${formatted}\n\n${hint}\n`);
352
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)
@@ -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
+ }
@@ -3,7 +3,7 @@ export function createPresetCreateFlowState(input) {
3
3
  const requiredKeys = input?.requiredKeys ?? [];
4
4
  const detectedKeys = Object.keys(detectedEnv).sort();
5
5
  const selectedKeys = requiredKeys.filter((key) => key in detectedEnv);
6
- if (detectedKeys.length > 0) {
6
+ if (selectedKeys.length > 0) {
7
7
  return {
8
8
  step: 'detectedPrompt',
9
9
  env: detectedEnv,
@@ -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,6 +1,8 @@
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';
6
8
  function DetectedPromptStep({ cursor }) {
@@ -21,8 +23,8 @@ function SourceStep({ cursor }) {
21
23
  ];
22
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))) })] }));
23
25
  }
24
- function FilePathStep({ value, error }) {
25
- 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] }));
26
28
  }
27
29
  function KeysStep({ keys, selectedKeys, cursor, }) {
28
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) => {
@@ -30,11 +32,11 @@ function KeysStep({ keys, selectedKeys, cursor, }) {
30
32
  return (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: isSelected ? '[x]' : '[ ]' }), _jsxs(Text, { children: [" ", key] })] }, key));
31
33
  }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [selectedKeys.length, " of ", keys.length, " selected"] }) })] }));
32
34
  }
33
- function ManualInputStep({ entries, value, error, }) {
34
- 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] }));
35
37
  }
36
- function NameStep({ value }) {
37
- 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 }) })] }));
38
40
  }
39
41
  function DestinationStep({ cursor }) {
40
42
  const options = [
@@ -48,7 +50,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
48
50
  const [state, setState] = useState(() => createPresetCreateFlowState(detectedEnv
49
51
  ? (requiredKeys ? { detectedEnv, requiredKeys } : { detectedEnv })
50
52
  : undefined));
51
- const [textInput, setTextInput] = useState('');
53
+ const textInput = useTextInput();
52
54
  const [listCursor, setListCursor] = useState(0);
53
55
  const [allKeys, setAllKeys] = useState([]);
54
56
  const [fileEnv, setFileEnv] = useState({});
@@ -75,7 +77,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
75
77
  ? { type: 'accept-detected-prompt' }
76
78
  : { type: 'reject-detected-prompt' }));
77
79
  setListCursor(0);
78
- setTextInput('');
80
+ textInput.reset();
79
81
  return;
80
82
  }
81
83
  }
@@ -105,7 +107,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
105
107
  if (key.return) {
106
108
  setState((s) => advancePresetCreateFlow(s, { type: 'confirm-detected-keys' }));
107
109
  setListCursor(0);
108
- setTextInput('');
110
+ textInput.reset();
109
111
  return;
110
112
  }
111
113
  }
@@ -126,7 +128,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
126
128
  const source = listCursor === 0 ? 'file' : 'manual';
127
129
  setState((s) => advancePresetCreateFlow(s, { type: 'select-source', source }));
128
130
  setListCursor(0);
129
- setTextInput('');
131
+ textInput.reset();
130
132
  return;
131
133
  }
132
134
  }
@@ -135,14 +137,10 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
135
137
  exit();
136
138
  return;
137
139
  }
138
- if (key.backspace || key.delete) {
139
- setTextInput((v) => v.slice(0, -1));
140
- return;
141
- }
142
140
  if (key.return) {
143
141
  void (async () => {
144
142
  try {
145
- const result = await readFile(textInput);
143
+ const result = await readFile(textInput.value);
146
144
  if (result.allKeys.length === 0) {
147
145
  setState((s) => advancePresetCreateFlow(s, {
148
146
  type: 'set-error',
@@ -154,7 +152,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
154
152
  setFileEnv(result.env);
155
153
  setState((s) => advancePresetCreateFlow(s, {
156
154
  type: 'set-file-path',
157
- filePath: textInput,
155
+ filePath: textInput.value,
158
156
  }));
159
157
  setListCursor(0);
160
158
  }
@@ -168,10 +166,8 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
168
166
  })();
169
167
  return;
170
168
  }
171
- if (input && !key.ctrl && !key.meta) {
172
- setTextInput((v) => v + input);
169
+ if (textInput.handleKey(input, key))
173
170
  return;
174
- }
175
171
  }
176
172
  if (state.step === 'keys') {
177
173
  if (input === 'q') {
@@ -206,12 +202,12 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
206
202
  keys: state.selectedKeys,
207
203
  env: selectedEnv,
208
204
  }));
209
- setTextInput('');
205
+ textInput.reset();
210
206
  return;
211
207
  }
212
208
  }
213
209
  if (state.step === 'manualInput') {
214
- if (input === 'q' && textInput === '') {
210
+ if (input === 'q' && textInput.value === '') {
215
211
  if (state.selectedKeys.length === 0) {
216
212
  setState((s) => advancePresetCreateFlow(s, {
217
213
  type: 'set-error',
@@ -220,15 +216,11 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
220
216
  return;
221
217
  }
222
218
  setState((s) => advancePresetCreateFlow(s, { type: 'finish-manual-input' }));
223
- setTextInput('');
224
- return;
225
- }
226
- if (key.backspace || key.delete) {
227
- setTextInput((v) => v.slice(0, -1));
219
+ textInput.reset();
228
220
  return;
229
221
  }
230
222
  if (key.return) {
231
- const separatorIndex = textInput.indexOf('=');
223
+ const separatorIndex = textInput.value.indexOf('=');
232
224
  if (separatorIndex <= 0) {
233
225
  setState((s) => advancePresetCreateFlow(s, {
234
226
  type: 'set-error',
@@ -236,8 +228,8 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
236
228
  }));
237
229
  return;
238
230
  }
239
- const k = textInput.slice(0, separatorIndex);
240
- const v = textInput.slice(separatorIndex + 1);
231
+ const k = textInput.value.slice(0, separatorIndex);
232
+ const v = textInput.value.slice(separatorIndex + 1);
241
233
  if (!/^[A-Z0-9_]+$/.test(k)) {
242
234
  setState((s) => advancePresetCreateFlow(s, {
243
235
  type: 'set-error',
@@ -250,35 +242,27 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
250
242
  key: k,
251
243
  value: v,
252
244
  }));
253
- setTextInput('');
245
+ textInput.reset();
254
246
  return;
255
247
  }
256
- if (input && !key.ctrl && !key.meta) {
257
- setTextInput((v) => v + input);
248
+ if (textInput.handleKey(input, key))
258
249
  return;
259
- }
260
250
  }
261
251
  if (state.step === 'name') {
262
252
  if (input === 'q') {
263
253
  exit();
264
254
  return;
265
255
  }
266
- if (key.backspace || key.delete) {
267
- setTextInput((v) => v.slice(0, -1));
268
- return;
269
- }
270
- if (key.return && textInput.trim().length > 0) {
256
+ if (key.return && textInput.value.trim().length > 0) {
271
257
  setState((s) => advancePresetCreateFlow(s, {
272
258
  type: 'set-name',
273
- name: textInput.trim(),
259
+ name: textInput.value.trim(),
274
260
  }));
275
261
  setListCursor(0);
276
262
  return;
277
263
  }
278
- if (input && !key.ctrl && !key.meta) {
279
- setTextInput((v) => v + input);
264
+ if (textInput.handleKey(input, key))
280
265
  return;
281
- }
282
266
  }
283
267
  if (state.step === 'destination') {
284
268
  if (input === 'q') {
@@ -326,7 +310,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
326
310
  if (state.step === 'done') {
327
311
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "Done" }) }));
328
312
  }
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)
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)
330
314
  .filter(([k]) => state.selectedKeys.includes(k))
331
315
  .sort(([a], [b]) => a.localeCompare(b)), mask: true, ...(state.filePath ? { fromFiles: [state.filePath] } : {}), toFiles: [
332
316
  state.destination === 'global'
@@ -1,19 +1,21 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } 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
  export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
5
7
  const { exit } = useApp();
6
8
  const [entries, setEntries] = useState(Object.entries(initialEnv));
7
9
  const [cursor, setCursor] = useState(0);
8
10
  const [editing, setEditing] = useState(null);
9
- const [textInput, setTextInput] = useState('');
11
+ const textInput = useTextInput();
10
12
  const [error, setError] = useState();
11
13
  const [step, setStep] = useState('list');
12
14
  useInput((input, key) => {
13
15
  if (key.escape || input === 'q') {
14
16
  if (editing !== null) {
15
17
  setEditing(null);
16
- setTextInput('');
18
+ textInput.reset();
17
19
  setError(undefined);
18
20
  return;
19
21
  }
@@ -33,7 +35,7 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
33
35
  if (key.return && entries.length > 0) {
34
36
  const entry = entries[cursor];
35
37
  if (entry) {
36
- setTextInput(`${entry[0]}=${entry[1]}`);
38
+ textInput.reset(`${entry[0]}=${entry[1]}`);
37
39
  setEditing(cursor);
38
40
  setError(undefined);
39
41
  }
@@ -45,7 +47,7 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
45
47
  return;
46
48
  }
47
49
  if (input === 'a') {
48
- setTextInput('');
50
+ textInput.reset();
49
51
  setEditing(entries.length);
50
52
  setError(undefined);
51
53
  return;
@@ -56,18 +58,14 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
56
58
  }
57
59
  }
58
60
  if (editing !== null) {
59
- if (key.backspace || key.delete) {
60
- setTextInput((v) => v.slice(0, -1));
61
- return;
62
- }
63
61
  if (key.return) {
64
- const sep = textInput.indexOf('=');
62
+ const sep = textInput.value.indexOf('=');
65
63
  if (sep <= 0) {
66
64
  setError('Format must be KEY=VALUE');
67
65
  return;
68
66
  }
69
- const k = textInput.slice(0, sep);
70
- const v = textInput.slice(sep + 1);
67
+ const k = textInput.value.slice(0, sep);
68
+ const v = textInput.value.slice(sep + 1);
71
69
  if (!/^[A-Z0-9_]+$/.test(k)) {
72
70
  setError('Key must match [A-Z0-9_]+');
73
71
  return;
@@ -83,14 +81,12 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
83
81
  return next;
84
82
  });
85
83
  setEditing(null);
86
- setTextInput('');
84
+ textInput.reset();
87
85
  setError(undefined);
88
86
  return;
89
87
  }
90
- if (input && !key.ctrl && !key.meta) {
91
- setTextInput((v) => v + input);
88
+ if (textInput.handleKey(input, key))
92
89
  return;
93
- }
94
90
  }
95
91
  if (step === 'confirm') {
96
92
  if (key.return) {
@@ -108,5 +104,5 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
108
104
  if (step === 'confirm') {
109
105
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Save changes to preset \"", name, "\"?"] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: entries.map(([k, v]) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u2022 " }), _jsx(Text, { color: "magenta", children: k }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: v })] }, k))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press enter to confirm \u00B7 q to go back" }) })] }));
110
106
  }
111
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Editing preset: ", name] }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter edit \u00B7 d delete \u00B7 a add \u00B7 s save \u00B7 q cancel" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [entries.map(([k, v], i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: "magenta", children: k }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: v })] }, k))), entries.length === 0 && _jsx(Text, { dimColor: true, children: "No entries. Press a to add." })] }), editing !== null && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: editing < entries.length ? 'Edit entry' : 'Add entry' }), _jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ['>', " "] }), _jsx(Text, { color: "cyan", children: textInput }), _jsx(Text, { dimColor: true, children: "\u2588" })] }), error ? _jsx(Text, { color: "red", children: error }) : null] }))] }));
107
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Editing preset: ", name] }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter edit \u00B7 d delete \u00B7 a add \u00B7 s save \u00B7 q cancel" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [entries.map(([k, v], i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: "magenta", children: k }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: v })] }, k))), entries.length === 0 && _jsx(Text, { dimColor: true, children: "No entries. Press a to add." })] }), editing !== null && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: editing < entries.length ? 'Edit entry' : 'Add entry' }), _jsx(TextInputDisplay, { value: textInput.value, cursorPos: textInput.cursorPos }), error ? _jsx(Text, { color: "red", children: error }) : null] }))] }));
112
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lkangd/cc-env",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Manage runtime environment variables for Claude Code",
5
5
  "homepage": "https://github.com/lkangd/cc-env#readme",
6
6
  "bugs": {
@@ -20,7 +20,8 @@
20
20
  "node": ">=20.19.2"
21
21
  },
22
22
  "bin": {
23
- "cc-env": "dist/cli.js"
23
+ "cc-env": "dist/cli.js",
24
+ "ccenv": "dist/cli.js"
24
25
  },
25
26
  "files": [
26
27
  "dist",