@lkangd/cc-env 1.3.0 → 1.4.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/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
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
+ import { spawn } from 'node:child_process';
4
5
  import { join } from 'node:path';
5
6
  import figlet from 'figlet';
6
7
  import gradient from 'gradient-string';
@@ -8,11 +9,6 @@ import { Command } from 'commander';
8
9
  import packageJson from '../package.json' with { type: 'json' };
9
10
  const h = React.createElement;
10
11
  import { createPresetCreateCommand } from './commands/preset/create.js';
11
- import { createDeletePresetCommand } from './commands/preset/delete.js';
12
- import { createEditPresetCommand } from './commands/preset/edit.js';
13
- import { createRenamePresetCommand } from './commands/preset/rename.js';
14
- import { PresetDeleteApp } from './ink/preset-delete-app.js';
15
- import { PresetEditApp } from './ink/preset-edit-app.js';
16
12
  import { createShowPresetsCommand } from './commands/preset/show.js';
17
13
  import { createRestoreCommand } from './commands/restore.js';
18
14
  import { createRunCommand } from './commands/run.js';
@@ -24,6 +20,7 @@ import { PresetShowApp } from './ink/preset-show-app.js';
24
20
  import { RunPresetSelectApp } from './ink/run-preset-select-app.js';
25
21
  import { advanceRestoreFlow, createRestoreFlowState } from './flows/restore-flow.js';
26
22
  import { RestoreApp } from './ink/restore-app.js';
23
+ import { getCliName } from './core/cli-name.js';
27
24
  import { CliError } from './core/errors.js';
28
25
  import { resolveGlobalRoot } from './core/paths.js';
29
26
  import { spawnCommand } from './core/spawn.js';
@@ -36,7 +33,7 @@ import { createSettingsEnvService } from './services/settings-env-service.js';
36
33
  import { createShellEnvService } from './services/shell-env-service.js';
37
34
  const program = new Command();
38
35
  program
39
- .name('cc-env')
36
+ .name(getCliName())
40
37
  .description('Manage runtime environment variables for Claude Code')
41
38
  .version(packageJson.version)
42
39
  .option('--verbose', 'Enable verbose output')
@@ -53,6 +50,19 @@ const projectEnvService = createProjectEnvService({ cwd });
53
50
  const presetService = createPresetService(globalRoot);
54
51
  const historyService = createHistoryService(globalRoot);
55
52
  const projectStateService = createProjectStateService(globalRoot);
53
+ function openDirectory(directoryPath) {
54
+ return new Promise((resolve, reject) => {
55
+ const child = spawn('open', [directoryPath], { stdio: 'ignore' });
56
+ child.once('error', reject);
57
+ child.once('exit', (code) => {
58
+ if (code === 0) {
59
+ resolve();
60
+ return;
61
+ }
62
+ reject(new Error(`Failed to open directory: ${directoryPath}`));
63
+ });
64
+ });
65
+ }
56
66
  async function runPresetCreateFlow({ detectedEnv, requiredKeys }) {
57
67
  let result;
58
68
  const app = render(h(PresetCreateApp, {
@@ -227,33 +237,24 @@ program
227
237
  .action((options) => createShowPresetsCommand({
228
238
  presetService,
229
239
  projectEnvService,
240
+ cwd,
241
+ openDirectory,
230
242
  renderShow: async (presets) => {
231
243
  if (options.json) {
232
244
  process.stdout.write(JSON.stringify(presets, null, 2) + '\n');
233
- return;
245
+ return { type: 'exit' };
234
246
  }
235
- const app = render(h(PresetShowApp, { presets }));
236
- await app.waitUntilExit();
237
- }
238
- })());
239
- program
240
- .command('delete')
241
- .description('Delete a saved preset')
242
- .action(createDeletePresetCommand({
243
- presetService,
244
- projectEnvService,
245
- renderDelete: async (presets) => {
246
- let result;
247
- const app = render(h(PresetDeleteApp, {
247
+ let result = { type: 'exit' };
248
+ const app = render(h(PresetShowApp, {
248
249
  presets,
249
- onSubmit: preset => {
250
- result = preset;
251
- }
250
+ onSubmit: action => {
251
+ result = action;
252
+ },
252
253
  }));
253
254
  await app.waitUntilExit();
254
255
  return result;
255
256
  }
256
- }));
257
+ })());
257
258
  program
258
259
  .command('create')
259
260
  .description('Create a new environment preset')
@@ -271,35 +272,13 @@ program
271
272
  .description('Check system health and configuration')
272
273
  .option('--json', 'Output as JSON')
273
274
  .action((options) => runDoctorCommand({ cwd, json: options.json }));
274
- program
275
- .command('edit <name>')
276
- .description('Edit an existing preset')
277
- .action((name) => createEditPresetCommand({
278
- presetService,
279
- renderEdit: async (preset) => {
280
- let result;
281
- const app = render(h(PresetEditApp, {
282
- name: preset.name,
283
- env: preset.env,
284
- onSubmit: (value) => {
285
- result = value;
286
- }
287
- }));
288
- await app.waitUntilExit();
289
- return result;
290
- }
291
- })({ name }));
292
- program
293
- .command('rename <from> <to>')
294
- .description('Rename a preset')
295
- .action((from, to) => createRenamePresetCommand({ presetService })({ from, to }));
296
275
  program
297
276
  .command('completion')
298
277
  .description('Generate shell completion script')
299
278
  .option('--shell <shell>', 'Shell type (bash, zsh, fish)', 'bash')
300
279
  .action(async (options) => {
301
280
  const { generateCompletion } = await import('./commands/completion.js');
302
- process.stdout.write(generateCompletion(options.shell));
281
+ process.stdout.write(generateCompletion(options.shell, getCliName()));
303
282
  });
304
283
  function printBanner() {
305
284
  const banner = figlet.textSync('CC ENV', { font: 'ANSI Shadow' });
@@ -332,6 +311,13 @@ async function main() {
332
311
  process.exitCode = 0;
333
312
  return;
334
313
  }
314
+ if (args[0] === 'claude') {
315
+ const opts = program.opts();
316
+ if (!opts.quiet)
317
+ printBanner();
318
+ await runWithBootstrap({ args, yes: !process.stdin.isTTY });
319
+ return;
320
+ }
335
321
  await program.parseAsync(process.argv);
336
322
  }
337
323
  main().catch((error) => {
@@ -346,7 +332,7 @@ main().catch((error) => {
346
332
  process.exitCode = 0;
347
333
  return;
348
334
  }
349
- const hint = ` Run "cc-env --help" to see available commands and options.\n`;
335
+ const hint = ` Run "${getCliName()} --help" to see available commands and options.\n`;
350
336
  const formatted = message?.replace(/^error:\s*/i, '') ?? 'Unknown error';
351
337
  process.stderr.write(`\n Error: ${formatted}\n\n${hint}\n`);
352
338
  process.exitCode = 1;
@@ -1,16 +1,16 @@
1
1
  const COMMANDS = ['run', 'init', 'restore', 'show', 'delete', 'create', 'doctor', 'completion', '--help', '--version'];
2
- export function generateCompletion(shell) {
2
+ export function generateCompletion(shell, cliName = 'cc-env') {
3
3
  switch (shell) {
4
4
  case 'zsh':
5
- return generateZsh();
5
+ return generateZsh(cliName);
6
6
  case 'fish':
7
- return generateFish();
7
+ return generateFish(cliName);
8
8
  default:
9
- return generateBash();
9
+ return generateBash(cliName);
10
10
  }
11
11
  }
12
- function generateBash() {
13
- return `# cc-env bash completion
12
+ function generateBash(cliName) {
13
+ return `# ${cliName} bash completion
14
14
  # Add to ~/.bashrc: eval "$(cc-env completion --shell bash)"
15
15
  _cc_env_completions() {
16
16
  local cur="\${COMP_WORDS[COMP_CWORD]}"
@@ -18,12 +18,13 @@ _cc_env_completions() {
18
18
  COMPREPLY=($(compgen -W "$commands" -- "$cur"))
19
19
  }
20
20
  complete -F _cc_env_completions cc-env
21
+ complete -F _cc_env_completions ccenv
21
22
  `;
22
23
  }
23
- function generateZsh() {
24
+ function generateZsh(cliName) {
24
25
  const cmds = COMMANDS.filter((c) => !c.startsWith('-'));
25
26
  const cmdList = cmds.map((c) => ` '${c}'`).join('\n');
26
- return `# cc-env zsh completion
27
+ return `# ${cliName} zsh completion
27
28
  # Add to ~/.zshrc: eval "$(cc-env completion --shell zsh)"
28
29
  _cc_env() {
29
30
  local -a commands
@@ -33,9 +34,10 @@ ${cmdList}
33
34
  _describe 'command' commands
34
35
  }
35
36
  compdef _cc_env cc-env
37
+ compdef _cc_env ccenv
36
38
  `;
37
39
  }
38
- function generateFish() {
40
+ function generateFish(cliName) {
39
41
  const cmds = [
40
42
  ['run', 'Run claude with merged environment variables'],
41
43
  ['init', 'Initialize cc-env for the current project'],
@@ -46,15 +48,19 @@ function generateFish() {
46
48
  ['doctor', 'Check system health and configuration'],
47
49
  ['completion', 'Generate shell completion script'],
48
50
  ];
49
- const lines = cmds.map(([cmd, desc]) => `complete -c cc-env -f -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`);
50
- return `# cc-env fish completion
51
+ const names = ['cc-env', 'ccenv'];
52
+ const subcommandLines = names.flatMap(name => cmds.map(([cmd, desc]) => `complete -c ${name} -f -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`));
53
+ const flagLines = names.flatMap(name => [
54
+ `complete -c ${name} -l help -d 'Show help'`,
55
+ `complete -c ${name} -l version -d 'Show version'`,
56
+ `complete -c ${name} -l json -d 'Output as JSON'`,
57
+ `complete -c ${name} -l quiet -d 'Suppress non-essential output'`,
58
+ `complete -c ${name} -l verbose -d 'Enable verbose output'`,
59
+ `complete -c ${name} -l no-interactive -d 'Disable interactive prompts'`,
60
+ ]);
61
+ return `# ${cliName} fish completion
51
62
  # Add to fish config: cc-env completion --shell fish | source
52
- ${lines.join('\n')}
53
- complete -c cc-env -l help -d 'Show help'
54
- complete -c cc-env -l version -d 'Show version'
55
- complete -c cc-env -l json -d 'Output as JSON'
56
- complete -c cc-env -l quiet -d 'Suppress non-essential output'
57
- complete -c cc-env -l verbose -d 'Enable verbose output'
58
- complete -c cc-env -l no-interactive -d 'Disable interactive prompts'
63
+ ${subcommandLines.join('\n')}
64
+ ${flagLines.join('\n')}
59
65
  `;
60
66
  }
@@ -1,5 +1,6 @@
1
1
  import { access, readdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { getCliName } from '../core/cli-name.js';
3
4
  import { findClaudeExecutable } from '../core/find-claude.js';
4
5
  import { resolveGlobalRoot } from '../core/paths.js';
5
6
  async function exists(p) {
@@ -36,7 +37,7 @@ async function checkClaudeExecutable() {
36
37
  async function checkProjectEnv(cwd) {
37
38
  const path = join(cwd, '.cc-env', 'env.json');
38
39
  const ok = await exists(path);
39
- return { label: 'Project env (.cc-env/env.json)', ok, detail: ok ? path : 'not initialized — run: cc-env init' };
40
+ return { label: 'Project env (.cc-env/env.json)', ok, detail: ok ? path : `not initialized — run: ${getCliName()} init` };
40
41
  }
41
42
  function renderCheck(result, json) {
42
43
  if (json)
@@ -1,16 +1,105 @@
1
- export function createShowPresetsCommand({ presetService, projectEnvService, renderShow, }) {
2
- return async function showPresets() {
1
+ import { join, dirname } from 'node:path';
2
+ import { CliError } from '../../core/errors.js';
3
+ export function createShowPresetsCommand({ presetService, projectEnvService, renderShow, cwd, openDirectory, }) {
4
+ async function loadPresets() {
3
5
  const names = await presetService.listNames();
4
6
  const globalPresets = await Promise.all(names.map((name) => presetService.read(name).then((p) => ({ name, env: p.env, source: 'global' }))));
5
7
  const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta();
6
8
  const projectPreset = Object.keys(projectEnv).length > 0
7
9
  ? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' }]
8
10
  : [];
9
- const presets = [...projectPreset, ...globalPresets];
10
- if (presets.length === 0) {
11
- console.log('No presets found.');
12
- return;
11
+ return [...projectPreset, ...globalPresets];
12
+ }
13
+ async function validateRename(nextName, current) {
14
+ const trimmed = nextName.trim();
15
+ if (!trimmed)
16
+ throw new CliError('Preset name cannot be empty');
17
+ if (trimmed === current.name)
18
+ throw new CliError('New name must be different from the current name');
19
+ const globalNames = await presetService.listNames();
20
+ const { name: projectName } = await projectEnvService.readWithMeta();
21
+ const takenNames = new Set([
22
+ ...globalNames.filter((name) => !(current.source === 'global' && name === current.name)),
23
+ ...(projectName && !(current.source === 'project' && projectName === current.name) ? [projectName] : []),
24
+ ]);
25
+ if (takenNames.has(trimmed))
26
+ throw new CliError(`Preset "${trimmed}" already exists`);
27
+ return trimmed;
28
+ }
29
+ function resolvePresetDirectory(preset) {
30
+ if (preset.source === 'project') {
31
+ return join(cwd, '.cc-env');
32
+ }
33
+ return dirname(presetService.getPath(preset.name));
34
+ }
35
+ return async function showPresets() {
36
+ while (true) {
37
+ const presets = await loadPresets();
38
+ if (presets.length === 0) {
39
+ console.log('No presets found.');
40
+ return;
41
+ }
42
+ const action = await renderShow(presets);
43
+ if (!action || action.type === 'exit')
44
+ return;
45
+ if (action.type === 'open-directory') {
46
+ await openDirectory(resolvePresetDirectory(action.preset));
47
+ continue;
48
+ }
49
+ if (action.type === 'delete') {
50
+ if (action.preset.source === 'project') {
51
+ await projectEnvService.write({});
52
+ }
53
+ else {
54
+ await presetService.remove(action.preset.name);
55
+ }
56
+ continue;
57
+ }
58
+ if (action.type === 'edit') {
59
+ if (!action.result.confirmed)
60
+ continue;
61
+ const updatedAt = new Date().toISOString();
62
+ if (action.preset.source === 'project') {
63
+ const existing = await projectEnvService.readWithMeta();
64
+ await projectEnvService.write(action.result.env, {
65
+ name: existing.name ?? action.preset.name,
66
+ ...(existing.createdAt ? { createdAt: existing.createdAt } : {}),
67
+ updatedAt,
68
+ });
69
+ }
70
+ else {
71
+ const existing = await presetService.read(action.preset.name);
72
+ await presetService.write({
73
+ name: action.preset.name,
74
+ env: action.result.env,
75
+ createdAt: existing.createdAt,
76
+ updatedAt,
77
+ });
78
+ }
79
+ continue;
80
+ }
81
+ if (!action.confirmed)
82
+ continue;
83
+ const updatedAt = new Date().toISOString();
84
+ const nextName = await validateRename(action.nextName, action.preset);
85
+ if (action.preset.source === 'project') {
86
+ const existing = await projectEnvService.readWithMeta();
87
+ await projectEnvService.write(existing.env, {
88
+ name: nextName,
89
+ ...(existing.createdAt ? { createdAt: existing.createdAt } : {}),
90
+ updatedAt,
91
+ });
92
+ }
93
+ else {
94
+ const existing = await presetService.read(action.preset.name);
95
+ await presetService.write({
96
+ name: nextName,
97
+ env: existing.env,
98
+ createdAt: existing.createdAt,
99
+ updatedAt,
100
+ });
101
+ await presetService.remove(action.preset.name);
102
+ }
13
103
  }
14
- await renderShow(presets);
15
104
  };
16
105
  }
@@ -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
  }
@@ -1,27 +1,111 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
3
  import { Box, Text, useApp, useInput } from 'ink';
4
+ import { PresetEditApp } from './preset-edit-app.js';
5
+ import { TextInputDisplay } from './components/text-input.js';
6
+ import { useTextInput } from './hooks/use-text-input.js';
4
7
  import { EnvEntries } from './summary.js';
5
- export function PresetShowApp({ presets, }) {
8
+ export function PresetShowApp({ presets, onSubmit, }) {
6
9
  const { exit } = useApp();
7
10
  const [cursor, setCursor] = useState(0);
11
+ const [step, setStep] = useState('list');
12
+ const [renameError, setRenameError] = useState();
13
+ const textInput = useTextInput();
8
14
  const activePreset = presets[cursor];
9
15
  const entries = useMemo(() => activePreset
10
16
  ? Object.entries(activePreset.env).sort(([a], [b]) => a.localeCompare(b))
11
17
  : [], [activePreset]);
12
18
  useInput((input, key) => {
13
- if (key.escape || input.toLowerCase() === 'q') {
14
- exit();
19
+ if (step === 'list') {
20
+ if (key.escape || input.toLowerCase() === 'q') {
21
+ onSubmit({ type: 'exit' });
22
+ exit();
23
+ return;
24
+ }
25
+ if (key.upArrow || input === 'k') {
26
+ setCursor((c) => Math.max(0, c - 1));
27
+ return;
28
+ }
29
+ if (key.downArrow || input === 'j') {
30
+ setCursor((c) => Math.min(presets.length - 1, c + 1));
31
+ return;
32
+ }
33
+ if (input === 'o' && activePreset) {
34
+ onSubmit({ type: 'open-directory', preset: activePreset });
35
+ exit();
36
+ return;
37
+ }
38
+ if (input === 'd' && activePreset) {
39
+ setStep('confirm-delete');
40
+ return;
41
+ }
42
+ if (input === 'r' && activePreset) {
43
+ textInput.reset(activePreset.name);
44
+ setRenameError(undefined);
45
+ setStep('rename');
46
+ return;
47
+ }
48
+ if (input === 'e' && activePreset) {
49
+ setStep('edit');
50
+ }
15
51
  return;
16
52
  }
17
- if (key.upArrow || input === 'k') {
18
- setCursor((c) => Math.max(0, c - 1));
53
+ if (step === 'rename') {
54
+ if (key.escape || input.toLowerCase() === 'q') {
55
+ setStep('list');
56
+ textInput.reset();
57
+ setRenameError(undefined);
58
+ return;
59
+ }
60
+ if (key.return) {
61
+ const nextName = textInput.value.trim();
62
+ if (!nextName) {
63
+ setRenameError('Name cannot be empty');
64
+ return;
65
+ }
66
+ if (nextName === activePreset?.name) {
67
+ setRenameError('New name must be different from the current name');
68
+ return;
69
+ }
70
+ setRenameError(undefined);
71
+ setStep('confirm-rename');
72
+ return;
73
+ }
74
+ if (textInput.handleKey(input, key))
75
+ return;
19
76
  return;
20
77
  }
21
- if (key.downArrow || input === 'j') {
22
- setCursor((c) => Math.min(presets.length - 1, c + 1));
78
+ if (step === 'confirm-delete') {
79
+ if (input.toLowerCase() === 'y' && activePreset) {
80
+ onSubmit({ type: 'delete', preset: activePreset });
81
+ exit();
82
+ return;
83
+ }
84
+ if (input.toLowerCase() === 'n' || key.escape) {
85
+ setStep('list');
86
+ return;
87
+ }
23
88
  return;
24
89
  }
90
+ if (step === 'confirm-rename') {
91
+ if (input.toLowerCase() === 'y' && activePreset) {
92
+ onSubmit({ type: 'rename', preset: activePreset, nextName: textInput.value.trim(), confirmed: true });
93
+ exit();
94
+ return;
95
+ }
96
+ if (input.toLowerCase() === 'n' || key.escape) {
97
+ setStep('list');
98
+ textInput.reset();
99
+ setRenameError(undefined);
100
+ return;
101
+ }
102
+ }
25
103
  });
26
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Preset show" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 q exit" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "Presets" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: presets.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { children: index === cursor ? '' : ' ' }), _jsx(Text, { ...(preset.source === 'project' ? { color: 'yellow' } : {}), children: preset.name }), _jsxs(Text, { dimColor: true, children: [" (", preset.source, ")"] })] }, `${preset.source}:${preset.name}`))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: activePreset?.name ?? 'Preview' }), _jsx(Text, { dimColor: true, children: activePreset?.source === 'project' ? 'Project preset' : 'Global preset' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: entries }) })] })] })] }));
104
+ if (step === 'edit' && activePreset) {
105
+ return (_jsx(PresetEditApp, { name: activePreset.name, env: activePreset.env, onSubmit: (result) => {
106
+ onSubmit({ type: 'edit', preset: activePreset, result });
107
+ exit();
108
+ } }));
109
+ }
110
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Preset show" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 o open \u00B7 e edit \u00B7 r rename \u00B7 d delete \u00B7 q exit" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "Presets" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: presets.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { children: index === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(preset.source === 'project' ? { color: 'yellow' } : {}), children: preset.name }), _jsxs(Text, { dimColor: true, children: [" (", preset.source, ")"] })] }, `${preset.source}:${preset.name}`))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: activePreset?.name ?? 'Preview' }), _jsx(Text, { dimColor: true, children: activePreset?.source === 'project' ? 'Project preset' : 'Global preset' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: entries }) })] })] }), step === 'rename' && activePreset ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, children: ["Rename preset: ", activePreset.name] }), _jsx(TextInputDisplay, { value: textInput.value, cursorPos: textInput.cursorPos }), renameError ? _jsx(Text, { color: "red", children: renameError }) : null, _jsx(Text, { dimColor: true, children: "Press enter to continue \u00B7 q to cancel" })] })) : null, step === 'confirm-delete' && activePreset ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "red", children: "Delete preset " }), _jsx(Text, { bold: true, children: activePreset.name }), _jsxs(Text, { color: "red", children: [" (", activePreset.source, ")?"] }), _jsx(Text, { dimColor: true, children: " y/n" })] })) : null, step === 'confirm-rename' && activePreset ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Rename preset ", _jsx(Text, { bold: true, children: activePreset.name }), " \u2192 ", _jsx(Text, { bold: true, children: textInput.value.trim() })] }), _jsx(Text, { dimColor: true, children: "Press y to confirm \u00B7 n to cancel" })] })) : null] }));
27
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lkangd/cc-env",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
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",
@@ -1,25 +0,0 @@
1
- export function createDeletePresetCommand({ presetService, projectEnvService, renderDelete, }) {
2
- return async function deletePreset() {
3
- const names = await presetService.listNames();
4
- const globalPresets = await Promise.all(names.map((name) => presetService.read(name).then((p) => ({ name, env: p.env, source: 'global' }))));
5
- const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta();
6
- const projectPreset = Object.keys(projectEnv).length > 0
7
- ? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' }]
8
- : [];
9
- const presets = [...projectPreset, ...globalPresets];
10
- if (presets.length === 0) {
11
- console.log('No presets found.');
12
- return;
13
- }
14
- const selected = await renderDelete(presets);
15
- if (!selected)
16
- return;
17
- if (selected.source === 'project') {
18
- await projectEnvService.write({});
19
- }
20
- else {
21
- await presetService.remove(selected.name);
22
- }
23
- console.log(`Deleted preset: ${selected.name}`);
24
- };
25
- }
@@ -1,20 +0,0 @@
1
- import { CliError } from '../../core/errors.js';
2
- export function createEditPresetCommand({ presetService, renderEdit, }) {
3
- return async function editPreset({ name }) {
4
- if (!name)
5
- throw new CliError('Usage: cc-env edit <preset-name>');
6
- const existing = await presetService.read(name);
7
- const result = await renderEdit({ name, env: existing.env });
8
- if (!result?.confirmed) {
9
- process.stdout.write('Edit cancelled.\n');
10
- return;
11
- }
12
- await presetService.write({
13
- name,
14
- env: result.env,
15
- createdAt: existing.createdAt,
16
- updatedAt: new Date().toISOString(),
17
- });
18
- process.stdout.write(`Updated preset "${name}"\n`);
19
- };
20
- }
@@ -1,16 +0,0 @@
1
- import { CliError } from '../../core/errors.js';
2
- export function createRenamePresetCommand({ presetService }) {
3
- return async function renamePreset({ from, to }) {
4
- if (!from || !to)
5
- throw new CliError('Usage: cc-env rename <from> <to>');
6
- if (from === to)
7
- throw new CliError('New name must be different from the current name');
8
- const existing = await presetService.read(from);
9
- const names = await presetService.listNames();
10
- if (names.includes(to))
11
- throw new CliError(`Preset "${to}" already exists`);
12
- await presetService.write({ ...existing, name: to, updatedAt: new Date().toISOString() });
13
- await presetService.remove(from);
14
- process.stdout.write(`Renamed preset "${from}" → "${to}"\n`);
15
- };
16
- }
@@ -1,47 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from 'react';
3
- import { Box, Text, useApp, useInput } from 'ink';
4
- import { EnvEntries } from './summary.js';
5
- export function PresetDeleteApp({ presets, onSubmit, }) {
6
- const { exit } = useApp();
7
- const [cursor, setCursor] = useState(0);
8
- const [step, setStep] = useState('browsing');
9
- const activePreset = presets[cursor];
10
- const entries = useMemo(() => activePreset
11
- ? Object.entries(activePreset.env).sort(([a], [b]) => a.localeCompare(b))
12
- : [], [activePreset]);
13
- useInput((input, key) => {
14
- if (step === 'browsing') {
15
- if (key.escape || input.toLowerCase() === 'q') {
16
- exit();
17
- return;
18
- }
19
- if (key.upArrow || input === 'k') {
20
- setCursor((c) => Math.max(0, c - 1));
21
- return;
22
- }
23
- if (key.downArrow || input === 'j') {
24
- setCursor((c) => Math.min(presets.length - 1, c + 1));
25
- return;
26
- }
27
- if (key.return) {
28
- setStep('confirming');
29
- return;
30
- }
31
- }
32
- if (step === 'confirming') {
33
- if (input.toLowerCase() === 'y') {
34
- onSubmit(activePreset);
35
- exit();
36
- return;
37
- }
38
- if (input.toLowerCase() === 'n' || key.escape) {
39
- setStep('browsing');
40
- return;
41
- }
42
- }
43
- });
44
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Preset delete" }), _jsx(Text, { dimColor: true, children: step === 'browsing'
45
- ? '↑/k ↓/j navigate · Enter select · q exit'
46
- : 'y confirm · n cancel' }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "Presets" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: presets.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { children: index === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(preset.source === 'project' ? { color: 'yellow' } : {}), children: preset.name }), _jsxs(Text, { dimColor: true, children: [" (", preset.source, ")"] })] }, `${preset.source}:${preset.name}`))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: activePreset.name }), _jsx(Text, { dimColor: true, children: activePreset.source === 'project' ? 'Project preset' : 'Global preset' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: entries }) })] })] }), step === 'confirming' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "red", children: "Delete preset " }), _jsx(Text, { bold: true, children: activePreset.name }), _jsx(Text, { color: "red", children: "?" }), _jsx(Text, { dimColor: true, children: " y/n" })] }))] }));
47
- }