@bytevion/cli 0.2.0 → 0.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/dist/base.js +8 -1
- package/dist/commands/opt/preset.js +8 -5
- package/dist/commands/opt/show.js +20 -6
- package/dist/commands/providers/add.js +10 -2
- package/dist/commands/run.js +1 -1
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +210 -39
- package/dist/commands/usage.js +32 -8
- package/dist/hooks/init/home.js +41 -5
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +6 -0
- package/dist/lib/friendly.d.ts +8 -0
- package/dist/lib/friendly.js +86 -0
- package/dist/lib/home.d.ts +16 -0
- package/dist/lib/home.js +97 -0
- package/dist/lib/output.d.ts +3 -0
- package/dist/lib/output.js +43 -0
- package/dist/lib/presets.d.ts +9 -0
- package/dist/lib/presets.js +40 -0
- package/dist/lib/tui.d.ts +105 -0
- package/dist/lib/tui.gate.test.d.ts +1 -0
- package/dist/lib/tui.gate.test.js +96 -0
- package/dist/lib/tui.js +62 -0
- package/dist/lib/ui.js +13 -1
- package/dist/tui/__tests__/home.render.test.d.ts +1 -0
- package/dist/tui/__tests__/home.render.test.js +59 -0
- package/dist/tui/__tests__/state.test.d.ts +1 -0
- package/dist/tui/__tests__/state.test.js +88 -0
- package/dist/tui/components/App.d.ts +7 -0
- package/dist/tui/components/App.js +129 -0
- package/dist/tui/components/Brand.d.ts +9 -0
- package/dist/tui/components/Brand.js +13 -0
- package/dist/tui/components/Card.d.ts +11 -0
- package/dist/tui/components/Card.js +12 -0
- package/dist/tui/components/DoneScreen.d.ts +11 -0
- package/dist/tui/components/DoneScreen.js +30 -0
- package/dist/tui/components/Home.d.ts +12 -0
- package/dist/tui/components/Home.js +144 -0
- package/dist/tui/components/KeyStep.d.ts +13 -0
- package/dist/tui/components/KeyStep.js +44 -0
- package/dist/tui/components/Panel.d.ts +11 -0
- package/dist/tui/components/Panel.js +12 -0
- package/dist/tui/components/Picker.d.ts +19 -0
- package/dist/tui/components/Picker.js +68 -0
- package/dist/tui/components/PresetStep.d.ts +12 -0
- package/dist/tui/components/PresetStep.js +26 -0
- package/dist/tui/components/ProviderStep.d.ts +20 -0
- package/dist/tui/components/ProviderStep.js +159 -0
- package/dist/tui/components/Stepper.d.ts +9 -0
- package/dist/tui/components/Stepper.js +29 -0
- package/dist/tui/contract.d.ts +99 -0
- package/dist/tui/contract.js +5 -0
- package/dist/tui/index.d.ts +3 -0
- package/dist/tui/index.js +78 -0
- package/dist/tui/package.json +1 -0
- package/dist/tui/state.d.ts +77 -0
- package/dist/tui/state.js +84 -0
- package/dist/tui/theme.d.ts +23 -0
- package/dist/tui/theme.js +49 -0
- package/oclif.manifest.json +152 -150
- package/package.json +13 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { glyphs, tones } from '../theme.js';
|
|
6
|
+
// A keyboard-driven list: Up/Down (and j/k) move the cursor, Enter selects, with an optional
|
|
7
|
+
// type-to-filter box. Long lists window around the cursor so they never blow past the screen.
|
|
8
|
+
// The selected row shows the accent arrow + bold label; hints render dim and inline.
|
|
9
|
+
export function Picker({ items, onSelect, ascii, plainColor, initialValue, search = false, searchLabel = 'Search', pageSize = 8, isActive = true, }) {
|
|
10
|
+
const g = glyphs(ascii);
|
|
11
|
+
const tone = tones(plainColor);
|
|
12
|
+
const [query, setQuery] = React.useState('');
|
|
13
|
+
const filtered = React.useMemo(() => {
|
|
14
|
+
if (!query.trim())
|
|
15
|
+
return items;
|
|
16
|
+
const q = query.toLowerCase();
|
|
17
|
+
return items.filter((it) => it.label.toLowerCase().includes(q) || it.value.toLowerCase().includes(q));
|
|
18
|
+
}, [items, query]);
|
|
19
|
+
const startIdx = React.useMemo(() => {
|
|
20
|
+
if (!initialValue)
|
|
21
|
+
return 0;
|
|
22
|
+
const i = items.findIndex((it) => it.value === initialValue);
|
|
23
|
+
return i >= 0 ? i : 0;
|
|
24
|
+
}, [items, initialValue]);
|
|
25
|
+
const [cursor, setCursor] = React.useState(startIdx);
|
|
26
|
+
// Clamp the cursor whenever the filtered set shrinks under it.
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (cursor > filtered.length - 1)
|
|
29
|
+
setCursor(Math.max(0, filtered.length - 1));
|
|
30
|
+
}, [filtered.length, cursor]);
|
|
31
|
+
useInput((input, key) => {
|
|
32
|
+
if (key.upArrow || input === 'k') {
|
|
33
|
+
setCursor((c) => (c - 1 + filtered.length) % Math.max(1, filtered.length));
|
|
34
|
+
}
|
|
35
|
+
else if (key.downArrow || input === 'j') {
|
|
36
|
+
setCursor((c) => (c + 1) % Math.max(1, filtered.length));
|
|
37
|
+
}
|
|
38
|
+
else if (key.return) {
|
|
39
|
+
const picked = filtered[cursor];
|
|
40
|
+
if (picked)
|
|
41
|
+
onSelect(picked.value);
|
|
42
|
+
}
|
|
43
|
+
}, { isActive: isActive && !search });
|
|
44
|
+
// With search on, the TextInput owns the keystrokes; Up/Down still need to move the cursor,
|
|
45
|
+
// so a second handler runs only for the arrow keys while typing is active.
|
|
46
|
+
useInput((_input, key) => {
|
|
47
|
+
if (key.upArrow)
|
|
48
|
+
setCursor((c) => (c - 1 + filtered.length) % Math.max(1, filtered.length));
|
|
49
|
+
else if (key.downArrow)
|
|
50
|
+
setCursor((c) => (c + 1) % Math.max(1, filtered.length));
|
|
51
|
+
}, { isActive: isActive && search });
|
|
52
|
+
// Window the rows so the cursor stays visible without scrolling the whole terminal.
|
|
53
|
+
const total = filtered.length;
|
|
54
|
+
const half = Math.floor(pageSize / 2);
|
|
55
|
+
let from = Math.max(0, cursor - half);
|
|
56
|
+
const to = Math.min(total, from + pageSize);
|
|
57
|
+
from = Math.max(0, to - pageSize);
|
|
58
|
+
const window = filtered.slice(from, to);
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", children: [search ? (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsxs(Text, { color: tone.accent, children: [g.arrow, " "] }), _jsxs(Text, { color: tone.muted, children: [searchLabel, ": "] }), _jsx(TextInput, { value: query, onChange: setQuery, placeholder: "type to filter", onSubmit: () => {
|
|
60
|
+
const picked = filtered[cursor];
|
|
61
|
+
if (picked)
|
|
62
|
+
onSelect(picked.value);
|
|
63
|
+
} })] })) : null, from > 0 ? _jsx(Text, { color: tone.muted, children: ` ${g.bullet} ${from} more above` }) : null, window.map((it, i) => {
|
|
64
|
+
const idx = from + i;
|
|
65
|
+
const focused = idx === cursor;
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: focused ? tone.accent : undefined, children: focused ? g.arrow : ' ' }), _jsx(Text, { children: " " }), _jsx(Text, { bold: focused, color: focused ? tone.accent : undefined, children: it.label }), it.hint ? _jsx(Text, { color: tone.muted, children: ` ${it.hint}` }) : null] }, it.value));
|
|
67
|
+
}), to < total ? _jsx(Text, { color: tone.muted, children: ` ${g.bullet} ${total - to} more below` }) : null, total === 0 ? _jsx(Text, { color: tone.muted, children: " no matches" }) : null] }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { PresetCardDTO } from '../contract.js';
|
|
3
|
+
interface PresetStepProps {
|
|
4
|
+
cards: PresetCardDTO[];
|
|
5
|
+
ascii: boolean;
|
|
6
|
+
plainColor: boolean;
|
|
7
|
+
busy: boolean;
|
|
8
|
+
initial?: string;
|
|
9
|
+
onSelect: (value: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function PresetStep({ cards, ascii, plainColor, busy, initial, onSelect }: PresetStepProps): React.ReactElement;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { glyphs, tones } from '../theme.js';
|
|
6
|
+
import { Card } from './Card.js';
|
|
7
|
+
// Optimization-mode picker rendered as stacked cards (label + hint + wrapped blurb). Up/Down
|
|
8
|
+
// moves focus, Enter applies. Default focus lands on the passed initial (or 'maximum').
|
|
9
|
+
export function PresetStep({ cards, ascii, plainColor, busy, initial = 'maximum', onSelect }) {
|
|
10
|
+
const g = glyphs(ascii);
|
|
11
|
+
const tone = tones(plainColor);
|
|
12
|
+
const startIdx = Math.max(0, cards.findIndex((c) => c.value === initial));
|
|
13
|
+
const [cursor, setCursor] = React.useState(startIdx === -1 ? 0 : startIdx);
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (key.upArrow || input === 'k')
|
|
16
|
+
setCursor((c) => (c - 1 + cards.length) % cards.length);
|
|
17
|
+
else if (key.downArrow || input === 'j')
|
|
18
|
+
setCursor((c) => (c + 1) % cards.length);
|
|
19
|
+
else if (key.return)
|
|
20
|
+
onSelect(cards[cursor].value);
|
|
21
|
+
}, { isActive: !busy });
|
|
22
|
+
if (busy) {
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: tone.accent, children: _jsx(Spinner, { type: ascii ? 'line' : 'dots' }) }), _jsx(Text, { children: " Applying the optimization mode\u2026" })] }));
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: "Optimization mode" }), _jsx(Text, { color: tone.muted, children: `How hard Byte optimizes every request. ${g.divider} ↑↓ move · enter apply` }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: cards.map((c, i) => (_jsx(Box, { marginBottom: i < cards.length - 1 ? 1 : 0, children: _jsx(Card, { label: c.label, hint: c.hint, blurb: c.blurb, focused: i === cursor, ascii: ascii, plainColor: plainColor }) }, c.value))) })] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ProviderPresetDTO, WizardPorts } from '../contract.js';
|
|
3
|
+
interface ProviderStepProps {
|
|
4
|
+
presets: ProviderPresetDTO[];
|
|
5
|
+
ports: WizardPorts;
|
|
6
|
+
ascii: boolean;
|
|
7
|
+
plainColor: boolean;
|
|
8
|
+
busy: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
onConnected: (r: {
|
|
11
|
+
status: 'connected' | 'saved' | 'failed';
|
|
12
|
+
models: any[];
|
|
13
|
+
connId?: number;
|
|
14
|
+
}) => void;
|
|
15
|
+
onSkip: () => void;
|
|
16
|
+
onBusy: (busy: boolean) => void;
|
|
17
|
+
onError: (message: string) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function ProviderStep({ presets, ports, ascii, plainColor, busy, error, onConnected, onSkip, onBusy, onError, }: ProviderStepProps): React.ReactElement;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { glyphs, tones } from '../theme.js';
|
|
7
|
+
import { Picker } from './Picker.js';
|
|
8
|
+
// Provider connection step. Walks: choose a provider (searchable) → optional base-URL for
|
|
9
|
+
// custom/advanced presets → masked API key (live non-empty validation) → spinner while the port
|
|
10
|
+
// runs → on success hand back the models; on a saved-but-no-models result, drop into a
|
|
11
|
+
// retry/keep/skip recovery picker so a failed import never passes as a clean success.
|
|
12
|
+
export function ProviderStep({ presets, ports, ascii, plainColor, busy, error, onConnected, onSkip, onBusy, onError, }) {
|
|
13
|
+
const g = glyphs(ascii);
|
|
14
|
+
const tone = tones(plainColor);
|
|
15
|
+
const [phase, setPhase] = React.useState('pick');
|
|
16
|
+
const [preset, setPreset] = React.useState();
|
|
17
|
+
const [baseUrl, setBaseUrl] = React.useState('');
|
|
18
|
+
const [apiKey, setApiKey] = React.useState('');
|
|
19
|
+
const [keyTouched, setKeyTouched] = React.useState(false);
|
|
20
|
+
const [recover, setRecover] = React.useState();
|
|
21
|
+
const items = React.useMemo(() => [
|
|
22
|
+
...presets.map((p) => ({ value: p.id, label: p.label, hint: p.advanced ? 'advanced' : p.custom ? 'custom' : undefined })),
|
|
23
|
+
{ value: '__skip__', label: 'Skip — connect a provider later' },
|
|
24
|
+
], [presets]);
|
|
25
|
+
const beginConnect = React.useCallback(async (chosen, url, key) => {
|
|
26
|
+
setPhase('connecting');
|
|
27
|
+
onBusy(true);
|
|
28
|
+
try {
|
|
29
|
+
const res = await ports.connectProvider({
|
|
30
|
+
id: chosen.id,
|
|
31
|
+
label: chosen.label,
|
|
32
|
+
baseUrl: url || chosen.base_url || undefined,
|
|
33
|
+
apiKey: key,
|
|
34
|
+
mode: chosen.gateway_mode,
|
|
35
|
+
});
|
|
36
|
+
if (res.status === 'connected' && res.models.length) {
|
|
37
|
+
// Enable the first imported model right away so the gateway can route without a
|
|
38
|
+
// follow-up BYTE_ALIAS_NOT_CONFIGURED on the first run.
|
|
39
|
+
const first = res.models[0];
|
|
40
|
+
if (first?.id)
|
|
41
|
+
await ports.enableModel(first.id).catch(() => undefined);
|
|
42
|
+
onConnected({ status: 'connected', models: res.models, connId: res.connId });
|
|
43
|
+
}
|
|
44
|
+
else if (res.status === 'failed') {
|
|
45
|
+
setRecover({ message: res.error || 'Could not save the provider connection.', connId: res.connId });
|
|
46
|
+
setPhase('recover');
|
|
47
|
+
onBusy(false);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// saved (or connected w/o models): offer recovery rather than claiming success.
|
|
51
|
+
setRecover({ message: res.error || 'Saved, but its models could not be loaded.', connId: res.connId });
|
|
52
|
+
setPhase('recover');
|
|
53
|
+
onBusy(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
setRecover({ message: err?.message ?? String(err) });
|
|
58
|
+
setPhase('recover');
|
|
59
|
+
onBusy(false);
|
|
60
|
+
}
|
|
61
|
+
}, [ports, onBusy, onConnected]);
|
|
62
|
+
const onPickProvider = (value) => {
|
|
63
|
+
if (value === '__skip__') {
|
|
64
|
+
onSkip();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const p = presets.find((x) => x.id === value);
|
|
68
|
+
if (!p)
|
|
69
|
+
return;
|
|
70
|
+
setPreset(p);
|
|
71
|
+
setApiKey('');
|
|
72
|
+
setKeyTouched(false);
|
|
73
|
+
setBaseUrl(p.base_url || '');
|
|
74
|
+
if (p.custom || !p.base_url)
|
|
75
|
+
setPhase('baseUrl');
|
|
76
|
+
else
|
|
77
|
+
setPhase('apiKey');
|
|
78
|
+
};
|
|
79
|
+
const retryWithKey = React.useCallback(async (connId, key) => {
|
|
80
|
+
setPhase('connecting');
|
|
81
|
+
onBusy(true);
|
|
82
|
+
try {
|
|
83
|
+
const res = await ports.rotateAndTest(connId, key);
|
|
84
|
+
if (res.models.length) {
|
|
85
|
+
const first = res.models[0];
|
|
86
|
+
if (first?.id)
|
|
87
|
+
await ports.enableModel(first.id).catch(() => undefined);
|
|
88
|
+
onConnected({ status: 'connected', models: res.models, connId });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
setRecover({ message: res.error || 'Still no models returned.', connId });
|
|
92
|
+
setPhase('recover');
|
|
93
|
+
onBusy(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
setRecover({ message: err?.message ?? String(err), connId });
|
|
98
|
+
setPhase('recover');
|
|
99
|
+
onBusy(false);
|
|
100
|
+
}
|
|
101
|
+
}, [ports, onBusy, onConnected]);
|
|
102
|
+
// ---- render per phase ----
|
|
103
|
+
if (phase === 'pick') {
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: "Connect a provider" }), _jsx(Text, { color: tone.muted, children: "Bring your own key \u2014 Byte routes through it. Type to filter." }), _jsx(Box, { marginTop: 1, children: _jsx(Picker, { items: items, onSelect: onPickProvider, ascii: ascii, plainColor: plainColor, search: true, searchLabel: "Provider" }) }), error ? _jsx(Text, { color: tone.bad, children: `${g.bullet} ${error}` }) : null] }));
|
|
105
|
+
}
|
|
106
|
+
if (phase === 'baseUrl' && preset) {
|
|
107
|
+
const valid = baseUrl.trim().startsWith('http');
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: `${preset.label} — base URL` }), preset.note ? _jsx(Text, { color: tone.warn, children: `${g.bullet} ${preset.note}` }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Text, { color: tone.accent, children: [g.arrow, " "] }), _jsx(TextInput, { value: baseUrl, onChange: setBaseUrl, placeholder: "https://api.example.com/v1", onSubmit: () => {
|
|
109
|
+
if (valid)
|
|
110
|
+
setPhase('apiKey');
|
|
111
|
+
} })] }), !valid ? _jsx(Text, { color: tone.muted, children: "Must be a URL (starts with http)" }) : _jsx(Text, { color: tone.muted, children: "Enter to continue" })] }));
|
|
112
|
+
}
|
|
113
|
+
if (phase === 'apiKey' && preset) {
|
|
114
|
+
const valid = apiKey.trim().length > 0;
|
|
115
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: `${preset.label} — API key` }), preset.note ? _jsx(Text, { color: tone.warn, children: `${g.bullet} ${preset.note}` }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Text, { color: tone.accent, children: [g.arrow, " "] }), _jsx(TextInput, { value: apiKey, mask: "\u2022", onChange: (v) => {
|
|
116
|
+
setApiKey(v);
|
|
117
|
+
setKeyTouched(true);
|
|
118
|
+
}, placeholder: "paste your key", onSubmit: () => {
|
|
119
|
+
if (valid)
|
|
120
|
+
void beginConnect(preset, baseUrl, apiKey);
|
|
121
|
+
} })] }), keyTouched && !valid ? _jsx(Text, { color: tone.bad, children: "Required" }) : _jsx(Text, { color: tone.muted, children: "Enter to connect" })] }));
|
|
122
|
+
}
|
|
123
|
+
if (phase === 'connecting' || busy) {
|
|
124
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: tone.accent, children: _jsx(Spinner, { type: ascii ? 'line' : 'dots' }) }), _jsx(Text, { children: ` Connecting ${preset?.label ?? 'provider'}…` })] }));
|
|
125
|
+
}
|
|
126
|
+
// recover: saved but no models, or a hard failure — let the user retry the key, keep it, or skip.
|
|
127
|
+
if (phase === 'recover' && recover) {
|
|
128
|
+
const canRetry = recover.connId !== undefined;
|
|
129
|
+
return (_jsx(RecoverPhase, { message: recover.message, label: preset?.label ?? 'Provider', canRetry: canRetry, ascii: ascii, plainColor: plainColor, onRetry: (key) => recover.connId !== undefined && retryWithKey(recover.connId, key), onKeep: () => onConnected({ status: 'saved', models: [], connId: recover.connId }), onSkip: onSkip }));
|
|
130
|
+
}
|
|
131
|
+
return _jsx(Text, { color: tone.muted, children: "\u2026" });
|
|
132
|
+
}
|
|
133
|
+
// The retry/keep/skip recovery UI, with an inline masked key re-entry when "retry" is chosen.
|
|
134
|
+
function RecoverPhase({ message, label, canRetry, ascii, plainColor, onRetry, onKeep, onSkip, }) {
|
|
135
|
+
const g = glyphs(ascii);
|
|
136
|
+
const tone = tones(plainColor);
|
|
137
|
+
const [mode, setMode] = React.useState('choose');
|
|
138
|
+
const [key, setKey] = React.useState('');
|
|
139
|
+
const options = [
|
|
140
|
+
...(canRetry ? [{ value: 'retry', label: 'Re-enter the key and retry' }] : []),
|
|
141
|
+
{ value: 'keep', label: 'Keep it — I will pick a model later' },
|
|
142
|
+
{ value: 'skip', label: 'Skip the provider for now' },
|
|
143
|
+
];
|
|
144
|
+
if (mode === 'retryKey') {
|
|
145
|
+
const valid = key.trim().length > 0;
|
|
146
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: `${label} — API key` }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Text, { color: tone.accent, children: [g.arrow, " "] }), _jsx(TextInput, { value: key, mask: "\u2022", onChange: setKey, placeholder: "paste your key", onSubmit: () => {
|
|
147
|
+
if (valid)
|
|
148
|
+
onRetry(key);
|
|
149
|
+
} })] }), !valid ? _jsx(Text, { color: tone.bad, children: "Required" }) : _jsx(Text, { color: tone.muted, children: "Enter to retry" })] }));
|
|
150
|
+
}
|
|
151
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.warn, children: `${g.bullet} ${message}` }), _jsx(Box, { marginTop: 1, children: _jsx(Picker, { items: options, ascii: ascii, plainColor: plainColor, onSelect: (v) => {
|
|
152
|
+
if (v === 'retry')
|
|
153
|
+
setMode('retryKey');
|
|
154
|
+
else if (v === 'keep')
|
|
155
|
+
onKeep();
|
|
156
|
+
else
|
|
157
|
+
onSkip();
|
|
158
|
+
} }) })] }));
|
|
159
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Step } from '../state.js';
|
|
3
|
+
interface StepperProps {
|
|
4
|
+
current: Step;
|
|
5
|
+
ascii: boolean;
|
|
6
|
+
plainColor: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function Stepper({ current, ascii, plainColor }: StepperProps): React.ReactElement;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { glyphs, tones } from '../theme.js';
|
|
4
|
+
// Ordered labels for the visible wizard steps. 'welcome' is the entry and 'done' the exit, so
|
|
5
|
+
// the progress dots track the seven meaningful stops in between (welcome folds into the first).
|
|
6
|
+
const ORDER = [
|
|
7
|
+
{ step: 'welcome', label: 'Start' },
|
|
8
|
+
{ step: 'signin', label: 'Sign in' },
|
|
9
|
+
{ step: 'provider', label: 'Provider' },
|
|
10
|
+
{ step: 'model', label: 'Model' },
|
|
11
|
+
{ step: 'key', label: 'Key' },
|
|
12
|
+
{ step: 'preset', label: 'Mode' },
|
|
13
|
+
{ step: 'integrate', label: 'Connect' },
|
|
14
|
+
{ step: 'done', label: 'Done' },
|
|
15
|
+
];
|
|
16
|
+
// Horizontal 8-step tracker: completed steps get a filled tone-ok glyph, the active step a
|
|
17
|
+
// bright accent glyph + label, upcoming steps a muted empty glyph. Connectors are dim dividers.
|
|
18
|
+
export function Stepper({ current, ascii, plainColor }) {
|
|
19
|
+
const g = glyphs(ascii);
|
|
20
|
+
const tone = tones(plainColor);
|
|
21
|
+
const activeIdx = ORDER.findIndex((o) => o.step === current);
|
|
22
|
+
return (_jsx(Box, { flexDirection: "row", alignItems: "center", children: ORDER.map((o, i) => {
|
|
23
|
+
const done = i < activeIdx;
|
|
24
|
+
const active = i === activeIdx;
|
|
25
|
+
const color = done ? tone.ok : active ? tone.accent : tone.muted;
|
|
26
|
+
const dot = done ? g.check : active ? g.filled : g.empty;
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [_jsx(Text, { color: color, bold: active, children: dot }), _jsx(Text, { children: " " }), _jsx(Text, { color: color, bold: active, dimColor: !active && !done, children: o.label }), i < ORDER.length - 1 ? _jsx(Text, { color: tone.muted, children: ` ${g.divider} ` }) : null] }, o.step));
|
|
28
|
+
}) }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface HomeData {
|
|
2
|
+
email?: string;
|
|
3
|
+
org?: string;
|
|
4
|
+
providers?: number;
|
|
5
|
+
models?: number;
|
|
6
|
+
byteKeyMasked?: string;
|
|
7
|
+
gateway: 'healthy' | 'down' | 'unknown';
|
|
8
|
+
savingsSeries: number[];
|
|
9
|
+
savedTotal: number;
|
|
10
|
+
tokensTotal: number;
|
|
11
|
+
hitRate?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface HomeIslandProps {
|
|
14
|
+
signedIn: boolean;
|
|
15
|
+
data: HomeData;
|
|
16
|
+
plainColor: boolean;
|
|
17
|
+
ascii: boolean;
|
|
18
|
+
version: string;
|
|
19
|
+
}
|
|
20
|
+
export type HomeResult = {
|
|
21
|
+
status: 'fallback';
|
|
22
|
+
} | {
|
|
23
|
+
status: 'exit';
|
|
24
|
+
} | {
|
|
25
|
+
status: 'action';
|
|
26
|
+
commandId: string;
|
|
27
|
+
argv: string[];
|
|
28
|
+
};
|
|
29
|
+
export interface WizardPorts {
|
|
30
|
+
signIn(): Promise<{
|
|
31
|
+
email?: string;
|
|
32
|
+
}>;
|
|
33
|
+
listProviders(): Promise<any[]>;
|
|
34
|
+
connectProvider(a: {
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
apiKey: string;
|
|
39
|
+
mode: string;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
status: 'connected' | 'saved' | 'failed';
|
|
42
|
+
models: any[];
|
|
43
|
+
connId?: number;
|
|
44
|
+
error?: string;
|
|
45
|
+
}>;
|
|
46
|
+
rotateAndTest(connId: number, apiKey: string): Promise<{
|
|
47
|
+
models: any[];
|
|
48
|
+
error?: string;
|
|
49
|
+
}>;
|
|
50
|
+
enableModel(id: number): Promise<void>;
|
|
51
|
+
createByteKey(name: string, preset: string): Promise<{
|
|
52
|
+
key: string;
|
|
53
|
+
}>;
|
|
54
|
+
applyPreset(preset: string): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export interface ProviderPresetDTO {
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
base_url: string;
|
|
60
|
+
gateway_mode: string;
|
|
61
|
+
advanced?: boolean;
|
|
62
|
+
custom?: boolean;
|
|
63
|
+
note?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface PresetCardDTO {
|
|
66
|
+
value: string;
|
|
67
|
+
label: string;
|
|
68
|
+
hint: string;
|
|
69
|
+
blurb: string;
|
|
70
|
+
}
|
|
71
|
+
export interface WizardIslandProps {
|
|
72
|
+
plainColor: boolean;
|
|
73
|
+
ascii: boolean;
|
|
74
|
+
version: string;
|
|
75
|
+
initial: {
|
|
76
|
+
signedIn: boolean;
|
|
77
|
+
email?: string;
|
|
78
|
+
byteKey?: string;
|
|
79
|
+
};
|
|
80
|
+
ports: WizardPorts;
|
|
81
|
+
providerPresets: ProviderPresetDTO[];
|
|
82
|
+
presetCards: PresetCardDTO[];
|
|
83
|
+
}
|
|
84
|
+
export interface WizardSummary {
|
|
85
|
+
profile?: string;
|
|
86
|
+
model?: string;
|
|
87
|
+
provider: 'connected' | 'saved' | 'skipped' | 'failed';
|
|
88
|
+
preset: string;
|
|
89
|
+
byteKeyCreated: boolean;
|
|
90
|
+
connect?: string;
|
|
91
|
+
}
|
|
92
|
+
export type WizardResult = {
|
|
93
|
+
status: 'fallback';
|
|
94
|
+
} | {
|
|
95
|
+
status: 'cancelled';
|
|
96
|
+
} | {
|
|
97
|
+
status: 'done';
|
|
98
|
+
summary: WizardSummary;
|
|
99
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The ONLY types that cross the CJS↔ESM boundary. Zero react/ink imports so this file can
|
|
2
|
+
// be referenced from both the CommonJS CLI and the ESM Ink island without dragging the UI
|
|
3
|
+
// runtime into the control path. Keep these structural — the bridge in src/lib/tui.ts mirrors
|
|
4
|
+
// them so neither side has to import the other across the build boundary.
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { render } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { App } from './components/App.js';
|
|
4
|
+
import { Home } from './components/Home.js';
|
|
5
|
+
// ESM island entry. The CJS bridge (src/lib/tui.ts) dynamic-imports this and calls renderHome /
|
|
6
|
+
// renderWizard. Each mounts an Ink root, resolves the result Promise when the component reports
|
|
7
|
+
// done/exit/cancel, then ALWAYS unmounts and restores the cursor in a finally so we never leave
|
|
8
|
+
// the terminal in raw mode or with a hidden cursor — even if rendering throws.
|
|
9
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
10
|
+
export async function renderHome(props) {
|
|
11
|
+
let settled = false;
|
|
12
|
+
let resolve;
|
|
13
|
+
const done = new Promise((res) => {
|
|
14
|
+
resolve = (r) => {
|
|
15
|
+
if (!settled) {
|
|
16
|
+
settled = true;
|
|
17
|
+
res(r);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
const instance = render(React.createElement(Home, {
|
|
22
|
+
signedIn: props.signedIn,
|
|
23
|
+
data: props.data,
|
|
24
|
+
ascii: props.ascii,
|
|
25
|
+
plainColor: props.plainColor,
|
|
26
|
+
version: props.version,
|
|
27
|
+
onDone: resolve,
|
|
28
|
+
}), { exitOnCtrlC: false });
|
|
29
|
+
try {
|
|
30
|
+
// If Ink exits on its own (e.g. an unexpected unmount), fall back rather than hang.
|
|
31
|
+
instance.waitUntilExit().then(() => resolve({ status: 'exit' }), () => resolve({ status: 'fallback' }));
|
|
32
|
+
return await done;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
instance.unmount();
|
|
36
|
+
try {
|
|
37
|
+
process.stdout.write(SHOW_CURSOR);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* stream may be closed */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function renderWizard(props) {
|
|
45
|
+
let settled = false;
|
|
46
|
+
let resolve;
|
|
47
|
+
const done = new Promise((res) => {
|
|
48
|
+
resolve = (r) => {
|
|
49
|
+
if (!settled) {
|
|
50
|
+
settled = true;
|
|
51
|
+
res(r);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const instance = render(React.createElement(App, {
|
|
56
|
+
plainColor: props.plainColor,
|
|
57
|
+
ascii: props.ascii,
|
|
58
|
+
version: props.version,
|
|
59
|
+
initial: props.initial,
|
|
60
|
+
ports: props.ports,
|
|
61
|
+
providerPresets: props.providerPresets,
|
|
62
|
+
presetCards: props.presetCards,
|
|
63
|
+
onDone: resolve,
|
|
64
|
+
}), { exitOnCtrlC: false });
|
|
65
|
+
try {
|
|
66
|
+
instance.waitUntilExit().then(() => resolve({ status: 'cancelled' }), () => resolve({ status: 'fallback' }));
|
|
67
|
+
return await done;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
instance.unmount();
|
|
71
|
+
try {
|
|
72
|
+
process.stdout.write(SHOW_CURSOR);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* stream may be closed */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { WizardSummary } from './contract.js';
|
|
2
|
+
export type Step = 'welcome' | 'signin' | 'provider' | 'model' | 'key' | 'preset' | 'integrate' | 'done';
|
|
3
|
+
export declare const STEPS: Step[];
|
|
4
|
+
export type ProviderStatus = 'connected' | 'saved' | 'skipped' | 'failed' | 'pending';
|
|
5
|
+
export interface State {
|
|
6
|
+
step: Step;
|
|
7
|
+
busy: boolean;
|
|
8
|
+
error?: {
|
|
9
|
+
from: Step;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
signedIn: boolean;
|
|
13
|
+
email?: string;
|
|
14
|
+
providerChoice?: string;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
providerStatus: ProviderStatus;
|
|
17
|
+
connId?: number;
|
|
18
|
+
models: any[];
|
|
19
|
+
model?: string;
|
|
20
|
+
preset: string;
|
|
21
|
+
byteKey?: string;
|
|
22
|
+
byteKeyRevealed: boolean;
|
|
23
|
+
connect?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Initial {
|
|
26
|
+
signedIn: boolean;
|
|
27
|
+
email?: string;
|
|
28
|
+
byteKey?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function initialState(initial: Initial): State;
|
|
31
|
+
export type Action = {
|
|
32
|
+
type: 'NEXT';
|
|
33
|
+
} | {
|
|
34
|
+
type: 'BUSY';
|
|
35
|
+
busy: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'ERROR';
|
|
38
|
+
from: Step;
|
|
39
|
+
message: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'CLEAR_ERROR';
|
|
42
|
+
} | {
|
|
43
|
+
type: 'SIGNED_IN';
|
|
44
|
+
email?: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'PICK_PROVIDER';
|
|
47
|
+
id: string;
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'PROVIDER_RESULT';
|
|
51
|
+
status: 'connected' | 'saved' | 'failed';
|
|
52
|
+
models: any[];
|
|
53
|
+
connId?: number;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'PROVIDER_SKIP';
|
|
56
|
+
} | {
|
|
57
|
+
type: 'PROVIDER_RETRY';
|
|
58
|
+
} | {
|
|
59
|
+
type: 'PICK_MODEL';
|
|
60
|
+
model: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'KEY_CREATED';
|
|
63
|
+
key: string;
|
|
64
|
+
} | {
|
|
65
|
+
type: 'REVEAL_KEY';
|
|
66
|
+
} | {
|
|
67
|
+
type: 'PICK_PRESET';
|
|
68
|
+
preset: string;
|
|
69
|
+
} | {
|
|
70
|
+
type: 'PICK_CONNECT';
|
|
71
|
+
connect?: string;
|
|
72
|
+
} | {
|
|
73
|
+
type: 'GOTO';
|
|
74
|
+
step: Step;
|
|
75
|
+
};
|
|
76
|
+
export declare function reducer(state: State, action: Action): State;
|
|
77
|
+
export declare function selectSummary(state: State): WizardSummary;
|