@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.
- 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 +96 -1
- package/dist/hooks/init/home.js +24 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +6 -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/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 +151 -151
- 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
|
-
|
|
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
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -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: '
|
|
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() {
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/hooks/init/home.js
CHANGED
|
@@ -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
|
+
});
|
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
|
+
}
|
|
@@ -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 {};
|