@pellux/goodvibes-tui 0.19.24 → 0.19.26
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/CHANGELOG.md +13 -0
- package/README.md +5 -5
- package/bin/goodvibes +10 -0
- package/bin/goodvibes-daemon +10 -0
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +3 -2
- package/src/cli/bundle-command.ts +225 -0
- package/src/cli/completion.ts +90 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +169 -0
- package/src/cli/help.ts +301 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/management-commands.ts +426 -0
- package/src/cli/management.ts +719 -0
- package/src/cli/network-posture.ts +46 -0
- package/src/cli/package-verification.ts +119 -0
- package/src/cli/parser.ts +369 -0
- package/src/cli/provider-classification.ts +107 -0
- package/src/cli/redaction.ts +105 -0
- package/src/cli/service-command.ts +45 -0
- package/src/cli/service-posture.ts +247 -0
- package/src/cli/status.ts +382 -0
- package/src/cli/surface-command.ts +248 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +69 -0
- package/src/cli-flags.ts +18 -55
- package/src/config/index.ts +1 -1
- package/src/config/secrets.ts +44 -0
- package/src/daemon/cli.ts +62 -11
- package/src/input/command-registry.ts +3 -0
- package/src/input/commands/guidance-runtime.ts +9 -4
- package/src/input/commands/local-runtime.ts +21 -7
- package/src/input/commands/local-setup.ts +31 -38
- package/src/input/commands/onboarding-runtime.ts +14 -0
- package/src/input/commands/runtime-services.ts +9 -0
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +8 -1
- package/src/input/handler-feed.ts +13 -8
- package/src/input/handler-interactions.ts +266 -0
- package/src/input/handler-modal-stack.ts +23 -3
- package/src/input/handler-modal-token-routes.ts +23 -1
- package/src/input/handler-onboarding.ts +696 -0
- package/src/input/handler-picker-routes.ts +15 -7
- package/src/input/handler-ui-state.ts +58 -0
- package/src/input/handler.ts +120 -246
- package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
- package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
- package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
- package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
- package/src/input/onboarding/onboarding-wizard.ts +594 -0
- package/src/main.ts +32 -39
- package/src/panels/builtin/operations.ts +0 -10
- package/src/panels/index.ts +0 -1
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
- package/src/runtime/bootstrap-core.ts +1 -0
- package/src/runtime/bootstrap.ts +123 -0
- package/src/runtime/onboarding/apply.ts +685 -0
- package/src/runtime/onboarding/derivation.ts +495 -0
- package/src/runtime/onboarding/index.ts +7 -0
- package/src/runtime/onboarding/markers.ts +161 -0
- package/src/runtime/onboarding/snapshot.ts +400 -0
- package/src/runtime/onboarding/state.ts +140 -0
- package/src/runtime/onboarding/types.ts +402 -0
- package/src/runtime/onboarding/verify.ts +233 -0
- package/src/runtime/ui-services.ts +16 -0
- package/src/shell/ui-openers.ts +12 -2
- package/src/version.ts +1 -1
- package/src/panels/welcome-panel.ts +0 -64
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { networkInterfaces } from 'node:os';
|
|
6
|
+
import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../config/index.ts';
|
|
7
|
+
import { CONFIG_SCHEMA } from '../config/index.ts';
|
|
8
|
+
import { bootstrapRuntime } from '../runtime/bootstrap.ts';
|
|
9
|
+
import { createRuntimeServices } from '../runtime/services.ts';
|
|
10
|
+
import { createRuntimeStore } from '../runtime/store/index.ts';
|
|
11
|
+
import type { RuntimeServices } from '../runtime/services.ts';
|
|
12
|
+
import { SecretsManager } from '../config/secrets.ts';
|
|
13
|
+
import { RuntimeEventBus, type TurnEvent } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
|
|
14
|
+
import { createShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
|
|
15
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
|
|
16
|
+
import { listProviderRuntimeSnapshots } from '@pellux/goodvibes-sdk/platform/providers/runtime-snapshot';
|
|
17
|
+
import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, resolveSecretRef } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
|
|
18
|
+
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
|
|
19
|
+
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
|
|
20
|
+
import { inspectProviderAuth } from '@pellux/goodvibes-sdk/platform/runtime/auth/inspection';
|
|
21
|
+
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing/index';
|
|
22
|
+
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
|
|
23
|
+
import type { GoodVibesCliParseResult } from './types.ts';
|
|
24
|
+
import { classifyProviderSetup } from './provider-classification.ts';
|
|
25
|
+
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
26
|
+
import { applyRuntimeEndpointFlagOverrides } from './config-overrides.ts';
|
|
27
|
+
import type { RuntimeEndpointId } from './endpoints.ts';
|
|
28
|
+
import { handleServiceCommand } from './service-command.ts';
|
|
29
|
+
import { handleBundleCommand } from './bundle-command.ts';
|
|
30
|
+
import { buildListenerTestResult, formatListenerTestResult, handleSurfacesCommand } from './surface-command.ts';
|
|
31
|
+
import { buildControlPlaneStatusResult, formatControlPlaneStatus, handleSecrets, handleSessions, handleTasks, renderPairing, renderRemote, renderSubscriptions, renderWeb } from './management-commands.ts';
|
|
32
|
+
|
|
33
|
+
export interface CliCommandRuntime {
|
|
34
|
+
readonly cli: GoodVibesCliParseResult;
|
|
35
|
+
readonly configManager: ConfigManager;
|
|
36
|
+
readonly workingDirectory: string;
|
|
37
|
+
readonly homeDirectory: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CliCommandResult {
|
|
41
|
+
readonly handled: boolean;
|
|
42
|
+
readonly exitCode: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Formatter = (value: unknown, text: string) => string;
|
|
46
|
+
|
|
47
|
+
export function yesNo(value: unknown): string {
|
|
48
|
+
return value === true ? 'yes' : 'no';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatJsonOrText(cli: GoodVibesCliParseResult): Formatter {
|
|
52
|
+
return (value, text) => cli.flags.outputFormat === 'json'
|
|
53
|
+
? JSON.stringify(value, null, 2)
|
|
54
|
+
: text;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function exitCodeForText(output: string): number {
|
|
58
|
+
if (output.startsWith('Usage:') || output.startsWith('Invalid ')) return 2;
|
|
59
|
+
if (output.startsWith('Session not found:') || output.startsWith('Unknown task:') || output.startsWith('Task submit failed ')) return 1;
|
|
60
|
+
if (output.startsWith('No stored ') || output.startsWith('No pending ') || output.startsWith('No model ') || output.startsWith('No provider ') || output.startsWith('No auth ')) return 1;
|
|
61
|
+
if (output.startsWith('Unknown ')) return 1;
|
|
62
|
+
if (output === 'Bundle has no config object to import.') return 1;
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function splitCommandOption(token: string): { readonly name: string; readonly value: string | undefined } {
|
|
67
|
+
const index = token.indexOf('=');
|
|
68
|
+
if (index < 0) return { name: token, value: undefined };
|
|
69
|
+
return { name: token.slice(0, index), value: token.slice(index + 1) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readOptionValue(args: readonly string[], name: string): string | undefined {
|
|
73
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
74
|
+
const token = args[index]!;
|
|
75
|
+
const split = splitCommandOption(token);
|
|
76
|
+
if (split.name !== name) continue;
|
|
77
|
+
if (split.value !== undefined) return split.value;
|
|
78
|
+
const next = args[index + 1];
|
|
79
|
+
if (next === undefined || next.startsWith('--')) return undefined;
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readOptionValues(args: readonly string[], name: string): string[] {
|
|
86
|
+
const values: string[] = [];
|
|
87
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
88
|
+
const token = args[index]!;
|
|
89
|
+
const split = splitCommandOption(token);
|
|
90
|
+
if (split.name !== name) continue;
|
|
91
|
+
if (split.value !== undefined) {
|
|
92
|
+
values.push(split.value);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const next = args[index + 1];
|
|
96
|
+
if (next !== undefined && !next.startsWith('--')) values.push(next);
|
|
97
|
+
}
|
|
98
|
+
return values;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function hasCommandFlag(args: readonly string[], name: string): boolean {
|
|
102
|
+
return args.some((arg) => splitCommandOption(arg).name === name);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function commandValues(args: readonly string[]): string[] {
|
|
106
|
+
const values: string[] = [];
|
|
107
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
108
|
+
const token = args[index]!;
|
|
109
|
+
if (!token.startsWith('--')) {
|
|
110
|
+
values.push(token);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!token.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1;
|
|
114
|
+
}
|
|
115
|
+
return values;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readPassword(args: readonly string[]): string | null {
|
|
119
|
+
const explicit = readOptionValue(args, '--password');
|
|
120
|
+
if (explicit !== undefined) return explicit;
|
|
121
|
+
if (hasCommandFlag(args, '--password-stdin')) return readFileSync(0, 'utf-8').trimEnd();
|
|
122
|
+
return process.env.GOODVIBES_AUTH_PASSWORD ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function extractAuthorizationCode(input: string): string {
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(input);
|
|
128
|
+
return url.searchParams.get('code') ?? input;
|
|
129
|
+
} catch {
|
|
130
|
+
return input;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isPresentConfigValue(value: unknown): boolean {
|
|
135
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
136
|
+
return value !== undefined && value !== null && value !== false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function inferProviderFromRegistryKey(modelKey: string): string {
|
|
140
|
+
if (modelKey.includes(':')) return modelKey.split(':')[0] || 'openai';
|
|
141
|
+
if (modelKey.includes('/')) return modelKey.split('/')[0] || 'openai';
|
|
142
|
+
return 'openai';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getNestedValue(source: unknown, key: string): unknown {
|
|
146
|
+
let cursor = source;
|
|
147
|
+
for (const part of key.split('.')) {
|
|
148
|
+
if (cursor == null || typeof cursor !== 'object') return undefined;
|
|
149
|
+
cursor = (cursor as Record<string, unknown>)[part];
|
|
150
|
+
}
|
|
151
|
+
return cursor;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getLocalNetworkIp(): string {
|
|
155
|
+
try {
|
|
156
|
+
const nets = networkInterfaces();
|
|
157
|
+
for (const name of Object.keys(nets)) {
|
|
158
|
+
for (const netInfo of nets[name] ?? []) {
|
|
159
|
+
if (netInfo.family === 'IPv4' && !netInfo.internal) return netInfo.address;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
return '127.0.0.1';
|
|
164
|
+
}
|
|
165
|
+
return '127.0.0.1';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function connectHostForBindHost(host: string): string {
|
|
169
|
+
if (host === '0.0.0.0' || host === '::') return '127.0.0.1';
|
|
170
|
+
return host || '127.0.0.1';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function urlHostForBindHost(host: string): string {
|
|
174
|
+
if (host === '0.0.0.0' || host === '::') return getLocalNetworkIp();
|
|
175
|
+
return host || '127.0.0.1';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function enableServicePosture(config: ConfigManager): void {
|
|
179
|
+
config.setDynamic('service.enabled', true);
|
|
180
|
+
config.setDynamic('service.autostart', true);
|
|
181
|
+
config.setDynamic('service.restartOnFailure', true);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function enableEndpointLanDefault(config: ConfigManager, endpoint: RuntimeEndpointId): void {
|
|
185
|
+
const binding = resolveRuntimeEndpointBinding(config, endpoint);
|
|
186
|
+
if (binding.hostMode === 'custom') return;
|
|
187
|
+
if (endpoint === 'controlPlane') {
|
|
188
|
+
config.setDynamic('controlPlane.hostMode', 'network');
|
|
189
|
+
config.setDynamic('controlPlane.host', '0.0.0.0');
|
|
190
|
+
config.setDynamic('controlPlane.allowRemote', true);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (endpoint === 'httpListener') {
|
|
194
|
+
config.setDynamic('httpListener.hostMode', 'network');
|
|
195
|
+
config.setDynamic('httpListener.host', '0.0.0.0');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
config.setDynamic('web.hostMode', 'network');
|
|
199
|
+
config.setDynamic('web.host', '0.0.0.0');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function applyTargetEndpointFlagsOrDefault(
|
|
203
|
+
runtime: CliCommandRuntime,
|
|
204
|
+
endpoint: RuntimeEndpointId,
|
|
205
|
+
): string | null {
|
|
206
|
+
const errors = applyRuntimeEndpointFlagOverrides(runtime.configManager, endpoint, runtime.cli.flags);
|
|
207
|
+
if (errors.length > 0) return errors.join('\n');
|
|
208
|
+
if (runtime.cli.flags.hostname === undefined) {
|
|
209
|
+
enableEndpointLanDefault(runtime.configManager, endpoint);
|
|
210
|
+
}
|
|
211
|
+
if (endpoint === 'controlPlane') {
|
|
212
|
+
const binding = resolveRuntimeEndpointBinding(runtime.configManager, endpoint);
|
|
213
|
+
runtime.configManager.setDynamic('controlPlane.allowRemote', binding.hostMode !== 'local');
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function openBrowser(url: string): string {
|
|
219
|
+
const platform = process.platform;
|
|
220
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
221
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
222
|
+
try {
|
|
223
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
224
|
+
child.once('error', () => {});
|
|
225
|
+
child.unref();
|
|
226
|
+
return 'browser open requested';
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return `browser open failed: ${summarizeError(error)}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function probeTcp(host: string, port: number, timeoutMs = 750): Promise<boolean> {
|
|
233
|
+
return await new Promise<boolean>((resolve) => {
|
|
234
|
+
const socket = net.createConnection({ host: connectHostForBindHost(host), port });
|
|
235
|
+
const finish = (value: boolean) => {
|
|
236
|
+
socket.removeAllListeners();
|
|
237
|
+
socket.destroy();
|
|
238
|
+
resolve(value);
|
|
239
|
+
};
|
|
240
|
+
socket.setTimeout(timeoutMs);
|
|
241
|
+
socket.once('connect', () => finish(true));
|
|
242
|
+
socket.once('timeout', () => finish(false));
|
|
243
|
+
socket.once('error', () => finish(false));
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function withRuntimeServices<T>(
|
|
248
|
+
runtime: CliCommandRuntime,
|
|
249
|
+
fn: (services: RuntimeServices) => Promise<T> | T,
|
|
250
|
+
): Promise<T> {
|
|
251
|
+
const runtimeBus = new RuntimeEventBus();
|
|
252
|
+
const runtimeStore = createRuntimeStore();
|
|
253
|
+
const services = createRuntimeServices({
|
|
254
|
+
configManager: runtime.configManager,
|
|
255
|
+
runtimeBus,
|
|
256
|
+
runtimeStore,
|
|
257
|
+
workingDir: runtime.workingDirectory,
|
|
258
|
+
homeDirectory: runtime.homeDirectory,
|
|
259
|
+
});
|
|
260
|
+
services.providerRegistry.initModelLimits();
|
|
261
|
+
services.benchmarkStore.initBenchmarks();
|
|
262
|
+
services.providerRegistry.initCatalog();
|
|
263
|
+
try {
|
|
264
|
+
await services.providerRegistry.ready();
|
|
265
|
+
return await fn(services);
|
|
266
|
+
} finally {
|
|
267
|
+
services.providerRegistry.stopWatching();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
272
|
+
const shellPaths = createShellPathService({
|
|
273
|
+
workingDirectory: runtime.workingDirectory,
|
|
274
|
+
homeDirectory: runtime.homeDirectory,
|
|
275
|
+
});
|
|
276
|
+
const userStorePath = shellPaths.resolveUserPath('tui', 'auth-users.json');
|
|
277
|
+
const bootstrapCredentialPath = shellPaths.resolveUserPath('tui', 'auth-bootstrap.txt');
|
|
278
|
+
const operatorTokenPath = join(runtime.homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
|
|
279
|
+
return {
|
|
280
|
+
userStorePath,
|
|
281
|
+
userStorePresent: existsSync(userStorePath),
|
|
282
|
+
bootstrapCredentialPath,
|
|
283
|
+
bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
|
|
284
|
+
operatorTokenPath,
|
|
285
|
+
operatorTokenPresent: existsSync(operatorTokenPath),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
|
|
290
|
+
const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
|
|
291
|
+
if (!prompt) {
|
|
292
|
+
console.error('Usage: goodvibes run|exec [prompt]');
|
|
293
|
+
return 2;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const outputFormat = runtime.cli.flags.outputFormat;
|
|
297
|
+
const ctx = await bootstrapRuntime(process.stdout, {
|
|
298
|
+
configManager: runtime.configManager,
|
|
299
|
+
workingDir: runtime.workingDirectory,
|
|
300
|
+
homeDirectory: runtime.homeDirectory,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const events: TurnEvent[] = [];
|
|
304
|
+
let finalResponse = '';
|
|
305
|
+
let finalError = '';
|
|
306
|
+
let finalStopReason = '';
|
|
307
|
+
let exitCode = 0;
|
|
308
|
+
|
|
309
|
+
const done = new Promise<void>((resolve) => {
|
|
310
|
+
const unsubs = [
|
|
311
|
+
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'STREAM_DELTA' }>>('STREAM_DELTA', ({ payload }) => {
|
|
312
|
+
events.push(payload);
|
|
313
|
+
if (outputFormat === 'stream-json') {
|
|
314
|
+
process.stdout.write(JSON.stringify({ type: payload.type, content: payload.content, accumulated: payload.accumulated }) + '\n');
|
|
315
|
+
}
|
|
316
|
+
}),
|
|
317
|
+
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_COMPLETED' }>>('TURN_COMPLETED', ({ payload }) => {
|
|
318
|
+
events.push(payload);
|
|
319
|
+
finalResponse = payload.response;
|
|
320
|
+
finalStopReason = payload.stopReason;
|
|
321
|
+
for (const unsub of unsubs) unsub();
|
|
322
|
+
resolve();
|
|
323
|
+
}),
|
|
324
|
+
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_ERROR' }>>('TURN_ERROR', ({ payload }) => {
|
|
325
|
+
events.push(payload);
|
|
326
|
+
finalError = payload.error;
|
|
327
|
+
finalStopReason = payload.stopReason;
|
|
328
|
+
exitCode = 1;
|
|
329
|
+
for (const unsub of unsubs) unsub();
|
|
330
|
+
resolve();
|
|
331
|
+
}),
|
|
332
|
+
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_CANCEL' }>>('TURN_CANCEL', ({ payload }) => {
|
|
333
|
+
events.push(payload);
|
|
334
|
+
finalError = payload.reason ?? 'cancelled';
|
|
335
|
+
finalStopReason = payload.stopReason;
|
|
336
|
+
exitCode = 130;
|
|
337
|
+
for (const unsub of unsubs) unsub();
|
|
338
|
+
resolve();
|
|
339
|
+
}),
|
|
340
|
+
];
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
await ctx.orchestrator.handleUserInput(prompt);
|
|
345
|
+
await done;
|
|
346
|
+
if (outputFormat === 'json') {
|
|
347
|
+
process.stdout.write(JSON.stringify({
|
|
348
|
+
ok: exitCode === 0,
|
|
349
|
+
response: finalResponse,
|
|
350
|
+
error: finalError || undefined,
|
|
351
|
+
stopReason: finalStopReason,
|
|
352
|
+
sessionId: ctx.runtime.sessionId,
|
|
353
|
+
model: ctx.runtime.model,
|
|
354
|
+
provider: ctx.runtime.provider,
|
|
355
|
+
events: events.length,
|
|
356
|
+
}, null, 2) + '\n');
|
|
357
|
+
} else if (outputFormat !== 'stream-json') {
|
|
358
|
+
process.stdout.write((exitCode === 0 ? finalResponse : finalError) + '\n');
|
|
359
|
+
} else {
|
|
360
|
+
process.stdout.write(JSON.stringify({
|
|
361
|
+
type: exitCode === 0 ? 'TURN_COMPLETED' : 'TURN_ERROR',
|
|
362
|
+
ok: exitCode === 0,
|
|
363
|
+
response: finalResponse,
|
|
364
|
+
error: finalError || undefined,
|
|
365
|
+
stopReason: finalStopReason,
|
|
366
|
+
}) + '\n');
|
|
367
|
+
}
|
|
368
|
+
} finally {
|
|
369
|
+
const snapshot = ctx.conversation.toJSON() as Parameters<typeof ctx.shutdown>[0];
|
|
370
|
+
await ctx.shutdown(snapshot);
|
|
371
|
+
}
|
|
372
|
+
return exitCode;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
376
|
+
return await withRuntimeServices(runtime, async (services) => {
|
|
377
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
378
|
+
const snapshots = await listProviderRuntimeSnapshots(services.providerRegistry);
|
|
379
|
+
const current = services.providerRegistry.getCurrentModel();
|
|
380
|
+
if (sub === 'current') {
|
|
381
|
+
const value = { provider: current.provider, model: current.registryKey };
|
|
382
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
383
|
+
'GoodVibes current provider',
|
|
384
|
+
` provider: ${current.provider}`,
|
|
385
|
+
` model: ${current.registryKey}`,
|
|
386
|
+
].join('\n'));
|
|
387
|
+
}
|
|
388
|
+
if (sub === 'use' || sub === 'set') {
|
|
389
|
+
const provider = rest[0];
|
|
390
|
+
if (!provider) return 'Usage: goodvibes providers use <provider> [modelRegistryKey]';
|
|
391
|
+
const providerModels = services.providerRegistry
|
|
392
|
+
.getSelectableModels()
|
|
393
|
+
.filter((model) => model.provider === provider || model.registryKey.startsWith(`${provider}:`));
|
|
394
|
+
const requestedModel = rest[1];
|
|
395
|
+
const selected = requestedModel
|
|
396
|
+
? providerModels.find((model) => model.registryKey === requestedModel || model.id === requestedModel)
|
|
397
|
+
: providerModels.find((model) => model.registryKey === current.registryKey) ?? providerModels[0];
|
|
398
|
+
if (providerModels.length === 0 || !selected) {
|
|
399
|
+
runtime.configManager.setDynamic('provider.provider', provider);
|
|
400
|
+
if (requestedModel) runtime.configManager.setDynamic('provider.model', requestedModel);
|
|
401
|
+
return requestedModel
|
|
402
|
+
? `Provider selected: ${provider} (${requestedModel})\n warning: model catalog entry was not available locally; saved explicit selection.`
|
|
403
|
+
: `Provider selected: ${provider}\n warning: model catalog entry was not available locally; model selection was left unchanged.`;
|
|
404
|
+
}
|
|
405
|
+
runtime.configManager.setDynamic('provider.provider', selected.provider);
|
|
406
|
+
runtime.configManager.setDynamic('provider.model', selected.registryKey);
|
|
407
|
+
return `Provider selected: ${selected.provider} (${selected.registryKey})`;
|
|
408
|
+
}
|
|
409
|
+
if (sub === 'inspect' || sub === 'show') {
|
|
410
|
+
const provider = rest[0];
|
|
411
|
+
if (!provider) return 'Usage: goodvibes providers inspect <provider>';
|
|
412
|
+
const snapshot = snapshots.find((candidate) => candidate.providerId === provider);
|
|
413
|
+
if (!snapshot) return `No provider found: ${provider}`;
|
|
414
|
+
const setup = classifyProviderSetup({
|
|
415
|
+
providerId: snapshot.providerId,
|
|
416
|
+
authMode: snapshot.runtime.auth?.mode,
|
|
417
|
+
configured: snapshot.runtime.auth?.configured ?? true,
|
|
418
|
+
modelCount: snapshot.modelCount,
|
|
419
|
+
});
|
|
420
|
+
return formatJsonOrText(runtime.cli)({ ...snapshot, setup }, [
|
|
421
|
+
`Provider ${snapshot.providerId}`,
|
|
422
|
+
` active: ${yesNo(snapshot.active)}`,
|
|
423
|
+
` setup: ${setup.setupLabel}`,
|
|
424
|
+
` configured: ${yesNo(snapshot.runtime.auth?.configured ?? true)}`,
|
|
425
|
+
` via: ${snapshot.runtime.auth?.mode ?? 'unknown'}`,
|
|
426
|
+
` models: ${snapshot.modelCount}`,
|
|
427
|
+
` detail: ${snapshot.runtime.auth?.detail ?? snapshot.runtime.notes?.join('; ') ?? ''}`,
|
|
428
|
+
].join('\n'));
|
|
429
|
+
}
|
|
430
|
+
if (sub !== 'list') return 'Usage: goodvibes providers [list|current|inspect <provider>|use <provider> [modelRegistryKey]]';
|
|
431
|
+
const value = snapshots.map((snapshot) => ({
|
|
432
|
+
...classifyProviderSetup({
|
|
433
|
+
providerId: snapshot.providerId,
|
|
434
|
+
authMode: snapshot.runtime.auth?.mode,
|
|
435
|
+
configured: snapshot.runtime.auth?.configured ?? true,
|
|
436
|
+
modelCount: snapshot.modelCount,
|
|
437
|
+
}),
|
|
438
|
+
provider: snapshot.providerId,
|
|
439
|
+
active: snapshot.active,
|
|
440
|
+
configured: snapshot.runtime.auth?.configured ?? true,
|
|
441
|
+
configuredVia: snapshot.runtime.auth?.mode ?? 'unknown',
|
|
442
|
+
models: snapshot.modelCount,
|
|
443
|
+
current: current.provider === snapshot.providerId,
|
|
444
|
+
detail: snapshot.runtime.auth?.detail ?? snapshot.runtime.notes?.join('; ') ?? '',
|
|
445
|
+
}));
|
|
446
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
447
|
+
'GoodVibes providers',
|
|
448
|
+
...value.map((provider) =>
|
|
449
|
+
` ${provider.current ? '*' : ' '} ${provider.provider.padEnd(18)} setup=${provider.setupClass} configured=${yesNo(provider.configured)} via=${provider.configuredVia ?? 'n/a'} models=${provider.models} ${provider.detail ?? ''}`.trimEnd(),
|
|
450
|
+
),
|
|
451
|
+
].join('\n'));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
456
|
+
return await withRuntimeServices(runtime, async (services) => {
|
|
457
|
+
const [subOrFilter, ...rest] = runtime.cli.commandArgs;
|
|
458
|
+
const current = services.providerRegistry.getCurrentModel().registryKey;
|
|
459
|
+
const providerSnapshots = await listProviderRuntimeSnapshots(services.providerRegistry);
|
|
460
|
+
const classifyModelProvider = (providerId: string) => {
|
|
461
|
+
const snapshot = providerSnapshots.find((candidate) => candidate.providerId === providerId);
|
|
462
|
+
return classifyProviderSetup({
|
|
463
|
+
providerId,
|
|
464
|
+
authMode: snapshot?.runtime.auth?.mode,
|
|
465
|
+
configured: snapshot?.runtime.auth?.configured,
|
|
466
|
+
modelCount: snapshot?.modelCount,
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
if (subOrFilter === 'current') {
|
|
470
|
+
const model = services.providerRegistry.getCurrentModel();
|
|
471
|
+
const setup = classifyModelProvider(model.provider);
|
|
472
|
+
const providerSnapshot = providerSnapshots.find((candidate) => candidate.providerId === model.provider);
|
|
473
|
+
const value = {
|
|
474
|
+
registryKey: model.registryKey,
|
|
475
|
+
provider: model.provider,
|
|
476
|
+
id: model.id,
|
|
477
|
+
displayName: model.displayName,
|
|
478
|
+
contextWindow: services.providerRegistry.getContextWindowForModel(model),
|
|
479
|
+
providerConfigured: providerSnapshot?.runtime.auth?.configured ?? true,
|
|
480
|
+
setup,
|
|
481
|
+
};
|
|
482
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
483
|
+
'GoodVibes current model',
|
|
484
|
+
` model: ${model.registryKey}`,
|
|
485
|
+
` provider: ${model.provider}`,
|
|
486
|
+
` setup: ${setup.setupLabel}`,
|
|
487
|
+
` provider configured: ${yesNo(value.providerConfigured)}`,
|
|
488
|
+
` context: ${value.contextWindow.toLocaleString()}`,
|
|
489
|
+
].join('\n'));
|
|
490
|
+
}
|
|
491
|
+
if (subOrFilter === 'use' || subOrFilter === 'set') {
|
|
492
|
+
const modelKey = rest[0];
|
|
493
|
+
if (!modelKey) return 'Usage: goodvibes models use <registryKey>';
|
|
494
|
+
const model = services.providerRegistry
|
|
495
|
+
.getSelectableModels()
|
|
496
|
+
.find((candidate) => candidate.registryKey === modelKey || candidate.id === modelKey);
|
|
497
|
+
if (!model) {
|
|
498
|
+
const provider = inferProviderFromRegistryKey(modelKey);
|
|
499
|
+
runtime.configManager.setDynamic('provider.provider', provider);
|
|
500
|
+
runtime.configManager.setDynamic('provider.model', modelKey);
|
|
501
|
+
await services.favoritesStore.recordUsage(modelKey);
|
|
502
|
+
return `Model selected: ${modelKey}\n warning: model catalog entry was not available locally; saved explicit selection.`;
|
|
503
|
+
}
|
|
504
|
+
runtime.configManager.setDynamic('provider.provider', model.provider);
|
|
505
|
+
runtime.configManager.setDynamic('provider.model', model.registryKey);
|
|
506
|
+
await services.favoritesStore.recordUsage(model.registryKey);
|
|
507
|
+
return `Model selected: ${model.registryKey}`;
|
|
508
|
+
}
|
|
509
|
+
if (subOrFilter === 'pin' || subOrFilter === 'unpin') {
|
|
510
|
+
const modelKey = rest[0];
|
|
511
|
+
if (!modelKey) return `Usage: goodvibes models ${subOrFilter} <registryKey>`;
|
|
512
|
+
if (subOrFilter === 'pin') await services.favoritesStore.pinModel(modelKey);
|
|
513
|
+
else await services.favoritesStore.unpinModel(modelKey);
|
|
514
|
+
return `Model ${subOrFilter === 'pin' ? 'pinned' : 'unpinned'}: ${modelKey}`;
|
|
515
|
+
}
|
|
516
|
+
if (subOrFilter === 'pinned') {
|
|
517
|
+
const pinned = await services.favoritesStore.getPinned();
|
|
518
|
+
return formatJsonOrText(runtime.cli)({ pinned }, [
|
|
519
|
+
`GoodVibes pinned models (${pinned.length})`,
|
|
520
|
+
...pinned.map((model) => ` ${model}`),
|
|
521
|
+
].join('\n'));
|
|
522
|
+
}
|
|
523
|
+
if (subOrFilter === 'recent') {
|
|
524
|
+
const recent = await services.favoritesStore.getRecentModels(25);
|
|
525
|
+
return formatJsonOrText(runtime.cli)({ recent }, [
|
|
526
|
+
`GoodVibes recent models (${recent.length})`,
|
|
527
|
+
...recent.map((model) => ` ${model}`),
|
|
528
|
+
].join('\n'));
|
|
529
|
+
}
|
|
530
|
+
const filter = subOrFilter === 'list' ? rest[0]?.toLowerCase() : subOrFilter?.toLowerCase();
|
|
531
|
+
const models = services.providerRegistry
|
|
532
|
+
.getSelectableModels()
|
|
533
|
+
.filter((model) => !filter || model.provider.toLowerCase() === filter || model.registryKey.toLowerCase().includes(filter))
|
|
534
|
+
.slice(0, 200);
|
|
535
|
+
const value = models.map((model) => ({
|
|
536
|
+
registryKey: model.registryKey,
|
|
537
|
+
provider: model.provider,
|
|
538
|
+
...classifyModelProvider(model.provider),
|
|
539
|
+
id: model.id,
|
|
540
|
+
displayName: model.displayName,
|
|
541
|
+
contextWindow: services.providerRegistry.getContextWindowForModel(model),
|
|
542
|
+
current: model.registryKey === current,
|
|
543
|
+
}));
|
|
544
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
545
|
+
`GoodVibes models${filter ? ` (${filter})` : ''}`,
|
|
546
|
+
...value.map((model) => ` ${model.current ? '*' : ' '} ${model.registryKey.padEnd(42)} setup=${model.setupClass} ctx=${model.contextWindow.toLocaleString()} ${model.displayName}`),
|
|
547
|
+
].join('\n'));
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
552
|
+
return await withRuntimeServices(runtime, (services) => {
|
|
553
|
+
const [sub = 'status', ...rawRest] = runtime.cli.commandArgs;
|
|
554
|
+
const rest = commandValues(rawRest);
|
|
555
|
+
if (sub === 'add-user' || sub === 'add') {
|
|
556
|
+
const username = rest[0];
|
|
557
|
+
if (!username) return 'Usage: goodvibes auth add-user <username> [--password <value>|--password-stdin] [--role <role>]';
|
|
558
|
+
const password = readPassword(rawRest);
|
|
559
|
+
if (!password) return 'Usage: goodvibes auth add-user <username> [--password <value>|--password-stdin] [--role <role>]';
|
|
560
|
+
const roles = readOptionValues(rawRest, '--role').filter((role) => role.length > 0);
|
|
561
|
+
const user = services.localUserAuthManager.addUser(username, password, roles.length > 0 ? roles : ['user']);
|
|
562
|
+
return `Auth user added: ${user.username} (${user.roles.join(', ') || 'no roles'})`;
|
|
563
|
+
}
|
|
564
|
+
if (sub === 'delete-user' || sub === 'remove-user') {
|
|
565
|
+
const username = rest[0];
|
|
566
|
+
if (!username) return 'Usage: goodvibes auth delete-user <username>';
|
|
567
|
+
return services.localUserAuthManager.deleteUser(username)
|
|
568
|
+
? `Auth user deleted: ${username}`
|
|
569
|
+
: `No auth user found: ${username}`;
|
|
570
|
+
}
|
|
571
|
+
if (sub === 'rotate-password' || sub === 'passwd') {
|
|
572
|
+
const username = rest[0];
|
|
573
|
+
if (!username) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
|
|
574
|
+
const password = readPassword(rawRest);
|
|
575
|
+
if (!password) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
|
|
576
|
+
services.localUserAuthManager.rotatePassword(username, password);
|
|
577
|
+
return `Auth password rotated: ${username}`;
|
|
578
|
+
}
|
|
579
|
+
if (sub === 'revoke-session') {
|
|
580
|
+
const token = rest[0];
|
|
581
|
+
if (!token) return 'Usage: goodvibes auth revoke-session <token>';
|
|
582
|
+
return services.localUserAuthManager.revokeSession(token)
|
|
583
|
+
? 'Auth session revoked.'
|
|
584
|
+
: 'No auth session found.';
|
|
585
|
+
}
|
|
586
|
+
if (sub === 'revoke-sessions') {
|
|
587
|
+
const username = rest[0];
|
|
588
|
+
if (!username) return 'Usage: goodvibes auth revoke-sessions <username>';
|
|
589
|
+
const count = services.localUserAuthManager.revokeSessionsForUser(username);
|
|
590
|
+
return `Auth sessions revoked for ${username}: ${count}`;
|
|
591
|
+
}
|
|
592
|
+
if (sub === 'clear-bootstrap') {
|
|
593
|
+
return services.localUserAuthManager.clearBootstrapCredentialFile()
|
|
594
|
+
? 'Bootstrap credential file removed.'
|
|
595
|
+
: 'Bootstrap credential file was already absent.';
|
|
596
|
+
}
|
|
597
|
+
if (sub !== 'status' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
|
|
598
|
+
return 'Usage: goodvibes auth [status|users|sessions|add-user|delete-user|rotate-password|revoke-session|revoke-sessions|clear-bootstrap]';
|
|
599
|
+
}
|
|
600
|
+
const snapshot = services.localUserAuthManager.inspect();
|
|
601
|
+
const paths = readAuthPaths(runtime);
|
|
602
|
+
const value = {
|
|
603
|
+
...paths,
|
|
604
|
+
users: snapshot.users.map((user) => ({ username: user.username, roles: user.roles })),
|
|
605
|
+
sessions: snapshot.sessions.length,
|
|
606
|
+
permissionMode: runtime.configManager.get('permissions.mode'),
|
|
607
|
+
};
|
|
608
|
+
if (sub === 'users') {
|
|
609
|
+
return formatJsonOrText(runtime.cli)(value.users, [
|
|
610
|
+
`GoodVibes auth users (${value.users.length})`,
|
|
611
|
+
...value.users.map((user) => ` ${user.username} (${user.roles.join(', ') || 'no roles'})`),
|
|
612
|
+
].join('\n'));
|
|
613
|
+
}
|
|
614
|
+
if (sub === 'sessions') {
|
|
615
|
+
return formatJsonOrText(runtime.cli)(snapshot.sessions, [
|
|
616
|
+
`GoodVibes auth sessions (${snapshot.sessions.length})`,
|
|
617
|
+
...snapshot.sessions.map((session) => ` ${session.username} expires=${new Date(session.expiresAt).toISOString()} token=${session.token.slice(0, 8)}...`),
|
|
618
|
+
].join('\n'));
|
|
619
|
+
}
|
|
620
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
621
|
+
'GoodVibes auth',
|
|
622
|
+
` permission mode: ${String(value.permissionMode)}`,
|
|
623
|
+
` users: ${value.users.length}`,
|
|
624
|
+
...value.users.map((user) => ` ${user.username} (${user.roles.join(', ') || 'no roles'})`),
|
|
625
|
+
` sessions: ${value.sessions}`,
|
|
626
|
+
` user store: ${paths.userStorePresent ? 'present' : 'missing'} (${paths.userStorePath})`,
|
|
627
|
+
` bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
|
|
628
|
+
` operator tokens: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
|
|
629
|
+
].join('\n'));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Promise<CliCommandResult> {
|
|
635
|
+
try {
|
|
636
|
+
switch (runtime.cli.command) {
|
|
637
|
+
case 'run':
|
|
638
|
+
return { handled: true, exitCode: await runNonInteractiveAgent(runtime) };
|
|
639
|
+
case 'web':
|
|
640
|
+
console.log(renderWeb(runtime));
|
|
641
|
+
return { handled: true, exitCode: 0 };
|
|
642
|
+
case 'service': {
|
|
643
|
+
const result = await handleServiceCommand(runtime);
|
|
644
|
+
console.log(result.output);
|
|
645
|
+
return { handled: true, exitCode: result.exitCode };
|
|
646
|
+
}
|
|
647
|
+
case 'providers': {
|
|
648
|
+
const output = await renderProviders(runtime);
|
|
649
|
+
console.log(output);
|
|
650
|
+
return { handled: true, exitCode: exitCodeForText(output) };
|
|
651
|
+
}
|
|
652
|
+
case 'models': {
|
|
653
|
+
const output = await renderModels(runtime);
|
|
654
|
+
console.log(output);
|
|
655
|
+
return { handled: true, exitCode: exitCodeForText(output) };
|
|
656
|
+
}
|
|
657
|
+
case 'auth': {
|
|
658
|
+
const output = await renderAuth(runtime);
|
|
659
|
+
console.log(output);
|
|
660
|
+
return { handled: true, exitCode: exitCodeForText(output) };
|
|
661
|
+
}
|
|
662
|
+
case 'subscription': {
|
|
663
|
+
const output = await renderSubscriptions(runtime);
|
|
664
|
+
console.log(output);
|
|
665
|
+
return { handled: true, exitCode: exitCodeForText(output) };
|
|
666
|
+
}
|
|
667
|
+
case 'secrets': {
|
|
668
|
+
const output = await handleSecrets(runtime);
|
|
669
|
+
console.log(output);
|
|
670
|
+
return { handled: true, exitCode: exitCodeForText(output) };
|
|
671
|
+
}
|
|
672
|
+
case 'sessions': {
|
|
673
|
+
const output = await handleSessions(runtime);
|
|
674
|
+
if (output === null) return { handled: false, exitCode: 0 };
|
|
675
|
+
console.log(output);
|
|
676
|
+
return { handled: true, exitCode: exitCodeForText(output) };
|
|
677
|
+
}
|
|
678
|
+
case 'tasks': {
|
|
679
|
+
const output = await handleTasks(runtime);
|
|
680
|
+
if (output) console.log(output);
|
|
681
|
+
return { handled: true, exitCode: exitCodeForText(output) };
|
|
682
|
+
}
|
|
683
|
+
case 'surfaces': {
|
|
684
|
+
const result = await handleSurfacesCommand(runtime);
|
|
685
|
+
console.log(result.output);
|
|
686
|
+
return { handled: true, exitCode: result.exitCode };
|
|
687
|
+
}
|
|
688
|
+
case 'listener': {
|
|
689
|
+
const result = await buildListenerTestResult(runtime);
|
|
690
|
+
console.log(formatListenerTestResult(runtime, result));
|
|
691
|
+
return { handled: true, exitCode: result.issues.length > 0 ? 1 : 0 };
|
|
692
|
+
}
|
|
693
|
+
case 'control-plane': {
|
|
694
|
+
const result = await buildControlPlaneStatusResult(runtime);
|
|
695
|
+
console.log(formatControlPlaneStatus(runtime, result));
|
|
696
|
+
return { handled: true, exitCode: result.issues.length > 0 ? 1 : 0 };
|
|
697
|
+
}
|
|
698
|
+
case 'pair':
|
|
699
|
+
console.log(await renderPairing(runtime));
|
|
700
|
+
return { handled: true, exitCode: 0 };
|
|
701
|
+
case 'bundle': {
|
|
702
|
+
const result = await handleBundleCommand(runtime);
|
|
703
|
+
console.log(result.output);
|
|
704
|
+
return { handled: true, exitCode: result.exitCode };
|
|
705
|
+
}
|
|
706
|
+
case 'remote':
|
|
707
|
+
console.log(await renderRemote(runtime, 'remote'));
|
|
708
|
+
return { handled: true, exitCode: 0 };
|
|
709
|
+
case 'bridge':
|
|
710
|
+
console.log(await renderRemote(runtime, 'bridge'));
|
|
711
|
+
return { handled: true, exitCode: 0 };
|
|
712
|
+
default:
|
|
713
|
+
return { handled: false, exitCode: 0 };
|
|
714
|
+
}
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.error(summarizeError(error));
|
|
717
|
+
return { handled: true, exitCode: 1 };
|
|
718
|
+
}
|
|
719
|
+
}
|