@lkangd/cc-env 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README.zh.md +4 -0
- package/dist/cli.js +109 -80
- package/dist/commands/completion.js +24 -18
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/init.js +2 -9
- package/dist/commands/preset/create.js +70 -9
- package/dist/commands/restore.js +25 -1
- package/dist/commands/run.js +27 -22
- package/dist/core/claude-required-keys.js +8 -0
- package/dist/core/cli-name.js +8 -0
- package/dist/core/schema.js +10 -0
- package/dist/flows/preset-create-flow.js +49 -1
- package/dist/flows/restore-flow.js +26 -7
- 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 +92 -43
- package/dist/ink/preset-edit-app.js +12 -16
- package/dist/ink/restore-app.js +24 -9
- package/package.json +3 -2
- package/dist/ink/init-app.js +0 -54
|
@@ -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/dist/ink/restore-app.js
CHANGED
|
@@ -3,6 +3,21 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
3
3
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
4
|
import { advanceRestoreFlow } from '../flows/restore-flow.js';
|
|
5
5
|
import { EnvEntries, EnvSummary } from './summary.js';
|
|
6
|
+
export function getRestorePreviewSections(record) {
|
|
7
|
+
if (!record) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
if (record.action === 'init' || record.action === 'preset-create') {
|
|
11
|
+
return record.sources
|
|
12
|
+
.filter((source) => Object.keys(source.backup).length > 0)
|
|
13
|
+
.map((source) => ({
|
|
14
|
+
file: source.file,
|
|
15
|
+
entries: Object.entries(source.backup)
|
|
16
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
6
21
|
export function RestoreApp({ state, onSubmit, }) {
|
|
7
22
|
const { exit } = useApp();
|
|
8
23
|
const [currentState, setCurrentState] = useState(state);
|
|
@@ -13,9 +28,11 @@ export function RestoreApp({ state, onSubmit, }) {
|
|
|
13
28
|
? recordAtCursor
|
|
14
29
|
: selectedRecord ?? currentState.records[0];
|
|
15
30
|
const restoreEntries = useMemo(() => activeRecord
|
|
16
|
-
? Object.entries(activeRecord.action === 'init'
|
|
31
|
+
? Object.entries(activeRecord.action === 'init' || activeRecord.action === 'preset-create'
|
|
17
32
|
? Object.fromEntries(activeRecord.sources.flatMap((s) => Object.entries(s.backup)))
|
|
18
|
-
: activeRecord.
|
|
33
|
+
: activeRecord.action === 'restore'
|
|
34
|
+
? activeRecord.backup
|
|
35
|
+
: {}).sort(([left], [right]) => left.localeCompare(right))
|
|
19
36
|
: [], [activeRecord]);
|
|
20
37
|
const fromFiles = useMemo(() => {
|
|
21
38
|
if (!activeRecord || activeRecord.action !== 'init') {
|
|
@@ -23,12 +40,7 @@ export function RestoreApp({ state, onSubmit, }) {
|
|
|
23
40
|
}
|
|
24
41
|
return activeRecord.shellWrites.map((sw) => sw.filePath);
|
|
25
42
|
}, [activeRecord]);
|
|
26
|
-
const
|
|
27
|
-
if (!activeRecord || activeRecord.action !== 'init') {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
return activeRecord.sources.map((s) => s.file);
|
|
31
|
-
}, [activeRecord]);
|
|
43
|
+
const previewSections = useMemo(() => getRestorePreviewSections(activeRecord), [activeRecord]);
|
|
32
44
|
useEffect(() => {
|
|
33
45
|
setCurrentState(state);
|
|
34
46
|
setCursor(0);
|
|
@@ -96,7 +108,10 @@ export function RestoreApp({ state, onSubmit, }) {
|
|
|
96
108
|
exit();
|
|
97
109
|
}
|
|
98
110
|
});
|
|
99
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Restore record" }), currentState.step === 'record' ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "History" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: currentState.
|
|
111
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Restore record" }), currentState.step === 'record' ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "History" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: currentState.groups.map((group) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: group.title }), currentState.records.slice(group.start, group.end).map((record, index) => {
|
|
112
|
+
const actualIndex = group.start + index;
|
|
113
|
+
return (_jsxs(Text, { children: [actualIndex === cursor ? '❯ ' : ' ', record.timestamp] }, record.timestamp));
|
|
114
|
+
})] }, group.title))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Preview" }), activeRecord?.action === 'restore' ? (_jsxs(Text, { dimColor: true, children: ["Restore to ", activeRecord.targetType === 'preset' ? `preset ${activeRecord.targetName}` : activeRecord.targetType] })) : null, previewSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: section.file }), _jsx(EnvEntries, { entries: section.entries })] }, section.file))), previewSections.length === 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: restoreEntries }) })) : null] })] })] })) : null, currentState.step === 'target' ? (_jsxs(Text, { children: ["Select target for ", selectedRecord?.timestamp ?? 'record', ": settings or preset"] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'init' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp })] }), _jsx(EnvSummary, { title: "Will restore", entries: restoreEntries, ...(fromFiles.length > 0 ? { fromFiles } : {}), toFiles: previewSections.map((section) => section.file) })] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'preset-create' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp })] }), previewSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: section.file }), _jsx(EnvEntries, { entries: section.entries })] }, section.file)))] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'restore' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp }), " to", ' ', _jsx(Text, { color: "green", children: currentState.targetType === 'preset'
|
|
100
115
|
? `preset ${currentState.targetName}`
|
|
101
116
|
: currentState.targetType ?? 'settings' })] }), _jsx(EnvSummary, { title: "Will restore", entries: restoreEntries })] })) : null, currentState.step === 'done' ? (_jsxs(Text, { color: "green", children: ['\n', "Restore complete"] })) : null, currentState.step !== 'done' ? (_jsx(Text, { children: "Press Enter to confirm or q to cancel" })) : null] }));
|
|
102
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lkangd/cc-env",
|
|
3
|
-
"version": "1.
|
|
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",
|
package/dist/ink/init-app.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
-
import { advanceInitFlow, createInitFlowState, } from '../flows/init-flow.js';
|
|
5
|
-
export function InitApp({ keys = [], requiredKeys = [], sourceFiles = [], onSubmit, }) {
|
|
6
|
-
const { exit } = useApp();
|
|
7
|
-
const [cursor, setCursor] = useState(0);
|
|
8
|
-
const [flowState, setFlowState] = useState(() => createInitFlowState(keys, requiredKeys));
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (!onSubmit) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
if (keys.length === 0) {
|
|
14
|
-
onSubmit({ confirmed: false, selectedKeys: [] });
|
|
15
|
-
exit();
|
|
16
|
-
}
|
|
17
|
-
}, [exit, keys.length, onSubmit]);
|
|
18
|
-
useInput((input, key) => {
|
|
19
|
-
if (!onSubmit) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
if (key.upArrow || input === 'k') {
|
|
23
|
-
setCursor((c) => Math.max(0, c - 1));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (key.downArrow || input === 'j') {
|
|
27
|
-
setCursor((c) => Math.min(keys.length - 1, c + 1));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (input === ' ') {
|
|
31
|
-
const targetKey = keys[cursor];
|
|
32
|
-
if (targetKey) {
|
|
33
|
-
setFlowState((prev) => advanceInitFlow(prev, { type: 'toggle-key', key: targetKey }));
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (key.return) {
|
|
38
|
-
onSubmit({ confirmed: true, selectedKeys: flowState.selectedKeys });
|
|
39
|
-
exit();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (key.escape || input.toLowerCase() === 'q') {
|
|
43
|
-
onSubmit({ confirmed: false, selectedKeys: [] });
|
|
44
|
-
exit();
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env keys to migrate into managed shell config" }), sourceFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Source:" }), sourceFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null, _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle \u00B7 enter confirm \u00B7 q cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
|
|
48
|
-
const isRequired = requiredKeys.includes(key);
|
|
49
|
-
const isSelected = flowState.selectedKeys.includes(key);
|
|
50
|
-
const isCursor = i === cursor;
|
|
51
|
-
const checkbox = isSelected ? '[x]' : '[ ]';
|
|
52
|
-
return (_jsxs(Box, { children: [_jsx(Text, { children: isCursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: checkbox }), _jsxs(Text, { children: [" ", key] }), isRequired ? _jsx(Text, { dimColor: true, children: " (required)" }) : null] }, key));
|
|
53
|
-
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [flowState.selectedKeys.length, " of ", keys.length, " selected"] }) })] }));
|
|
54
|
-
}
|