@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
@@ -106,16 +106,24 @@ class ProvidersAdd extends base_1.BaseCommand {
106
106
  message: 'Model id to use in your tools',
107
107
  options: models.slice(0, 50).map((m) => ({ value: m.byte_alias, label: m.byte_alias, hint: m.model_id })),
108
108
  });
109
- await ui.note(alias, 'Model id (use this everywhere)');
109
+ // Enable the chosen model so the gateway can serve it immediately.
110
+ const picked = models.find((m) => m.byte_alias === alias);
111
+ if (picked?.id)
112
+ await api.modelEnable(picked.id).catch(() => undefined);
113
+ await ui.note(`${alias}\n\n${ui.theme.muted('Enabled and ready — use this id in your tools.')}`, 'Model id');
110
114
  }
111
115
  return res;
112
116
  }
113
117
  const res = await api.providersAdd(body);
118
+ // Enable the first imported model so the gateway is usable right away.
119
+ const firstModel = (res.fetch_result?.models ?? res.models ?? [])[0];
120
+ if (firstModel?.id)
121
+ await api.modelEnable(firstModel.id).catch(() => undefined);
114
122
  if (this.jsonEnabled())
115
123
  return res;
116
124
  const id = res.id ?? res.connection?.id;
117
125
  const imported = res.fetch_result?.imported ?? res.imported ?? 0;
118
- this.log(`Provider connection added (id ${id}). Models imported: ${imported}.`);
126
+ this.log(`Provider connection added (id ${id}). Models imported: ${imported}.${firstModel?.byte_alias ? ` Enabled ${firstModel.byte_alias}.` : ''}`);
119
127
  return res;
120
128
  }
121
129
  }
@@ -9,7 +9,7 @@ class Run extends base_1.BaseCommand {
9
9
  prompt: core_1.Args.string({ description: 'The prompt to send', required: true }),
10
10
  };
11
11
  static flags = {
12
- model: core_1.Flags.string({ description: 'Model or Byte alias', default: 'byte-default' }),
12
+ model: core_1.Flags.string({ description: 'Model or Byte alias (default: auto — routes to your enabled model)', default: 'auto' }),
13
13
  'max-tokens': core_1.Flags.integer({ description: 'Maximum output tokens' }),
14
14
  };
15
15
  async run() {
@@ -14,6 +14,7 @@ export default class Setup extends BaseCommand {
14
14
  connect: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
15
  };
16
16
  run(): Promise<unknown>;
17
+ private buildWizardPorts;
17
18
  private connectProvider;
18
19
  private runNonInteractive;
19
20
  }
@@ -66,6 +66,35 @@ class Setup extends base_1.BaseCommand {
66
66
  if (this.jsonEnabled() || !(0, tty_1.interactive)()) {
67
67
  return this.runNonInteractive(flags, profile, base);
68
68
  }
69
+ // Full-screen Ink wizard when the terminal supports it. Any failure inside the island
70
+ // returns {status:'fallback'} and we drop through to the clack flow below — no regression.
71
+ const tui = require('../lib/tui');
72
+ if (tui.useInk({ json: this.jsonEnabled() })) {
73
+ const ports = this.buildWizardPorts(flags, profile, base);
74
+ const res = await tui.renderWizardIsland({
75
+ plainColor: Boolean(process.env.NO_COLOR),
76
+ ascii: tui.computeAscii(),
77
+ version: this.config.version,
78
+ initial: { signedIn: Boolean((0, credentials_1.getToken)(profile)), email: undefined, byteKey: (0, credentials_1.getByteKey)(profile) },
79
+ ports,
80
+ providerPresets: providers_1.PROVIDER_PRESETS,
81
+ presetCards: presets_1.PRESET_CARDS,
82
+ });
83
+ if (res.status === 'cancelled')
84
+ return this.exit(130);
85
+ if (res.status === 'done') {
86
+ if (res.summary.connect) {
87
+ await this.config.runCommand('integrate', [
88
+ res.summary.connect,
89
+ '--write',
90
+ '--yes',
91
+ ...(res.summary.model ? ['--model', res.summary.model] : []),
92
+ ]);
93
+ }
94
+ return res.summary;
95
+ }
96
+ // status === 'fallback' → continue to the clack flow.
97
+ }
69
98
  await ui.intro(ui.banner());
70
99
  // 1) Sign in
71
100
  let token = (0, credentials_1.getToken)(profile);
@@ -150,6 +179,63 @@ class Setup extends base_1.BaseCommand {
150
179
  await ui.outro(`${ui.theme.ok('Setup complete.')} Next: ${tips.join(' · ')}`);
151
180
  return { status: 'setup_complete', profile, model: provider.alias, provider: provider.status, preset };
152
181
  }
182
+ // Adapts the existing auth + API surface into the WizardPorts the Ink island calls. Every port
183
+ // wraps a call the clack flow already uses, so both paths hit identical backend behavior.
184
+ buildWizardPorts(flags, profile, base) {
185
+ const api = this.api(flags);
186
+ return {
187
+ signIn: async () => {
188
+ const r = await (0, auth_1.deviceLogin)({ baseUrl: base, profile });
189
+ return { email: r.email };
190
+ },
191
+ listProviders: async () => {
192
+ const existing = await api.providersList().catch(() => ({ connections: [] }));
193
+ return existing.connections ?? (Array.isArray(existing) ? existing : []);
194
+ },
195
+ connectProvider: async (a) => {
196
+ const res = await api.providersAdd({
197
+ api_key: a.apiKey,
198
+ gateway_mode: flags.mode || a.mode,
199
+ base_url: a.baseUrl,
200
+ auto_fetch: true,
201
+ });
202
+ const fetchResult = res.fetch_result ?? {};
203
+ const models = res.models ?? fetchResult.models ?? [];
204
+ const connId = res.id ?? res.connection?.id;
205
+ const failed = fetchResult.status === 'error' || Boolean(res.fetch_error) || Boolean(fetchResult.last_error);
206
+ const status = failed && !models.length ? 'saved' : 'connected';
207
+ return {
208
+ status,
209
+ models,
210
+ connId,
211
+ error: failed ? String(fetchResult.last_error || res.fetch_error || 'Provider models could not be loaded.') : undefined,
212
+ };
213
+ },
214
+ rotateAndTest: async (connId, apiKey) => {
215
+ try {
216
+ await api.providersRotate(connId, apiKey);
217
+ const test = await api.providersTest(connId);
218
+ const models = test.models ?? [];
219
+ return { models, error: models.length ? undefined : 'No models returned.' };
220
+ }
221
+ catch (err) {
222
+ return { models: [], error: err?.message ?? String(err) };
223
+ }
224
+ },
225
+ enableModel: async (id) => {
226
+ await api.modelEnable(id).catch(() => undefined);
227
+ },
228
+ createByteKey: async (name, preset) => {
229
+ const res = await api.keysCreate({ name: name || 'cli', preset });
230
+ const key = String(res.key);
231
+ (0, credentials_1.setByteKey)(profile, key);
232
+ return { key };
233
+ },
234
+ applyPreset: async (preset) => {
235
+ await api.optPatch({ default_mode: preset }).catch(() => undefined);
236
+ },
237
+ };
238
+ }
153
239
  async connectProvider(api, flags) {
154
240
  const providerId = await ui.select({
155
241
  message: 'Which provider?',
@@ -220,6 +306,11 @@ class Setup extends base_1.BaseCommand {
220
306
  message: 'Default model',
221
307
  options: models.slice(0, 50).map((m) => ({ value: m.byte_alias, label: m.byte_alias, hint: m.model_id })),
222
308
  });
309
+ // Enable the chosen model so the gateway can route to it right away. Imported models
310
+ // start disabled — without this the first `byte run` would hit BYTE_ALIAS_NOT_CONFIGURED.
311
+ const picked = models.find((m) => m.byte_alias === alias);
312
+ if (picked?.id)
313
+ await api.modelEnable(picked.id).catch(() => undefined);
223
314
  return { alias, status: 'connected' };
224
315
  }
225
316
  return { status: 'saved' };
@@ -242,7 +333,11 @@ class Setup extends base_1.BaseCommand {
242
333
  const res = await api.providersAdd({ api_key: key, gateway_mode: mode, base_url: baseUrl, auto_fetch: true });
243
334
  const fetchResult = res.fetch_result ?? {};
244
335
  imported = fetchResult.imported ?? res.imported ?? 0;
245
- chosenAlias = res.models?.[0]?.byte_alias;
336
+ const firstModel = res.models?.[0];
337
+ chosenAlias = firstModel?.byte_alias;
338
+ // Enable the first model so the gateway works immediately (imported models start disabled).
339
+ if (firstModel?.id)
340
+ await api.modelEnable(firstModel.id).catch(() => undefined);
246
341
  if (fetchResult.status === 'error' || res.fetch_error) {
247
342
  providerStatus = 'saved';
248
343
  providerError = String(fetchResult.last_error || res.fetch_error);
@@ -37,6 +37,7 @@ const api_1 = require("../../lib/api");
37
37
  const config_1 = require("../../lib/config");
38
38
  const credentials_1 = require("../../lib/credentials");
39
39
  const home_1 = require("../../lib/home");
40
+ const output_1 = require("../../lib/output");
40
41
  const tty_1 = require("../../lib/tty");
41
42
  const ui = __importStar(require("../../lib/ui"));
42
43
  // oclif runs init hooks before it would print help. Bare `byte` (no command) in a real
@@ -64,6 +65,29 @@ const hook = async function (opts) {
64
65
  if (loaded)
65
66
  data = { ...data, ...loaded };
66
67
  }
68
+ // Full-screen Ink home dashboard + quick-actions menu, when the terminal supports it. Any
69
+ // failure inside the island returns {status:'fallback'} and we drop straight through to the
70
+ // existing clack dashboard below — nothing regresses.
71
+ const tui = require('../../lib/tui');
72
+ if (tui.useInk()) {
73
+ const res = await tui.renderHomeIsland({
74
+ signedIn,
75
+ data: { ...data, gateway: data.gateway ?? 'unknown', byteKeyMasked: (0, output_1.maskKey)(byteKey) },
76
+ plainColor: Boolean(process.env.NO_COLOR),
77
+ ascii: tui.computeAscii(),
78
+ version: this.config.version,
79
+ });
80
+ if (res.status === 'exit') {
81
+ await this.exit(0);
82
+ return;
83
+ }
84
+ if (res.status === 'action') {
85
+ await this.config.runCommand(res.commandId, res.argv);
86
+ await this.exit(0);
87
+ return;
88
+ }
89
+ // status === 'fallback' → continue to the clack dashboard.
90
+ }
67
91
  this.log(`\n${(0, home_1.renderHome)(data, signedIn)}\n`);
68
92
  const choice = await ui.select({
69
93
  message: 'What would you like to do?',
package/dist/lib/api.d.ts CHANGED
@@ -22,6 +22,7 @@ export declare class ByteApi {
22
22
  providersAdd(body: Record<string, unknown>): Promise<any>;
23
23
  providersRotate(id: number, apiKey: string): Promise<any>;
24
24
  providersTest(id: number): Promise<any>;
25
+ modelEnable(id: number): Promise<any>;
25
26
  optShow(): Promise<any>;
26
27
  optPatch(body: Record<string, unknown>): Promise<any>;
27
28
  usage(granularity: string): Promise<any>;
package/dist/lib/api.js CHANGED
@@ -94,6 +94,12 @@ class ByteApi {
94
94
  providersTest(id) {
95
95
  return this.request('POST', `/api/v1/model-connections/${id}/fetch-models`, { body: {} });
96
96
  }
97
+ // Enable an imported provider model so the gateway can route to it. Imported models
98
+ // arrive disabled; without enabling at least one, the gateway returns
99
+ // BYTE_ALIAS_NOT_CONFIGURED. Setup/providers-add call this on the model the user picks.
100
+ modelEnable(id) {
101
+ return this.request('PATCH', `/api/v1/models/${id}`, { body: { enabled: true } });
102
+ }
97
103
  // --- optimizations ---
98
104
  optShow() {
99
105
  return this.request('GET', '/api/v1/optimizations');
@@ -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
+ });
@@ -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
+ }
@@ -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 {};
@@ -0,0 +1,88 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { initialState, reducer, selectSummary } from '../state.js';
4
+ const base = (over = {}) => ({ ...initialState({ signedIn: false }), ...over });
5
+ test('initialState seeds maximum preset and pending provider', () => {
6
+ const s = initialState({ signedIn: true, email: 'a@b.co', byteKey: 'byte_sk_live_x' });
7
+ assert.equal(s.step, 'welcome');
8
+ assert.equal(s.signedIn, true);
9
+ assert.equal(s.email, 'a@b.co');
10
+ assert.equal(s.preset, 'maximum');
11
+ assert.equal(s.providerStatus, 'pending');
12
+ assert.equal(s.byteKey, 'byte_sk_live_x');
13
+ assert.equal(s.byteKeyRevealed, false);
14
+ });
15
+ test('happy path advances welcome→signin→provider→model→key→preset→integrate→done', () => {
16
+ let s = base();
17
+ s = reducer(s, { type: 'GOTO', step: 'signin' });
18
+ assert.equal(s.step, 'signin');
19
+ s = reducer(s, { type: 'SIGNED_IN', email: 'me@byte.co' });
20
+ assert.equal(s.step, 'provider');
21
+ assert.equal(s.signedIn, true);
22
+ assert.equal(s.email, 'me@byte.co');
23
+ s = reducer(s, { type: 'PICK_PROVIDER', id: 'openai', baseUrl: undefined });
24
+ assert.equal(s.providerChoice, 'openai');
25
+ s = reducer(s, { type: 'PROVIDER_RESULT', status: 'connected', models: [{ id: 1, byte_alias: 'byte-gpt' }], connId: 7 });
26
+ assert.equal(s.step, 'model');
27
+ assert.equal(s.providerStatus, 'connected');
28
+ assert.equal(s.connId, 7);
29
+ assert.equal(s.models.length, 1);
30
+ s = reducer(s, { type: 'PICK_MODEL', model: 'byte-gpt' });
31
+ assert.equal(s.step, 'key');
32
+ assert.equal(s.model, 'byte-gpt');
33
+ s = reducer(s, { type: 'KEY_CREATED', key: 'byte_sk_live_abc' });
34
+ assert.equal(s.byteKey, 'byte_sk_live_abc');
35
+ assert.equal(s.byteKeyRevealed, true);
36
+ // KEY_CREATED reveals but does not auto-advance; NEXT moves on.
37
+ s = reducer(s, { type: 'NEXT' });
38
+ assert.equal(s.step, 'preset');
39
+ s = reducer(s, { type: 'PICK_PRESET', preset: 'balanced' });
40
+ assert.equal(s.step, 'integrate');
41
+ assert.equal(s.preset, 'balanced');
42
+ s = reducer(s, { type: 'PICK_CONNECT', connect: 'opencode' });
43
+ assert.equal(s.step, 'done');
44
+ const summary = selectSummary(s);
45
+ assert.equal(summary.provider, 'connected');
46
+ assert.equal(summary.model, 'byte-gpt');
47
+ assert.equal(summary.preset, 'balanced');
48
+ assert.equal(summary.byteKeyCreated, true);
49
+ assert.equal(summary.connect, 'opencode');
50
+ });
51
+ test('PROVIDER_RESULT connected without models skips the model step', () => {
52
+ const s = reducer(base({ step: 'provider' }), { type: 'PROVIDER_RESULT', status: 'connected', models: [], connId: 3 });
53
+ assert.equal(s.step, 'key');
54
+ });
55
+ test('PROVIDER_RESULT saved routes to key and summary reports saved', () => {
56
+ const s = reducer(base({ step: 'provider' }), { type: 'PROVIDER_RESULT', status: 'saved', models: [], connId: 9 });
57
+ assert.equal(s.step, 'key');
58
+ assert.equal(s.providerStatus, 'saved');
59
+ assert.equal(selectSummary(s).provider, 'saved');
60
+ });
61
+ test('ERROR is non-fatal: keeps the step, drops busy, records message', () => {
62
+ const s = reducer(base({ step: 'provider', busy: true }), { type: 'ERROR', from: 'provider', message: 'bad key' });
63
+ assert.equal(s.step, 'provider');
64
+ assert.equal(s.busy, false);
65
+ assert.deepEqual(s.error, { from: 'provider', message: 'bad key' });
66
+ // a following NEXT clears the error
67
+ const cleared = reducer(s, { type: 'CLEAR_ERROR' });
68
+ assert.equal(cleared.error, undefined);
69
+ });
70
+ test('PROVIDER_RETRY returns to the provider step with a pending status', () => {
71
+ const errored = reducer(base({ step: 'provider' }), { type: 'ERROR', from: 'provider', message: 'unreachable' });
72
+ const retry = reducer(errored, { type: 'PROVIDER_RETRY' });
73
+ assert.equal(retry.step, 'provider');
74
+ assert.equal(retry.providerStatus, 'pending');
75
+ assert.equal(retry.error, undefined);
76
+ });
77
+ test('PROVIDER_SKIP marks skipped and summary downgrades pending→skipped', () => {
78
+ const s = reducer(base({ step: 'provider' }), { type: 'PROVIDER_SKIP' });
79
+ assert.equal(s.providerStatus, 'skipped');
80
+ assert.equal(s.step, 'key');
81
+ assert.equal(selectSummary(s).provider, 'skipped');
82
+ // a never-touched provider (still pending) also reports as skipped in the summary
83
+ assert.equal(selectSummary(base()).provider, 'skipped');
84
+ });
85
+ test('selectSummary drops a "skip" connect choice', () => {
86
+ const s = reducer(base({ step: 'integrate' }), { type: 'PICK_CONNECT', connect: 'skip' });
87
+ assert.equal(selectSummary(s).connect, undefined);
88
+ });
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import type { WizardIslandProps, WizardResult } from '../contract.js';
3
+ interface AppProps extends WizardIslandProps {
4
+ onDone: (result: WizardResult) => void;
5
+ }
6
+ export declare function App({ plainColor, ascii, version, initial, ports, providerPresets, presetCards, onDone, }: AppProps): React.ReactElement;
7
+ export {};