@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 +4 -0
- package/README.zh.md +4 -0
- package/dist/cli.js +4 -3
- package/dist/commands/completion.js +24 -18
- package/dist/commands/doctor.js +2 -1
- package/dist/core/cli-name.js +8 -0
- package/dist/flows/preset-create-flow.js +1 -1
- package/dist/ink/components/text-input.js +7 -0
- package/dist/ink/hooks/use-text-input.js +71 -0
- package/dist/ink/preset-create-app.js +27 -43
- package/dist/ink/preset-edit-app.js +12 -16
- package/package.json +3 -2
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(
|
|
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 "
|
|
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 `#
|
|
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 `#
|
|
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
|
|
50
|
-
|
|
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
|
-
${
|
|
53
|
-
|
|
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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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 :
|
|
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)
|
|
@@ -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 (
|
|
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)" }),
|
|
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,
|
|
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" }),
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
+
textInput.reset();
|
|
254
246
|
return;
|
|
255
247
|
}
|
|
256
|
-
if (input
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
textInput.reset();
|
|
87
85
|
setError(undefined);
|
|
88
86
|
return;
|
|
89
87
|
}
|
|
90
|
-
if (input
|
|
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' }),
|
|
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.
|
|
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",
|