@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,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.friendlyError = friendlyError;
|
|
7
|
+
exports.renderErrorPanel = renderErrorPanel;
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
|
+
// Translates a ByteError (HTTP status + BYTE_* / provider detail code) into plain
|
|
10
|
+
// language a first-time user can act on: what happened, why, and the exact next
|
|
11
|
+
// command. The goal is that no error ever leaves someone stuck.
|
|
12
|
+
function friendlyError(err) {
|
|
13
|
+
const code = (err.code || '').toUpperCase();
|
|
14
|
+
const status = err.status;
|
|
15
|
+
const byCode = {
|
|
16
|
+
BYTE_AUTH_REQUIRED: {
|
|
17
|
+
issue: 'You are not signed in yet.',
|
|
18
|
+
why: 'This command needs your Byte account.',
|
|
19
|
+
next: 'byte login',
|
|
20
|
+
},
|
|
21
|
+
BYTE_KEY_REQUIRED: {
|
|
22
|
+
issue: 'This profile has no Byte API key.',
|
|
23
|
+
why: 'Requests run through a Byte key so every optimization applies automatically.',
|
|
24
|
+
next: 'byte keys create',
|
|
25
|
+
},
|
|
26
|
+
BYTE_API_OFFLINE: {
|
|
27
|
+
issue: 'Could not reach the Byte service.',
|
|
28
|
+
why: 'You may be offline, or the base URL is pointed somewhere unexpected.',
|
|
29
|
+
next: 'byte doctor',
|
|
30
|
+
},
|
|
31
|
+
BYTE_MODEL_FETCH_FAILED: {
|
|
32
|
+
issue: 'Your provider was saved, but its model list could not be loaded.',
|
|
33
|
+
why: 'The provider URL or key looks unreachable right now — your connection is still saved.',
|
|
34
|
+
next: 'byte providers test',
|
|
35
|
+
},
|
|
36
|
+
BYTE_DEVICE_DENIED: {
|
|
37
|
+
issue: 'Sign-in was declined in the browser.',
|
|
38
|
+
why: 'The device approval was rejected or closed.',
|
|
39
|
+
next: 'byte login',
|
|
40
|
+
},
|
|
41
|
+
BYTE_DEVICE_EXPIRED: {
|
|
42
|
+
issue: 'The sign-in code expired.',
|
|
43
|
+
why: 'It was not approved in time.',
|
|
44
|
+
next: 'byte login',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
if (byCode[code])
|
|
48
|
+
return byCode[code];
|
|
49
|
+
if (status === 0) {
|
|
50
|
+
return { issue: 'Could not reach the Byte service.', why: 'You may be offline.', next: 'byte doctor' };
|
|
51
|
+
}
|
|
52
|
+
if (status === 401 || status === 403) {
|
|
53
|
+
return { issue: 'Your session or key was rejected.', why: 'It may have expired, been revoked, or be missing a scope.', next: 'byte login' };
|
|
54
|
+
}
|
|
55
|
+
if (status === 404) {
|
|
56
|
+
return {
|
|
57
|
+
issue: 'That route or model was not found.',
|
|
58
|
+
why: 'The model may not be wired to your key, or the surface (e.g. /v1/messages) is not enabled for it.',
|
|
59
|
+
next: 'byte doctor',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (status === 429) {
|
|
63
|
+
return { issue: 'Rate limit reached.', why: 'Too many requests landed in a short window.', next: 'wait a few seconds, then retry' };
|
|
64
|
+
}
|
|
65
|
+
if (status && status >= 500) {
|
|
66
|
+
return {
|
|
67
|
+
issue: 'The service hit an error finishing your request.',
|
|
68
|
+
why: 'This is on the Byte side, not your input — your key and provider are fine.',
|
|
69
|
+
next: 'byte doctor (then retry shortly)',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { issue: err.message || 'Something went wrong.', why: `Error code: ${err.code}`, next: 'byte doctor' };
|
|
73
|
+
}
|
|
74
|
+
// A compact, left-accent error panel. picocolors auto-disables under NO_COLOR, so this
|
|
75
|
+
// degrades to clean monochrome text in plain terminals.
|
|
76
|
+
function renderErrorPanel(f) {
|
|
77
|
+
const bar = picocolors_1.default.red('▌');
|
|
78
|
+
const lines = [
|
|
79
|
+
`${bar} ${picocolors_1.default.bold(picocolors_1.default.red('Byte hit a problem'))}`,
|
|
80
|
+
`${bar} ${f.issue}`,
|
|
81
|
+
`${bar}`,
|
|
82
|
+
`${bar} ${picocolors_1.default.dim('Why ')} ${f.why}`,
|
|
83
|
+
`${bar} ${picocolors_1.default.dim('Next')} ${picocolors_1.default.cyan(f.next)}`,
|
|
84
|
+
];
|
|
85
|
+
return `\n${lines.join('\n')}\n`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ByteApi } from './api';
|
|
2
|
+
export declare function wordmark(): string;
|
|
3
|
+
export interface HomeData {
|
|
4
|
+
email?: string;
|
|
5
|
+
org?: string;
|
|
6
|
+
providers?: number;
|
|
7
|
+
models?: number;
|
|
8
|
+
byteKey?: string;
|
|
9
|
+
gateway?: 'healthy' | 'down' | 'unknown';
|
|
10
|
+
savingsSeries: number[];
|
|
11
|
+
savedTotal: number;
|
|
12
|
+
tokensTotal: number;
|
|
13
|
+
hitRate?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadHomeData(api: ByteApi, byteKey: string | undefined, fallbackId: string): Promise<HomeData>;
|
|
16
|
+
export declare function renderHome(data: HomeData, signedIn: boolean): string;
|
package/dist/lib/home.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.wordmark = wordmark;
|
|
7
|
+
exports.loadHomeData = loadHomeData;
|
|
8
|
+
exports.renderHome = renderHome;
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const output_1 = require("./output");
|
|
11
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
12
|
+
const visLen = (s) => s.replace(ANSI, '').length;
|
|
13
|
+
const plainMode = () => Boolean(process.env.NO_COLOR) || process.env.BYTE_PLAIN === '1';
|
|
14
|
+
// Cyan→blue gradient wordmark. picocolors has no true gradient, so we step the letters
|
|
15
|
+
// across a cyan→blue ramp — a recognizable Byte signature that degrades to plain text.
|
|
16
|
+
function wordmark() {
|
|
17
|
+
const mark = `${picocolors_1.default.dim('›')}${picocolors_1.default.bold(picocolors_1.default.cyanBright('b'))}${picocolors_1.default.dim('_')}`;
|
|
18
|
+
if (plainMode())
|
|
19
|
+
return `${mark} BYTE`;
|
|
20
|
+
const ramp = [picocolors_1.default.cyanBright, picocolors_1.default.cyan, picocolors_1.default.blue, picocolors_1.default.blueBright];
|
|
21
|
+
const name = 'BYTE'
|
|
22
|
+
.split('')
|
|
23
|
+
.map((ch, i) => picocolors_1.default.bold(ramp[i % ramp.length](ch)))
|
|
24
|
+
.join('');
|
|
25
|
+
return `${mark} ${name}`;
|
|
26
|
+
}
|
|
27
|
+
function box(lines, pad = 2) {
|
|
28
|
+
const inner = Math.max(...lines.map(visLen));
|
|
29
|
+
const horiz = '─'.repeat(inner + pad * 2);
|
|
30
|
+
const body = lines.map((l) => `${picocolors_1.default.dim('│')}${' '.repeat(pad)}${l}${' '.repeat(inner - visLen(l) + pad)}${picocolors_1.default.dim('│')}`);
|
|
31
|
+
return [picocolors_1.default.dim(`╭${horiz}╮`), ...body, picocolors_1.default.dim(`╰${horiz}╯`)].join('\n');
|
|
32
|
+
}
|
|
33
|
+
function withTimeout(p, ms) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const timer = setTimeout(() => resolve(undefined), ms);
|
|
36
|
+
if (typeof timer.unref === 'function')
|
|
37
|
+
timer.unref();
|
|
38
|
+
p.then((v) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
resolve(v);
|
|
41
|
+
}, () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
resolve(undefined);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Fetches everything bare `byte` shows, in parallel, each guarded by a short timeout so a
|
|
48
|
+
// slow or unreachable service never blocks the home screen — missing cells just render dim.
|
|
49
|
+
async function loadHomeData(api, byteKey, fallbackId) {
|
|
50
|
+
const [me, provs, usage, health] = await Promise.all([
|
|
51
|
+
withTimeout(api.me(), 4000),
|
|
52
|
+
withTimeout(api.providersList(), 4000),
|
|
53
|
+
withTimeout(api.usage('day'), 4000),
|
|
54
|
+
withTimeout(api.health(), 3000),
|
|
55
|
+
]);
|
|
56
|
+
const conns = provs?.connections ?? (Array.isArray(provs) ? provs : []);
|
|
57
|
+
const models = conns.reduce((a, c) => a + (Number(c.models ?? c.model_count ?? 0) || 0), 0);
|
|
58
|
+
const series = usage?.series ?? [];
|
|
59
|
+
const num = (v) => {
|
|
60
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
61
|
+
return Number.isFinite(n) ? n : 0;
|
|
62
|
+
};
|
|
63
|
+
const requests = series.reduce((a, p) => a + num(p.requests), 0);
|
|
64
|
+
const cached = series.reduce((a, p) => a + num(p.cached), 0);
|
|
65
|
+
return {
|
|
66
|
+
email: me?.user?.email,
|
|
67
|
+
org: me?.org?.name ?? me?.user?.org_name,
|
|
68
|
+
providers: conns.length || undefined,
|
|
69
|
+
models: models || undefined,
|
|
70
|
+
byteKey,
|
|
71
|
+
gateway: health ? (health.status === 'ok' || health.ok ? 'healthy' : 'down') : 'unknown',
|
|
72
|
+
savingsSeries: series.map((p) => num(p.savings_usd)),
|
|
73
|
+
savedTotal: series.reduce((a, p) => a + num(p.savings_usd), 0),
|
|
74
|
+
tokensTotal: series.reduce((a, p) => a + num(p.tokens_in) + num(p.tokens_out), 0),
|
|
75
|
+
hitRate: requests > 0 ? cached / requests : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function renderHome(data, signedIn) {
|
|
79
|
+
const dim = picocolors_1.default.dim;
|
|
80
|
+
const label = (s) => dim(s.padEnd(10));
|
|
81
|
+
const dash = dim('—');
|
|
82
|
+
const gatewayDot = data.gateway === 'healthy' ? `${picocolors_1.default.green('●')} healthy` : data.gateway === 'down' ? `${picocolors_1.default.red('●')} unreachable` : dim('● unknown');
|
|
83
|
+
const lines = [
|
|
84
|
+
`${wordmark()}${' '.repeat(6)}${dim('optimization gateway')}`,
|
|
85
|
+
'',
|
|
86
|
+
`${label('Account')}${signedIn ? `${data.email ?? 'you'}${data.org ? ` ${dim('·')} ${data.org}` : ''}` : picocolors_1.default.yellow('not signed in')}`,
|
|
87
|
+
`${label('Providers')}${data.providers ? `${data.providers} connected${data.models ? ` ${dim('·')} ${data.models} models` : ''}` : dash}`,
|
|
88
|
+
`${label('Byte key')}${data.byteKey ? picocolors_1.default.cyan((0, output_1.maskKey)(data.byteKey)) : dash}`,
|
|
89
|
+
`${label('Gateway')}${gatewayDot}`,
|
|
90
|
+
];
|
|
91
|
+
if (data.savingsSeries.some((v) => v > 0) || data.savedTotal > 0) {
|
|
92
|
+
const spark = (0, output_1.sparkline)(data.savingsSeries, plainMode());
|
|
93
|
+
const summary = `${picocolors_1.default.green((0, output_1.fmtUsd)(data.savedTotal))} saved ${dim('·')} ${(0, output_1.fmtNum)(data.tokensTotal)} tokens${data.hitRate !== undefined ? ` ${dim('·')} ${(0, output_1.pct)(data.hitRate)} cache hits` : ''}`;
|
|
94
|
+
lines.push('', `${label('7-day')}${picocolors_1.default.cyan(spark)} ${summary}`);
|
|
95
|
+
}
|
|
96
|
+
return box(lines);
|
|
97
|
+
}
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -2,3 +2,6 @@ export declare function renderTable(head: string[], rows: Array<Array<string | n
|
|
|
2
2
|
export declare function maskKey(value: string | undefined): string;
|
|
3
3
|
export declare function fmtUsd(value: unknown): string;
|
|
4
4
|
export declare function pct(value: unknown): string;
|
|
5
|
+
export declare function fmtNum(value: unknown): string;
|
|
6
|
+
export declare function fmtBucket(value: unknown): string;
|
|
7
|
+
export declare function sparkline(values: number[], plain?: boolean): string;
|
package/dist/lib/output.js
CHANGED
|
@@ -7,6 +7,9 @@ exports.renderTable = renderTable;
|
|
|
7
7
|
exports.maskKey = maskKey;
|
|
8
8
|
exports.fmtUsd = fmtUsd;
|
|
9
9
|
exports.pct = pct;
|
|
10
|
+
exports.fmtNum = fmtNum;
|
|
11
|
+
exports.fmtBucket = fmtBucket;
|
|
12
|
+
exports.sparkline = sparkline;
|
|
10
13
|
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
11
14
|
function renderTable(head, rows) {
|
|
12
15
|
const table = new cli_table3_1.default({ head });
|
|
@@ -35,3 +38,43 @@ function pct(value) {
|
|
|
35
38
|
const scaled = n <= 1 ? n * 100 : n;
|
|
36
39
|
return `${scaled.toFixed(1)}%`;
|
|
37
40
|
}
|
|
41
|
+
// 1234 -> "1.2K", 1_500_000 -> "1.5M". Keeps small counts exact.
|
|
42
|
+
function fmtNum(value) {
|
|
43
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
44
|
+
if (!Number.isFinite(n))
|
|
45
|
+
return '-';
|
|
46
|
+
const abs = Math.abs(n);
|
|
47
|
+
if (abs >= 1_000_000)
|
|
48
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
49
|
+
if (abs >= 1_000)
|
|
50
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
51
|
+
return String(Math.round(n));
|
|
52
|
+
}
|
|
53
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
54
|
+
// "2026-05-30" -> "May 30"; "2026-05-30T14:00:00Z" -> "May 30 14:00".
|
|
55
|
+
function fmtBucket(value) {
|
|
56
|
+
if (typeof value !== 'string' || !value)
|
|
57
|
+
return '-';
|
|
58
|
+
const m = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):)?/);
|
|
59
|
+
if (!m)
|
|
60
|
+
return value;
|
|
61
|
+
const mon = MONTHS[Number(m[2]) - 1] ?? m[2];
|
|
62
|
+
const day = String(Number(m[3]));
|
|
63
|
+
return m[4] ? `${mon} ${day} ${m[4]}:00` : `${mon} ${day}`;
|
|
64
|
+
}
|
|
65
|
+
// Unicode block sparkline with an ASCII fallback for legacy terminals (NO_COLOR/conhost).
|
|
66
|
+
function sparkline(values, plain = false) {
|
|
67
|
+
const nums = values.map((v) => (Number.isFinite(v) ? v : 0));
|
|
68
|
+
if (!nums.length)
|
|
69
|
+
return '';
|
|
70
|
+
const ramp = plain ? '.:-=+*#'.split('') : '▁▂▃▄▅▆▇█'.split('');
|
|
71
|
+
const max = Math.max(...nums);
|
|
72
|
+
const min = Math.min(...nums);
|
|
73
|
+
const span = max - min || 1;
|
|
74
|
+
return nums
|
|
75
|
+
.map((v) => {
|
|
76
|
+
const idx = Math.round(((v - min) / span) * (ramp.length - 1));
|
|
77
|
+
return ramp[Math.max(0, Math.min(ramp.length - 1, idx))];
|
|
78
|
+
})
|
|
79
|
+
.join('');
|
|
80
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface PresetCard {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hint: string;
|
|
5
|
+
blurb: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const PRESET_CARDS: PresetCard[];
|
|
8
|
+
export declare const PRESET_VALUES: string[];
|
|
9
|
+
export declare function presetCard(value: string): PresetCard | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRESET_VALUES = exports.PRESET_CARDS = void 0;
|
|
4
|
+
exports.presetCard = presetCard;
|
|
5
|
+
exports.PRESET_CARDS = [
|
|
6
|
+
{
|
|
7
|
+
value: 'maximum',
|
|
8
|
+
label: 'Maximum — everything on (recommended)',
|
|
9
|
+
hint: 'best savings + quality',
|
|
10
|
+
blurb: 'Every production-safe optimization runs on every request: full caching, compression, routing, and quality guards.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
value: 'max_savings',
|
|
14
|
+
label: 'Maximum savings',
|
|
15
|
+
hint: 'cheapest',
|
|
16
|
+
blurb: 'Leans hardest on caching, compression, and cheaper routing to cut spend the most.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: 'balanced',
|
|
20
|
+
label: 'Balanced',
|
|
21
|
+
hint: 'savings + speed',
|
|
22
|
+
blurb: 'A middle ground: strong savings without the deepest latency-adding passes.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: 'lowest_latency',
|
|
26
|
+
label: 'Lowest latency',
|
|
27
|
+
hint: 'fastest',
|
|
28
|
+
blurb: 'Skips the heavier passes so responses come back as fast as possible.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
value: 'reliability_first',
|
|
32
|
+
label: 'Reliability first',
|
|
33
|
+
hint: 'most robust',
|
|
34
|
+
blurb: 'Prioritizes verification, repair, and fallbacks over maximum savings.',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
exports.PRESET_VALUES = exports.PRESET_CARDS.map((c) => c.value);
|
|
38
|
+
function presetCard(value) {
|
|
39
|
+
return exports.PRESET_CARDS.find((c) => c.value === value);
|
|
40
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
};
|
|
100
|
+
export declare function useInk(opts?: {
|
|
101
|
+
json?: boolean;
|
|
102
|
+
}): boolean;
|
|
103
|
+
export declare function computeAscii(): boolean;
|
|
104
|
+
export declare function renderHomeIsland(props: HomeIslandProps): Promise<HomeResult>;
|
|
105
|
+
export declare function renderWizardIsland(props: WizardIslandProps): Promise<WizardResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = require("node:test");
|
|
8
|
+
const tui_1 = require("./tui");
|
|
9
|
+
// Snapshot the process knobs useInk reads so each case can set a clean world and restore it.
|
|
10
|
+
const saved = {
|
|
11
|
+
stdinTTY: process.stdin.isTTY,
|
|
12
|
+
stdoutTTY: process.stdout.isTTY,
|
|
13
|
+
columns: process.stdout.columns,
|
|
14
|
+
env: { ...process.env },
|
|
15
|
+
};
|
|
16
|
+
function setWorld(opts) {
|
|
17
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: opts.stdin ?? true, configurable: true });
|
|
18
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: opts.stdout ?? true, configurable: true });
|
|
19
|
+
Object.defineProperty(process.stdout, 'columns', { value: opts.columns ?? 120, configurable: true });
|
|
20
|
+
// Clear the opt-out / CI / color env that useInk inspects, then apply overrides.
|
|
21
|
+
for (const k of ['NO_COLOR', 'CI', 'BYTE_PLAIN', 'BYTE_TUI', 'BYTE_NO_MENU'])
|
|
22
|
+
delete process.env[k];
|
|
23
|
+
for (const [k, v] of Object.entries(opts.env ?? {})) {
|
|
24
|
+
if (v === undefined)
|
|
25
|
+
delete process.env[k];
|
|
26
|
+
else
|
|
27
|
+
process.env[k] = v;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
(0, node_test_1.afterEach)(() => {
|
|
31
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: saved.stdinTTY, configurable: true });
|
|
32
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: saved.stdoutTTY, configurable: true });
|
|
33
|
+
Object.defineProperty(process.stdout, 'columns', { value: saved.columns, configurable: true });
|
|
34
|
+
for (const k of Object.keys(process.env))
|
|
35
|
+
if (!(k in saved.env))
|
|
36
|
+
delete process.env[k];
|
|
37
|
+
Object.assign(process.env, saved.env);
|
|
38
|
+
});
|
|
39
|
+
(0, node_test_1.test)('useInk true on a wide, color-capable TTY with no opt-out', () => {
|
|
40
|
+
setWorld({ stdin: true, stdout: true, columns: 120 });
|
|
41
|
+
strict_1.default.equal((0, tui_1.useInk)(), true);
|
|
42
|
+
});
|
|
43
|
+
(0, node_test_1.test)('useInk false when stdout is not a TTY (piped)', () => {
|
|
44
|
+
setWorld({ stdout: false });
|
|
45
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
46
|
+
});
|
|
47
|
+
(0, node_test_1.test)('useInk false when stdin is not a TTY', () => {
|
|
48
|
+
setWorld({ stdin: false });
|
|
49
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
50
|
+
});
|
|
51
|
+
(0, node_test_1.test)('useInk false under CI', () => {
|
|
52
|
+
setWorld({ env: { CI: '1' } });
|
|
53
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
54
|
+
});
|
|
55
|
+
(0, node_test_1.test)('useInk false under NO_COLOR', () => {
|
|
56
|
+
setWorld({ env: { NO_COLOR: '1' } });
|
|
57
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
58
|
+
});
|
|
59
|
+
(0, node_test_1.test)('useInk false when the window is narrower than 80 columns', () => {
|
|
60
|
+
setWorld({ columns: 70 });
|
|
61
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
62
|
+
});
|
|
63
|
+
(0, node_test_1.test)('useInk false with BYTE_PLAIN=1, BYTE_TUI=0, or BYTE_NO_MENU=1', () => {
|
|
64
|
+
setWorld({ env: { BYTE_PLAIN: '1' } });
|
|
65
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
66
|
+
setWorld({ env: { BYTE_TUI: '0' } });
|
|
67
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
68
|
+
setWorld({ env: { BYTE_NO_MENU: '1' } });
|
|
69
|
+
strict_1.default.equal((0, tui_1.useInk)(), false);
|
|
70
|
+
});
|
|
71
|
+
(0, node_test_1.test)('useInk false when the JSON flag is set even on a good TTY', () => {
|
|
72
|
+
setWorld({});
|
|
73
|
+
strict_1.default.equal((0, tui_1.useInk)({ json: true }), false);
|
|
74
|
+
});
|
|
75
|
+
(0, node_test_1.test)('computeAscii true on bare win32 (no Windows Terminal / WSL)', () => {
|
|
76
|
+
const savedPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
77
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
78
|
+
delete process.env.WT_SESSION;
|
|
79
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
80
|
+
delete process.env.TERM;
|
|
81
|
+
strict_1.default.equal((0, tui_1.computeAscii)(), true);
|
|
82
|
+
// Windows Terminal sets WT_SESSION → unicode is safe.
|
|
83
|
+
process.env.WT_SESSION = '1';
|
|
84
|
+
strict_1.default.equal((0, tui_1.computeAscii)(), false);
|
|
85
|
+
delete process.env.WT_SESSION;
|
|
86
|
+
Object.defineProperty(process, 'platform', savedPlatform);
|
|
87
|
+
});
|
|
88
|
+
(0, node_test_1.test)('computeAscii true on a dumb terminal regardless of platform', () => {
|
|
89
|
+
const savedPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
90
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
91
|
+
process.env.TERM = 'dumb';
|
|
92
|
+
strict_1.default.equal((0, tui_1.computeAscii)(), true);
|
|
93
|
+
delete process.env.TERM;
|
|
94
|
+
strict_1.default.equal((0, tui_1.computeAscii)(), false);
|
|
95
|
+
Object.defineProperty(process, 'platform', savedPlatform);
|
|
96
|
+
});
|
package/dist/lib/tui.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.useInk = useInk;
|
|
7
|
+
exports.computeAscii = computeAscii;
|
|
8
|
+
exports.renderHomeIsland = renderHomeIsland;
|
|
9
|
+
exports.renderWizardIsland = renderWizardIsland;
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_url_1 = require("node:url");
|
|
12
|
+
// CJS→ESM bridge for the Ink island. This file stays under src/lib (CommonJS) and is the ONLY
|
|
13
|
+
// thing the commands touch; the island itself lives under src/tui and is compiled separately to
|
|
14
|
+
// ESM (dist/tui). Like src/lib/ui.ts, a plain import() would be down-levelled to require() and
|
|
15
|
+
// blow up on ESM-only Ink, so we hide it behind `new Function`. Every entry point is
|
|
16
|
+
// fallback-first: ANY failure resolves to {status:'fallback'} and the caller drops to clack.
|
|
17
|
+
const importESM = new Function('s', 'return import(s)');
|
|
18
|
+
// Resolve the island entry as an absolute file:// URL off this module's location. Both the
|
|
19
|
+
// CJS bridge (dist/lib/tui.js) and the island (dist/tui/index.js) live under dist/, so step up
|
|
20
|
+
// one level. pathToFileURL is MANDATORY on Windows — a bare path makes dynamic import throw
|
|
21
|
+
// ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
22
|
+
function islandUrl() {
|
|
23
|
+
return (0, node_url_1.pathToFileURL)(node_path_1.default.join(__dirname, '..', 'tui', 'index.js')).href;
|
|
24
|
+
}
|
|
25
|
+
// Gate: the Ink UI only takes over when we have a real, wide-enough, color-capable TTY on both
|
|
26
|
+
// ends and nobody opted out. Any miss (pipe, CI, narrow window, NO_COLOR, BYTE_PLAIN, the JSON
|
|
27
|
+
// flag) returns false and the caller stays on the existing clack path. Mirrors lib/tty.ts.
|
|
28
|
+
function useInk(opts) {
|
|
29
|
+
return (Boolean(process.stdin.isTTY) &&
|
|
30
|
+
Boolean(process.stdout.isTTY) &&
|
|
31
|
+
(process.stdout.columns ?? 0) >= 80 &&
|
|
32
|
+
!process.env.NO_COLOR &&
|
|
33
|
+
!process.env.CI &&
|
|
34
|
+
process.env.BYTE_PLAIN !== '1' &&
|
|
35
|
+
process.env.BYTE_TUI !== '0' &&
|
|
36
|
+
process.env.BYTE_NO_MENU !== '1' &&
|
|
37
|
+
!opts?.json);
|
|
38
|
+
}
|
|
39
|
+
// Drop to 7-bit glyphs on legacy Windows consoles (conhost without Windows Terminal / WSL) and
|
|
40
|
+
// dumb terminals, where unicode block glyphs render as mojibake.
|
|
41
|
+
function computeAscii() {
|
|
42
|
+
return ((process.platform === 'win32' && !process.env.WT_SESSION && !process.env.WSL_DISTRO_NAME) ||
|
|
43
|
+
process.env.TERM === 'dumb');
|
|
44
|
+
}
|
|
45
|
+
async function renderHomeIsland(props) {
|
|
46
|
+
try {
|
|
47
|
+
const mod = await importESM(islandUrl());
|
|
48
|
+
return (await mod.renderHome(props));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { status: 'fallback' };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function renderWizardIsland(props) {
|
|
55
|
+
try {
|
|
56
|
+
const mod = await importESM(islandUrl());
|
|
57
|
+
return (await mod.renderWizard(props));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { status: 'fallback' };
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/lib/ui.js
CHANGED
|
@@ -32,8 +32,20 @@ exports.theme = {
|
|
|
32
32
|
muted: (s) => picocolors_1.default.dim(s),
|
|
33
33
|
bold: (s) => picocolors_1.default.bold(s),
|
|
34
34
|
};
|
|
35
|
+
// The Byte signature: a `›b_` prompt glyph and a cyan→blue gradient wordmark. picocolors
|
|
36
|
+
// has no true gradient, so the letters step across a cyan→blue ramp; degrades to plain
|
|
37
|
+
// text under NO_COLOR/BYTE_PLAIN. Kept in sync with the home dashboard wordmark.
|
|
35
38
|
function banner() {
|
|
36
|
-
|
|
39
|
+
const plain = Boolean(process.env.NO_COLOR) || process.env.BYTE_PLAIN === '1';
|
|
40
|
+
const mark = `${picocolors_1.default.dim('›')}${picocolors_1.default.bold(picocolors_1.default.cyanBright('b'))}${picocolors_1.default.dim('_')}`;
|
|
41
|
+
if (plain)
|
|
42
|
+
return `${mark} BYTE`;
|
|
43
|
+
const ramp = [picocolors_1.default.cyanBright, picocolors_1.default.cyan, picocolors_1.default.blue, picocolors_1.default.blueBright];
|
|
44
|
+
const name = 'BYTE'
|
|
45
|
+
.split('')
|
|
46
|
+
.map((ch, i) => picocolors_1.default.bold(ramp[i % ramp.length](ch)))
|
|
47
|
+
.join('');
|
|
48
|
+
return `${mark} ${name}`;
|
|
37
49
|
}
|
|
38
50
|
function guard(value, c) {
|
|
39
51
|
if (c.isCancel(value)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import { render } from 'ink-testing-library';
|
|
5
|
+
import { Home } from '../components/Home.js';
|
|
6
|
+
// Strip ANSI so assertions match the visible text regardless of the per-char gradient coloring
|
|
7
|
+
// (the gradient inserts color codes between letters, so "BYTE" is only contiguous once stripped).
|
|
8
|
+
const ANSI = /\[[0-9;]*m/g;
|
|
9
|
+
const plain = (s) => (s ?? '').replace(ANSI, '');
|
|
10
|
+
const data = {
|
|
11
|
+
email: 'founder@byte.co',
|
|
12
|
+
org: 'Bytevion',
|
|
13
|
+
providers: 2,
|
|
14
|
+
models: 14,
|
|
15
|
+
byteKeyMasked: 'byte_sk_live_abcd...wxyz',
|
|
16
|
+
gateway: 'healthy',
|
|
17
|
+
savingsSeries: [1, 3, 2, 6, 4, 8, 5],
|
|
18
|
+
savedTotal: 12.5,
|
|
19
|
+
tokensTotal: 1_200_000,
|
|
20
|
+
hitRate: 0.42,
|
|
21
|
+
};
|
|
22
|
+
test('Home (unicode) renders brand, identity, masked key, gateway dot, and sparkline', () => {
|
|
23
|
+
const { lastFrame, unmount } = render(_jsx(Home, { signedIn: true, data: data, ascii: false, plainColor: false, version: "0.3.0", onDone: () => { } }));
|
|
24
|
+
const out = plain(lastFrame());
|
|
25
|
+
unmount();
|
|
26
|
+
assert.match(out, /BYTE/); // gradient wordmark (contiguous after stripping ANSI)
|
|
27
|
+
assert.match(out, /optimization gateway/);
|
|
28
|
+
assert.match(out, /founder@byte\.co/);
|
|
29
|
+
assert.match(out, /Bytevion/);
|
|
30
|
+
assert.match(out, /2 connected/);
|
|
31
|
+
assert.match(out, /14 models/);
|
|
32
|
+
assert.match(out, /byte_sk_live_abcd\.\.\.wxyz/);
|
|
33
|
+
assert.ok(out.includes('●'), 'gateway dot glyph present');
|
|
34
|
+
assert.match(out, /healthy/);
|
|
35
|
+
// unicode block sparkline ramp
|
|
36
|
+
assert.ok(/[▁▂▃▄▅▆▇█]/.test(out), 'unicode sparkline present');
|
|
37
|
+
assert.match(out, /saved/);
|
|
38
|
+
assert.match(out, /Set up Byte/); // quick-actions menu
|
|
39
|
+
});
|
|
40
|
+
test('Home (ascii) swaps glyphs: classic dot + ascii sparkline, still shows the key', () => {
|
|
41
|
+
const { lastFrame, unmount } = render(_jsx(Home, { signedIn: true, data: data, ascii: true, plainColor: true, version: "0.3.0", onDone: () => { } }));
|
|
42
|
+
const out = plain(lastFrame());
|
|
43
|
+
unmount();
|
|
44
|
+
assert.match(out, /BYTE/);
|
|
45
|
+
assert.match(out, /byte_sk_live_abcd\.\.\.wxyz/);
|
|
46
|
+
assert.match(out, /healthy/);
|
|
47
|
+
// ascii gateway dot + ascii sparkline ramp; no unicode block glyphs
|
|
48
|
+
assert.ok(out.includes('[*]'), 'ascii gateway glyph present');
|
|
49
|
+
assert.ok(!/[▁▂▃▄▅▆▇█●]/.test(out), 'no unicode glyphs in ascii mode');
|
|
50
|
+
assert.ok(/[.:\-=+*#]/.test(out), 'ascii sparkline ramp present');
|
|
51
|
+
});
|
|
52
|
+
test('Home signed-out shows "not signed in" and a Sign in action', () => {
|
|
53
|
+
const signedOut = { gateway: 'unknown', savingsSeries: [], savedTotal: 0, tokensTotal: 0 };
|
|
54
|
+
const { lastFrame, unmount } = render(_jsx(Home, { signedIn: false, data: signedOut, ascii: false, plainColor: false, version: "0.3.0", onDone: () => { } }));
|
|
55
|
+
const out = plain(lastFrame());
|
|
56
|
+
unmount();
|
|
57
|
+
assert.match(out, /not signed in/);
|
|
58
|
+
assert.match(out, /Sign in/);
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|