@bytevion/cli 0.3.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.
Files changed (48) hide show
  1. package/dist/commands/providers/add.js +10 -2
  2. package/dist/commands/run.js +1 -1
  3. package/dist/commands/setup.d.ts +1 -0
  4. package/dist/commands/setup.js +96 -1
  5. package/dist/hooks/init/home.js +24 -0
  6. package/dist/lib/api.d.ts +1 -0
  7. package/dist/lib/api.js +6 -0
  8. package/dist/lib/tui.d.ts +105 -0
  9. package/dist/lib/tui.gate.test.d.ts +1 -0
  10. package/dist/lib/tui.gate.test.js +96 -0
  11. package/dist/lib/tui.js +62 -0
  12. package/dist/tui/__tests__/home.render.test.d.ts +1 -0
  13. package/dist/tui/__tests__/home.render.test.js +59 -0
  14. package/dist/tui/__tests__/state.test.d.ts +1 -0
  15. package/dist/tui/__tests__/state.test.js +88 -0
  16. package/dist/tui/components/App.d.ts +7 -0
  17. package/dist/tui/components/App.js +129 -0
  18. package/dist/tui/components/Brand.d.ts +9 -0
  19. package/dist/tui/components/Brand.js +13 -0
  20. package/dist/tui/components/Card.d.ts +11 -0
  21. package/dist/tui/components/Card.js +12 -0
  22. package/dist/tui/components/DoneScreen.d.ts +11 -0
  23. package/dist/tui/components/DoneScreen.js +30 -0
  24. package/dist/tui/components/Home.d.ts +12 -0
  25. package/dist/tui/components/Home.js +144 -0
  26. package/dist/tui/components/KeyStep.d.ts +13 -0
  27. package/dist/tui/components/KeyStep.js +44 -0
  28. package/dist/tui/components/Panel.d.ts +11 -0
  29. package/dist/tui/components/Panel.js +12 -0
  30. package/dist/tui/components/Picker.d.ts +19 -0
  31. package/dist/tui/components/Picker.js +68 -0
  32. package/dist/tui/components/PresetStep.d.ts +12 -0
  33. package/dist/tui/components/PresetStep.js +26 -0
  34. package/dist/tui/components/ProviderStep.d.ts +20 -0
  35. package/dist/tui/components/ProviderStep.js +159 -0
  36. package/dist/tui/components/Stepper.d.ts +9 -0
  37. package/dist/tui/components/Stepper.js +29 -0
  38. package/dist/tui/contract.d.ts +99 -0
  39. package/dist/tui/contract.js +5 -0
  40. package/dist/tui/index.d.ts +3 -0
  41. package/dist/tui/index.js +78 -0
  42. package/dist/tui/package.json +1 -0
  43. package/dist/tui/state.d.ts +77 -0
  44. package/dist/tui/state.js +84 -0
  45. package/dist/tui/theme.d.ts +23 -0
  46. package/dist/tui/theme.js +49 -0
  47. package/oclif.manifest.json +151 -151
  48. package/package.json +13 -3
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import React from 'react';
5
+ import { initialState, reducer, selectSummary } from '../state.js';
6
+ import { glyphs, tones } from '../theme.js';
7
+ import { Brand } from './Brand.js';
8
+ import { DoneScreen } from './DoneScreen.js';
9
+ import { KeyStep } from './KeyStep.js';
10
+ import { Picker } from './Picker.js';
11
+ import { PresetStep } from './PresetStep.js';
12
+ import { ProviderStep } from './ProviderStep.js';
13
+ import { Stepper } from './Stepper.js';
14
+ // Tools offered at the integrate step — mirrors the clack setup list so both paths wire the
15
+ // same harnesses.
16
+ const TOOLS = [
17
+ { value: 'skip', label: 'Skip for now' },
18
+ { value: 'opencode', label: 'opencode' },
19
+ { value: 'claude-code', label: 'Claude Code' },
20
+ { value: 'codex', label: 'Codex CLI' },
21
+ { value: 'cursor', label: 'Cursor' },
22
+ { value: 'cline', label: 'Cline' },
23
+ { value: 'aider', label: 'Aider' },
24
+ ];
25
+ // The wizard root. Owns the reducer + all side effects (it calls the ports and dispatches the
26
+ // results), draws the brand + 8-step stepper + the active step, and resolves the island Promise
27
+ // via onDone. Ctrl-C / Esc at any step → {status:'cancelled'} so nothing is half-applied
28
+ // without the user knowing.
29
+ export function App({ plainColor, ascii, version, initial, ports, providerPresets, presetCards, onDone, }) {
30
+ const g = glyphs(ascii);
31
+ const tone = tones(plainColor);
32
+ const { exit } = useApp();
33
+ const [state, dispatch] = React.useReducer(reducer, initial, initialState);
34
+ const finish = React.useCallback((result) => {
35
+ exit();
36
+ onDone(result);
37
+ }, [exit, onDone]);
38
+ useInput((input, key) => {
39
+ if (key.escape || (key.ctrl && input === 'c'))
40
+ finish({ status: 'cancelled' });
41
+ });
42
+ // ---- welcome: Enter to begin (skip sign-in if already authed) ----
43
+ const beginWelcome = React.useCallback(() => {
44
+ dispatch({ type: 'GOTO', step: initial.signedIn ? 'provider' : 'signin' });
45
+ }, [initial.signedIn]);
46
+ // ---- sign-in: run the device-login port ----
47
+ const runSignIn = React.useCallback(async () => {
48
+ dispatch({ type: 'BUSY', busy: true });
49
+ try {
50
+ const r = await ports.signIn();
51
+ dispatch({ type: 'SIGNED_IN', email: r.email });
52
+ }
53
+ catch (err) {
54
+ dispatch({ type: 'ERROR', from: 'signin', message: err?.message ?? String(err) });
55
+ }
56
+ }, [ports]);
57
+ // ---- key: create via the port, reveal once ----
58
+ const createKey = React.useCallback(async (name) => {
59
+ dispatch({ type: 'BUSY', busy: true });
60
+ try {
61
+ const r = await ports.createByteKey(name, state.preset);
62
+ dispatch({ type: 'KEY_CREATED', key: r.key });
63
+ }
64
+ catch (err) {
65
+ dispatch({ type: 'ERROR', from: 'key', message: err?.message ?? String(err) });
66
+ }
67
+ }, [ports, state.preset]);
68
+ // ---- preset: apply to the org, then advance ----
69
+ const applyPreset = React.useCallback(async (preset) => {
70
+ dispatch({ type: 'BUSY', busy: true });
71
+ try {
72
+ await ports.applyPreset(preset);
73
+ }
74
+ catch {
75
+ // The clack path also treats this as best-effort; never block the wizard on it.
76
+ }
77
+ dispatch({ type: 'PICK_PRESET', preset });
78
+ }, [ports]);
79
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Brand, { ascii: ascii, plainColor: plainColor, version: version }), _jsx(Box, { marginTop: 1, children: _jsx(Stepper, { current: state.step, ascii: ascii, plainColor: plainColor }) }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(StepBody, { state: state, ascii: ascii, plainColor: plainColor, version: version, ports: ports, providerPresets: providerPresets, presetCards: presetCards, tools: TOOLS, dispatch: dispatch, onBeginWelcome: beginWelcome, onSignIn: runSignIn, onCreateKey: createKey, onApplyPreset: applyPreset, onFinish: finish, glyphArrow: g.arrow, glyphBullet: g.bullet, muted: tone.muted, accent: tone.accent, bad: tone.bad }) }), state.error ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: tone.bad, children: `${g.bullet} ${state.error.message}` }) })) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: tone.muted, children: `esc cancel ${g.divider} Byte never claims success it can't prove` }) })] }));
80
+ }
81
+ // Step router. Kept as a plain function (not a component) so the parent owns all hooks/state and
82
+ // the per-step UI stays declarative.
83
+ function StepBody(props) {
84
+ const { state, ascii, plainColor, version, ports, providerPresets, presetCards, tools, dispatch, onBeginWelcome, onSignIn, onCreateKey, onApplyPreset, onFinish, muted, accent, } = props;
85
+ switch (state.step) {
86
+ case 'welcome':
87
+ return _jsx(WelcomeStep, { signedIn: state.signedIn, email: state.email, accent: accent, muted: muted, onBegin: onBeginWelcome });
88
+ case 'signin':
89
+ return _jsx(SignInStep, { busy: state.busy, ascii: ascii, accent: accent, muted: muted, onSignIn: onSignIn });
90
+ case 'provider':
91
+ return (_jsx(ProviderStep, { presets: providerPresets, ports: ports, ascii: ascii, plainColor: plainColor, busy: state.busy, error: state.error?.from === 'provider' ? state.error.message : undefined, onConnected: (r) => dispatch({ type: 'PROVIDER_RESULT', status: r.status, models: r.models, connId: r.connId }), onSkip: () => dispatch({ type: 'PROVIDER_SKIP' }), onBusy: (busy) => dispatch({ type: 'BUSY', busy }), onError: (message) => dispatch({ type: 'ERROR', from: 'provider', message }) }));
92
+ case 'model':
93
+ return (_jsx(ModelStep, { models: state.models, ascii: ascii, plainColor: plainColor, accent: accent, muted: muted, onPick: (model) => dispatch({ type: 'PICK_MODEL', model }) }));
94
+ case 'key':
95
+ return (_jsx(KeyStep, { ascii: ascii, plainColor: plainColor, busy: state.busy, preset: state.preset, byteKey: state.byteKeyRevealed ? state.byteKey : undefined, existingKey: !state.byteKeyRevealed ? state.byteKey : undefined, onCreate: onCreateKey, onContinue: () => dispatch({ type: 'NEXT' }) }));
96
+ case 'preset':
97
+ return (_jsx(PresetStep, { cards: presetCards, ascii: ascii, plainColor: plainColor, busy: state.busy, initial: state.preset, onSelect: onApplyPreset }));
98
+ case 'integrate':
99
+ return (_jsx(IntegrateStep, { tools: tools, ascii: ascii, plainColor: plainColor, accent: accent, muted: muted, onPick: (connect) => dispatch({ type: 'PICK_CONNECT', connect }) }));
100
+ case 'done':
101
+ return (_jsx(DoneScreen, { summary: selectSummary(state), ascii: ascii, plainColor: plainColor, version: version, onClose: () => onFinish({ status: 'done', summary: selectSummary(state) }) }));
102
+ default:
103
+ return _jsx(Text, { children: " " });
104
+ }
105
+ }
106
+ function WelcomeStep({ signedIn, email, accent, muted, onBegin, }) {
107
+ useInput((_input, key) => {
108
+ if (key.return)
109
+ onBegin();
110
+ });
111
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: accent, bold: true, children: "Welcome to Byte" }), _jsx(Text, { color: muted, children: "Sign in, connect a provider, create a key, pick a mode, and wire a tool \u2014 a few keystrokes." }), signedIn ? _jsx(Text, { color: muted, children: `Already signed in${email ? ` as ${email}` : ''}.` }) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: muted, children: "Press enter to begin" }) })] }));
112
+ }
113
+ function SignInStep({ busy, ascii, accent, muted, onSignIn, }) {
114
+ useInput((_input, key) => {
115
+ if (key.return)
116
+ onSignIn();
117
+ }, { isActive: !busy });
118
+ if (busy) {
119
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: accent, children: _jsx(Spinner, { type: ascii ? 'line' : 'dots' }) }), _jsx(Text, { children: " Opening your browser \u2014 approve the device there\u2026" })] }));
120
+ }
121
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: accent, bold: true, children: "Sign in" }), _jsx(Text, { color: muted, children: "We'll open your browser for a one-time device approval." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: muted, children: "Press enter to sign in" }) })] }));
122
+ }
123
+ function ModelStep({ models, ascii, plainColor, accent, muted, onPick, }) {
124
+ const items = React.useMemo(() => models.slice(0, 50).map((m) => ({ value: m.byte_alias, label: m.byte_alias, hint: m.model_id })), [models]);
125
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: accent, bold: true, children: "Default model" }), _jsx(Text, { color: muted, children: "Pick the model your tools route to by default. Type to filter." }), _jsx(Box, { marginTop: 1, children: _jsx(Picker, { items: items, onSelect: onPick, ascii: ascii, plainColor: plainColor, search: true, searchLabel: "Model" }) })] }));
126
+ }
127
+ function IntegrateStep({ tools, ascii, plainColor, accent, muted, onPick, }) {
128
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: accent, bold: true, children: "Connect a coding tool" }), _jsx(Text, { color: muted, children: "Wire a harness now, or skip and do it later with `byte integrate`." }), _jsx(Box, { marginTop: 1, children: _jsx(Picker, { items: tools, onSelect: onPick, ascii: ascii, plainColor: plainColor, initialValue: "skip" }) })] }));
129
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface BrandProps {
3
+ ascii: boolean;
4
+ plainColor: boolean;
5
+ version: string;
6
+ tagline?: string;
7
+ }
8
+ export declare function Brand({ ascii, plainColor, version, tagline }: BrandProps): React.ReactElement;
9
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { glyphs, gradient, tones } from '../theme.js';
4
+ // The Byte signature: a dim `›b_` prompt glyph followed by BYTE rendered one character at a
5
+ // time across the cyan→blue gradient, with a muted tagline + version to the right. Under
6
+ // plainColor the gradient collapses to bold plain text; under ascii the glyph swaps to `>b_`.
7
+ export function Brand({ ascii, plainColor, version, tagline = 'optimization gateway' }) {
8
+ const g = glyphs(ascii);
9
+ const tone = tones(plainColor);
10
+ const letters = 'BYTE'.split('');
11
+ const ramp = gradient(letters.length);
12
+ return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [_jsxs(Text, { children: [_jsx(Text, { color: tone.muted, children: g.brand[0] }), _jsx(Text, { color: tone.accent, bold: true, children: g.brand[1] }), _jsx(Text, { color: tone.muted, children: g.brand[2] })] }), _jsx(Text, { children: " " }), _jsx(Text, { children: letters.map((ch, i) => (_jsx(Text, { bold: true, color: plainColor ? undefined : ramp[i], children: ch }, i))) }), _jsx(Text, { children: " " }), _jsx(Text, { color: tone.muted, children: tagline }), _jsx(Text, { color: tone.muted, children: ` · v${version}` })] }));
13
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ interface CardProps {
3
+ label: string;
4
+ hint?: string;
5
+ blurb?: string;
6
+ focused: boolean;
7
+ ascii: boolean;
8
+ plainColor: boolean;
9
+ }
10
+ export declare function Card({ label, hint, blurb, focused, ascii, plainColor }: CardProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { tones } from '../theme.js';
4
+ // A selectable card: when focused it grows a left accent bar and brightens the label; the hint
5
+ // sits inline (dim) and an optional blurb wraps underneath. Unfocused cards stay quiet so the
6
+ // selection reads at a glance. The bar is a solid block (unicode) or '|' (ascii).
7
+ export function Card({ label, hint, blurb, focused, ascii, plainColor }) {
8
+ const tone = tones(plainColor);
9
+ const bar = ascii ? '|' : '▌';
10
+ const accent = focused ? tone.accent : undefined;
11
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: focused ? tone.accent : tone.muted, children: focused ? bar : ' ' }), _jsx(Text, { children: " " }), _jsx(Text, { bold: focused, color: accent, children: label }), hint ? _jsx(Text, { color: tone.muted, children: ` ${hint}` }) : null] }), blurb ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: ' ' }), _jsx(Text, { color: tone.muted, dimColor: true, wrap: "wrap", children: blurb })] })) : null] }));
12
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import type { WizardSummary } from '../contract.js';
3
+ interface DoneScreenProps {
4
+ summary: WizardSummary;
5
+ ascii: boolean;
6
+ plainColor: boolean;
7
+ version: string;
8
+ onClose: () => void;
9
+ }
10
+ export declare function DoneScreen({ summary, ascii, plainColor, version, onClose }: DoneScreenProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { glyphs, gradient, tones } from '../theme.js';
4
+ import { Brand } from './Brand.js';
5
+ // The celebratory close-out. A big gradient check, a tidy summary of what got wired, and the
6
+ // exact next commands to run. The summary never over-claims: a provider that only saved (no
7
+ // models) says so, and tips adapt accordingly. Enter closes the wizard.
8
+ export function DoneScreen({ summary, ascii, plainColor, version, onClose }) {
9
+ const g = glyphs(ascii);
10
+ const tone = tones(plainColor);
11
+ const ramp = gradient(3);
12
+ useInput((_input, key) => {
13
+ if (key.return || key.escape)
14
+ onClose();
15
+ });
16
+ const providerLine = summary.provider === 'connected'
17
+ ? { color: tone.ok, text: summary.model ? `connected · ${summary.model}` : 'connected' }
18
+ : summary.provider === 'saved'
19
+ ? { color: tone.warn, text: 'saved (finish the key/URL, then `byte providers test`)' }
20
+ : summary.provider === 'failed'
21
+ ? { color: tone.bad, text: 'failed' }
22
+ : { color: tone.muted, text: 'skipped' };
23
+ const tips = ['byte run "hello from byte"', 'byte usage'];
24
+ if (summary.provider === 'skipped')
25
+ tips.unshift('byte providers add');
26
+ if (summary.provider === 'saved')
27
+ tips.unshift('byte providers test');
28
+ const Line = ({ label, children }) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: tone.muted, children: label }) }), _jsx(Text, { children: children })] }));
29
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [_jsx(Text, { bold: true, color: plainColor ? undefined : ramp[0], children: g.check }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: tone.ok, children: "Setup complete." })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: g.border, borderColor: tone.ok, paddingX: 2, paddingY: 1, children: [_jsx(Brand, { ascii: ascii, plainColor: plainColor, version: version, tagline: "you are wired up" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Line, { label: "Provider", children: _jsx(Text, { color: providerLine.color, children: providerLine.text }) }), summary.model ? (_jsx(Line, { label: "Model", children: _jsx(Text, { children: summary.model }) })) : null, _jsxs(Line, { label: "Mode", children: [_jsx(Text, { children: summary.preset }), _jsx(Text, { color: tone.muted, children: " applied" })] }), _jsx(Line, { label: "Byte key", children: _jsx(Text, { color: summary.byteKeyCreated ? tone.ok : tone.muted, children: summary.byteKeyCreated ? 'created' : '—' }) }), summary.connect ? (_jsxs(Line, { label: "Tool", children: [_jsx(Text, { color: tone.accent, children: summary.connect }), _jsx(Text, { color: tone.muted, children: " queued" })] })) : null] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: tone.muted, children: "Next" }), tips.map((t) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: tone.accent, children: ` ${g.arrow} ` }), _jsx(Text, { color: tone.accent, children: t })] }, t)))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: tone.muted, children: "Press enter to finish" }) })] }));
30
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { HomeData, HomeResult } from '../contract.js';
3
+ interface HomeProps {
4
+ signedIn: boolean;
5
+ data: HomeData;
6
+ ascii: boolean;
7
+ plainColor: boolean;
8
+ version: string;
9
+ onDone: (result: HomeResult) => void;
10
+ }
11
+ export declare function Home({ signedIn, data, ascii, plainColor, version, onDone }: HomeProps): React.ReactElement;
12
+ export {};
@@ -0,0 +1,144 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import React from 'react';
5
+ import { glyphs, gradient, tones } from '../theme.js';
6
+ import { Brand } from './Brand.js';
7
+ import { Picker } from './Picker.js';
8
+ // Actions that need a single text argument before they can run, with the prompt label and the
9
+ // same defaults the clack menu used — so the Ink path collects them inline rather than firing a
10
+ // command that would fail oclif's required-arg check.
11
+ const NEEDS_INPUT = {
12
+ run: { label: 'Prompt', placeholder: 'hello from byte', fallback: 'hello from byte' },
13
+ compare: { label: 'Prompt to compare', placeholder: 'Summarize the latest changes', fallback: 'Summarize the latest changes' },
14
+ 'keys:create': { label: 'Key name', placeholder: 'cli', fallback: 'cli' },
15
+ };
16
+ const fmtUsd = (n) => `$${(Number.isFinite(n) ? n : 0).toFixed(n >= 1 ? 2 : 4)}`;
17
+ const fmtNum = (v) => {
18
+ const n = Number.isFinite(v) ? v : 0;
19
+ const abs = Math.abs(n);
20
+ if (abs >= 1_000_000)
21
+ return `${(n / 1_000_000).toFixed(1)}M`;
22
+ if (abs >= 1_000)
23
+ return `${(n / 1_000).toFixed(1)}K`;
24
+ return String(Math.round(n));
25
+ };
26
+ const fmtPct = (v) => {
27
+ if (v === undefined || !Number.isFinite(v))
28
+ return undefined;
29
+ const scaled = v <= 1 ? v * 100 : v;
30
+ return `${scaled.toFixed(1)}%`;
31
+ };
32
+ // Block sparkline driven by the theme ramp. Each value maps to a glyph and gets a per-column
33
+ // color from the cyan→blue gradient so the series reads as a little branded bar chart.
34
+ function Sparkline({ values, ascii, plainColor }) {
35
+ const g = glyphs(ascii);
36
+ const nums = values.map((v) => (Number.isFinite(v) ? v : 0));
37
+ if (!nums.length)
38
+ return _jsx(Text, { color: tones(plainColor).muted, children: "no activity yet" });
39
+ const max = Math.max(...nums);
40
+ const min = Math.min(...nums);
41
+ const span = max - min || 1;
42
+ const ramp = gradient(nums.length);
43
+ return (_jsx(Text, { children: nums.map((v, i) => {
44
+ const idx = Math.round(((v - min) / span) * (g.spark.length - 1));
45
+ const ch = g.spark[Math.max(0, Math.min(g.spark.length - 1, idx))];
46
+ return (_jsx(Text, { color: plainColor ? undefined : ramp[i], children: ch }, i));
47
+ }) }));
48
+ }
49
+ // Eases the saved-$ figure from 0 → target over ~700ms on mount, a small touch of life when the
50
+ // screen opens. Disabled under ascii/plainColor (and when nothing was saved) so it never
51
+ // animates a static or non-color terminal — those just show the final number immediately.
52
+ function useSavingsTicker(target, animate) {
53
+ const [value, setValue] = React.useState(animate ? 0 : target);
54
+ React.useEffect(() => {
55
+ if (!animate || target <= 0) {
56
+ setValue(target);
57
+ return;
58
+ }
59
+ const duration = 700;
60
+ const stepMs = 40;
61
+ const steps = Math.max(1, Math.round(duration / stepMs));
62
+ let frame = 0;
63
+ const id = setInterval(() => {
64
+ frame += 1;
65
+ const t = frame / steps;
66
+ // ease-out cubic
67
+ const eased = 1 - Math.pow(1 - t, 3);
68
+ setValue(t >= 1 ? target : target * eased);
69
+ if (frame >= steps)
70
+ clearInterval(id);
71
+ }, stepMs);
72
+ return () => clearInterval(id);
73
+ }, [target, animate]);
74
+ return value;
75
+ }
76
+ const Row = ({ label, children, plainColor }) => {
77
+ const tone = tones(plainColor);
78
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 11, children: _jsx(Text, { color: tone.muted, children: label }) }), _jsx(Text, { children: children })] }));
79
+ };
80
+ // The live home dashboard. A bordered card carries identity, providers, the masked key, a
81
+ // gateway health dot, and a 7-day savings sparkline with an animated total; below it a
82
+ // quick-actions Picker resolves to {status:'action'} (run a command) or {status:'exit'}.
83
+ export function Home({ signedIn, data, ascii, plainColor, version, onDone }) {
84
+ const g = glyphs(ascii);
85
+ const tone = tones(plainColor);
86
+ const { exit } = useApp();
87
+ const hasSavings = data.savingsSeries.some((v) => v > 0) || data.savedTotal > 0;
88
+ const animate = !ascii && !plainColor && hasSavings;
89
+ const saved = useSavingsTicker(data.savedTotal, animate);
90
+ // When a chosen action needs a text argument, we switch into an inline input phase rather than
91
+ // resolving immediately. null = the quick-actions menu is showing.
92
+ const [pending, setPending] = React.useState(null);
93
+ const [inputValue, setInputValue] = React.useState('');
94
+ // Ctrl-C / Esc / q → exit. Disabled while typing so those chars reach the TextInput. Picker
95
+ // owns Up/Down/Enter on the menu; this only adds the escapes.
96
+ useInput((input, key) => {
97
+ if (key.escape || input === 'q' || (key.ctrl && input === 'c')) {
98
+ exit();
99
+ onDone({ status: 'exit' });
100
+ }
101
+ }, { isActive: pending === null });
102
+ const gateway = data.gateway === 'healthy'
103
+ ? { color: tone.ok, text: 'healthy' }
104
+ : data.gateway === 'down'
105
+ ? { color: tone.bad, text: 'unreachable' }
106
+ : { color: tone.muted, text: 'unknown' };
107
+ const actions = [
108
+ { value: 'setup', label: 'Set up Byte', hint: 'guided' },
109
+ { value: 'providers:add', label: 'Connect a provider' },
110
+ { value: 'keys:create', label: 'Create a Byte key' },
111
+ { value: 'integrate', label: 'Connect a tool' },
112
+ { value: 'run', label: 'Run a test prompt' },
113
+ { value: 'compare', label: 'Compare direct vs Byte', hint: 'prove the win' },
114
+ { value: 'usage', label: 'View usage' },
115
+ { value: signedIn ? 'logout' : 'login', label: signedIn ? 'Sign out' : 'Sign in' },
116
+ { value: 'exit', label: 'Exit' },
117
+ ];
118
+ const onAction = (value) => {
119
+ if (value === 'exit') {
120
+ exit();
121
+ onDone({ status: 'exit' });
122
+ return;
123
+ }
124
+ if (NEEDS_INPUT[value]) {
125
+ setInputValue('');
126
+ setPending(value);
127
+ return;
128
+ }
129
+ exit();
130
+ onDone({ status: 'action', commandId: value, argv: [] });
131
+ };
132
+ const submitInput = (commandId, raw) => {
133
+ const spec = NEEDS_INPUT[commandId];
134
+ const argv = [raw.trim() || spec.fallback];
135
+ exit();
136
+ onDone({ status: 'action', commandId, argv });
137
+ };
138
+ // Inline input phase: a single labelled TextInput for run/compare/keys:create.
139
+ if (pending) {
140
+ const spec = NEEDS_INPUT[pending];
141
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: spec.label }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Text, { color: tone.accent, children: [g.arrow, " "] }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, placeholder: spec.placeholder, onSubmit: (v) => submitInput(pending, v) })] }), _jsx(Text, { color: tone.muted, children: "Enter to run" })] }));
142
+ }
143
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: g.border, borderColor: tone.accent, paddingX: 2, paddingY: 1, children: [_jsx(Brand, { ascii: ascii, plainColor: plainColor, version: version }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Row, { label: "Account", plainColor: plainColor, children: signedIn ? (_jsxs(Text, { children: [data.email ?? 'you', data.org ? _jsx(Text, { color: tone.muted, children: ` · ${data.org}` }) : null] })) : (_jsx(Text, { color: tone.warn, children: "not signed in" })) }), _jsx(Row, { label: "Providers", plainColor: plainColor, children: data.providers ? (_jsxs(Text, { children: [data.providers, " connected", data.models ? _jsx(Text, { color: tone.muted, children: ` · ${data.models} models` }) : null] })) : (_jsx(Text, { color: tone.muted, children: "\u2014" })) }), _jsx(Row, { label: "Byte key", plainColor: plainColor, children: data.byteKeyMasked ? _jsx(Text, { color: tone.accent, children: data.byteKeyMasked }) : _jsx(Text, { color: tone.muted, children: "\u2014" }) }), _jsxs(Row, { label: "Gateway", plainColor: plainColor, children: [_jsx(Text, { color: gateway.color, children: g.filled }), _jsx(Text, { children: ` ${gateway.text}` })] }), hasSavings ? (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Box, { width: 11, children: _jsx(Text, { color: tone.muted, children: "7-day" }) }), _jsx(Sparkline, { values: data.savingsSeries, ascii: ascii, plainColor: plainColor }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: tone.ok, children: fmtUsd(saved) }), _jsx(Text, { color: tone.muted, children: ' saved · ' }), _jsx(Text, { children: fmtNum(data.tokensTotal) }), _jsx(Text, { color: tone.muted, children: " tokens" }), fmtPct(data.hitRate) ? _jsx(Text, { color: tone.muted, children: ` · ${fmtPct(data.hitRate)} cache hits` }) : null] })) : null] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: tone.muted, children: `What would you like to do? ${g.divider} ↑↓ move · enter select · q quit` }), _jsx(Box, { marginTop: 1, children: _jsx(Picker, { items: actions, onSelect: onAction, ascii: ascii, plainColor: plainColor, initialValue: signedIn ? 'setup' : 'login', pageSize: actions.length }) })] })] }));
144
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ interface KeyStepProps {
3
+ ascii: boolean;
4
+ plainColor: boolean;
5
+ busy: boolean;
6
+ preset: string;
7
+ existingKey?: string;
8
+ byteKey?: string;
9
+ onCreate: (name: string) => void;
10
+ onContinue: () => void;
11
+ }
12
+ export declare function KeyStep({ ascii, plainColor, busy, byteKey, existingKey, onCreate, onContinue }: KeyStepProps): React.ReactElement;
13
+ export {};
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } 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 { Panel } from './Panel.js';
8
+ // Byte key step. If a key already exists we skip straight to a continue prompt; otherwise the
9
+ // user names the key, it's created via the port, then revealed ONCE in a panel with the
10
+ // "copy it now" warning. Enter past the reveal advances the wizard.
11
+ export function KeyStep({ ascii, plainColor, busy, byteKey, existingKey, onCreate, onContinue }) {
12
+ const g = glyphs(ascii);
13
+ const tone = tones(plainColor);
14
+ const [name, setName] = React.useState('cli');
15
+ const revealed = Boolean(byteKey);
16
+ // Once a key is on screen, Enter continues. (TextInput is unmounted by then, so this hook
17
+ // owns the keystroke.)
18
+ useInput((_input, key) => {
19
+ if (key.return)
20
+ onContinue();
21
+ }, { isActive: revealed });
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: " Creating your Byte key\u2026" })] }));
24
+ }
25
+ if (revealed && byteKey) {
26
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Panel, { title: "Your Byte API key", tone: "accent", ascii: ascii, plainColor: plainColor, children: [_jsx(Text, { color: tone.accent, children: byteKey }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: tone.warn, children: `${g.bullet} Copy it now — this is shown only once.` }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: tone.muted, children: "Press enter to continue" }) })] }));
27
+ }
28
+ if (existingKey) {
29
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: "Byte key" }), _jsx(Text, { color: tone.muted, children: "This profile already has a Byte key \u2014 reusing it." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: tone.muted, children: "Press enter to continue" }) }), _jsx(EnterToContinue, { onContinue: onContinue })] }));
30
+ }
31
+ const valid = name.trim().length > 0;
32
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tone.accent, bold: true, children: "Create a Byte key" }), _jsx(Text, { color: tone.muted, children: "This is the key your tools and SDKs will use." }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: tone.muted, children: "Name " }), _jsxs(Text, { color: tone.accent, children: [g.arrow, " "] }), _jsx(TextInput, { value: name, onChange: setName, placeholder: "cli", onSubmit: () => {
33
+ if (valid)
34
+ onCreate(name.trim() || 'cli');
35
+ } })] }), _jsx(Text, { color: tone.muted, children: "Enter to create" })] }));
36
+ }
37
+ // Tiny helper: a hook-only Enter handler used when there is no TextInput to own the key.
38
+ function EnterToContinue({ onContinue }) {
39
+ useInput((_input, key) => {
40
+ if (key.return)
41
+ onContinue();
42
+ });
43
+ return _jsx(Text, { children: " " });
44
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ type ToneName = 'accent' | 'ok' | 'warn' | 'bad' | 'muted';
3
+ interface PanelProps {
4
+ title?: string;
5
+ tone?: ToneName;
6
+ ascii: boolean;
7
+ plainColor: boolean;
8
+ children: React.ReactNode;
9
+ }
10
+ export declare function Panel({ title, tone, ascii, plainColor, children }: PanelProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,12 @@
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
+ // A titled, bordered region. The border style follows the glyph set (round vs classic) and the
5
+ // border + title color follow the requested tone, degrading to the terminal default under
6
+ // plainColor. Used for the reveal-once key, errors, and grouped wizard content.
7
+ export function Panel({ title, tone = 'accent', ascii, plainColor, children }) {
8
+ const g = glyphs(ascii);
9
+ const t = tones(plainColor);
10
+ const color = t[tone];
11
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: g.border, borderColor: color, paddingX: 1, children: [title ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: color, bold: true, children: title }) })) : null, children] }));
12
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ export interface PickItem {
3
+ value: string;
4
+ label: string;
5
+ hint?: string;
6
+ }
7
+ interface PickerProps {
8
+ items: PickItem[];
9
+ onSelect: (value: string) => void;
10
+ ascii: boolean;
11
+ plainColor: boolean;
12
+ initialValue?: string;
13
+ search?: boolean;
14
+ searchLabel?: string;
15
+ pageSize?: number;
16
+ isActive?: boolean;
17
+ }
18
+ export declare function Picker({ items, onSelect, ascii, plainColor, initialValue, search, searchLabel, pageSize, isActive, }: PickerProps): React.ReactElement;
19
+ export {};
@@ -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 {};