@lkangd/cc-env 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
  }
@@ -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.backup).sort(([left], [right]) => left.localeCompare(right))
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 toFiles = useMemo(() => {
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.records.map((record, index) => (_jsxs(Text, { children: [index === cursor ? '❯ ' : ' ', record.timestamp] }, record.timestamp))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Preview" }), activeRecord?.action === 'init' ? (_jsxs(Box, { flexDirection: "column", children: [fromFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "From:" }), fromFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null, toFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "To:" }), toFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null] })) : (_jsxs(Text, { dimColor: true, children: ["Restore to ", activeRecord?.targetType === 'preset' ? `preset ${activeRecord.targetName}` : activeRecord?.targetType ?? 'settings'] })), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: restoreEntries }) })] })] })] })) : 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.length > 0 ? { toFiles } : {}) })] })) : 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 ?? 'record' }), " to", ' ', _jsx(Text, { color: "green", children: currentState.targetType === 'preset'
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.2.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",
@@ -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
- }