@pellux/goodvibes-tui 0.19.25 → 0.19.27
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 +1 -1
- package/bin/goodvibes +5 -0
- package/bin/goodvibes-daemon +5 -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 +1 -0
- package/src/cli/entrypoint.ts +16 -2
- package/src/cli/help.ts +180 -1
- package/src/cli/index.ts +3 -0
- package/src/cli/management-commands.ts +53 -203
- package/src/cli/management.ts +59 -33
- package/src/cli/network-posture.ts +46 -0
- package/src/cli/package-verification.ts +119 -0
- package/src/cli/parser.ts +2 -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 +289 -19
- package/src/cli/surface-command.ts +248 -0
- package/src/cli/types.ts +6 -0
- package/src/cli-flags.ts +1 -0
- package/src/version.ts +1 -1
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import type { ConfigKey, GoodVibesConfig } from '../config/index.ts';
|
|
4
|
-
import { CONFIG_SCHEMA } from '../config/index.ts';
|
|
5
3
|
import { SecretsManager } from '../config/secrets.ts';
|
|
6
|
-
import { createShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
|
|
7
4
|
import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, resolveSecretRef } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
|
|
8
5
|
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
|
|
9
6
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
|
|
@@ -11,8 +8,9 @@ import { inspectProviderAuth } from '@pellux/goodvibes-sdk/platform/runtime/auth
|
|
|
11
8
|
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing/index';
|
|
12
9
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
|
|
13
10
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
11
|
+
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
14
12
|
import type { CliCommandRuntime } from './management.ts';
|
|
15
|
-
import {
|
|
13
|
+
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
|
|
16
14
|
|
|
17
15
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
18
16
|
return await withRuntimeServices(runtime, async (services) => {
|
|
@@ -305,157 +303,71 @@ export async function handleTasks(runtime: CliCommandRuntime): Promise<string> {
|
|
|
305
303
|
});
|
|
306
304
|
}
|
|
307
305
|
|
|
308
|
-
export
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (webError) return webError;
|
|
322
|
-
const webBinding = resolveRuntimeEndpointBinding(runtime.configManager, 'web');
|
|
323
|
-
if (runtime.cli.flags.hostname !== undefined && webBinding.hostMode === 'local') {
|
|
324
|
-
runtime.configManager.setDynamic('controlPlane.hostMode', 'local');
|
|
325
|
-
runtime.configManager.setDynamic('controlPlane.host', '127.0.0.1');
|
|
326
|
-
runtime.configManager.setDynamic('controlPlane.allowRemote', false);
|
|
327
|
-
} else {
|
|
328
|
-
enableEndpointLanDefault(runtime.configManager, 'controlPlane');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
else if (target === 'listener' || target === 'http-listener') {
|
|
333
|
-
runtime.configManager.setDynamic('danger.httpListener', enabled);
|
|
334
|
-
if (enabled) {
|
|
335
|
-
const listenerError = applyTargetEndpointFlagsOrDefault(runtime, 'httpListener');
|
|
336
|
-
if (listenerError) return listenerError;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
else if (target === 'control-plane' || target === 'controlPlane') {
|
|
340
|
-
runtime.configManager.setDynamic('controlPlane.enabled', enabled);
|
|
341
|
-
runtime.configManager.setDynamic('danger.daemon', enabled);
|
|
342
|
-
if (enabled) {
|
|
343
|
-
const controlPlaneError = applyTargetEndpointFlagsOrDefault(runtime, 'controlPlane');
|
|
344
|
-
if (controlPlaneError) return controlPlaneError;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
else if (SURFACE_CONFIGS.some(([id]) => id === target)) {
|
|
348
|
-
runtime.configManager.setDynamic(`surfaces.${target}.enabled` as ConfigKey, enabled);
|
|
349
|
-
if (enabled) {
|
|
350
|
-
runtime.configManager.setDynamic('danger.httpListener', true);
|
|
351
|
-
enableEndpointLanDefault(runtime.configManager, 'httpListener');
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
else return `Unknown surface: ${target}`;
|
|
355
|
-
if (enabled) {
|
|
356
|
-
enableServicePosture(runtime.configManager);
|
|
357
|
-
}
|
|
358
|
-
return `Surface ${enabled ? 'enabled' : 'disabled'}: ${target}`;
|
|
359
|
-
}
|
|
360
|
-
if (sub !== 'list' && sub !== 'status' && sub !== 'check' && sub !== 'show') {
|
|
361
|
-
return 'Usage: goodvibes surfaces [list|check|show <surfaceId>|enable <surfaceId>|disable <surfaceId>]';
|
|
362
|
-
}
|
|
363
|
-
const controlPlane = resolveRuntimeEndpointBinding(config, 'controlPlane');
|
|
364
|
-
const web = resolveRuntimeEndpointBinding(config, 'web');
|
|
365
|
-
const httpListener = resolveRuntimeEndpointBinding(config, 'httpListener');
|
|
366
|
-
const includeProbe = sub === 'check';
|
|
367
|
-
const [controlPlaneReachable, webReachable, listenerReachable] = includeProbe
|
|
368
|
-
? await Promise.all([
|
|
369
|
-
probeTcp(controlPlane.host, controlPlane.port),
|
|
370
|
-
probeTcp(web.host, web.port),
|
|
371
|
-
probeTcp(httpListener.host, httpListener.port),
|
|
372
|
-
])
|
|
373
|
-
: [undefined, undefined, undefined];
|
|
374
|
-
const externalSurfaces = SURFACE_CONFIGS.map(([id, label, requiredKeys]) => {
|
|
375
|
-
const enabled = config.get(`surfaces.${id}.enabled` as ConfigKey);
|
|
376
|
-
const missing = requiredKeys.filter((key) => !isPresentConfigValue(config.get(key as ConfigKey)));
|
|
377
|
-
return {
|
|
378
|
-
id,
|
|
379
|
-
label,
|
|
380
|
-
enabled,
|
|
381
|
-
ready: !enabled || missing.length === 0,
|
|
382
|
-
missing,
|
|
383
|
-
};
|
|
384
|
-
});
|
|
385
|
-
const filteredSurfaces = target ? externalSurfaces.filter((surface) => surface.id === target) : externalSurfaces;
|
|
386
|
-
if (target && filteredSurfaces.length === 0) return `Unknown surface: ${target}`;
|
|
387
|
-
const value = {
|
|
388
|
-
controlPlane: {
|
|
389
|
-
enabled: config.get('controlPlane.enabled'),
|
|
390
|
-
hostMode: controlPlane.hostMode,
|
|
391
|
-
configuredHost: controlPlane.configuredHost,
|
|
392
|
-
host: controlPlane.host,
|
|
393
|
-
port: controlPlane.port,
|
|
394
|
-
reachable: controlPlaneReachable,
|
|
395
|
-
},
|
|
396
|
-
web: {
|
|
397
|
-
enabled: config.get('web.enabled'),
|
|
398
|
-
hostMode: web.hostMode,
|
|
399
|
-
configuredHost: web.configuredHost,
|
|
400
|
-
host: web.host,
|
|
401
|
-
port: web.port,
|
|
402
|
-
reachable: webReachable,
|
|
403
|
-
},
|
|
404
|
-
httpListener: {
|
|
405
|
-
enabled: config.get('danger.httpListener'),
|
|
406
|
-
hostMode: httpListener.hostMode,
|
|
407
|
-
configuredHost: httpListener.configuredHost,
|
|
408
|
-
host: httpListener.host,
|
|
409
|
-
port: httpListener.port,
|
|
410
|
-
reachable: listenerReachable,
|
|
411
|
-
},
|
|
412
|
-
surfaces: filteredSurfaces,
|
|
306
|
+
export interface ControlPlaneStatusResult {
|
|
307
|
+
readonly enabled: unknown;
|
|
308
|
+
readonly hostMode: string;
|
|
309
|
+
readonly configuredHost: string;
|
|
310
|
+
readonly host: string;
|
|
311
|
+
readonly port: number;
|
|
312
|
+
readonly posture: ReturnType<typeof classifyBindPosture>;
|
|
313
|
+
readonly reachable: boolean;
|
|
314
|
+
readonly auth: ReturnType<typeof readAuthPaths>;
|
|
315
|
+
readonly service: {
|
|
316
|
+
readonly enabled: unknown;
|
|
317
|
+
readonly autostart: unknown;
|
|
318
|
+
readonly restartOnFailure: unknown;
|
|
413
319
|
};
|
|
414
|
-
|
|
415
|
-
'GoodVibes surfaces',
|
|
416
|
-
` control-plane: ${yesNo(value.controlPlane.enabled)} (${value.controlPlane.hostMode} ${value.controlPlane.host}:${value.controlPlane.port})${includeProbe ? ` reachable=${yesNo(value.controlPlane.reachable)}` : ''}`,
|
|
417
|
-
` web: ${yesNo(value.web.enabled)} (${value.web.hostMode} ${value.web.host}:${value.web.port})${includeProbe ? ` reachable=${yesNo(value.web.reachable)}` : ''}`,
|
|
418
|
-
` http-listener: ${yesNo(value.httpListener.enabled)} (${value.httpListener.hostMode} ${value.httpListener.host}:${value.httpListener.port})${includeProbe ? ` reachable=${yesNo(value.httpListener.reachable)}` : ''}`,
|
|
419
|
-
'',
|
|
420
|
-
'External surfaces:',
|
|
421
|
-
...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}`),
|
|
422
|
-
].join('\n'));
|
|
320
|
+
readonly issues: readonly string[];
|
|
423
321
|
}
|
|
424
322
|
|
|
425
|
-
export async function
|
|
426
|
-
const enabled = runtime.configManager.get('danger.httpListener');
|
|
427
|
-
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'httpListener');
|
|
428
|
-
const reachable = await probeTcp(binding.host, binding.port);
|
|
429
|
-
const value = { enabled, ...binding, reachable };
|
|
430
|
-
return formatJsonOrText(runtime.cli)(value, [
|
|
431
|
-
'GoodVibes listener test',
|
|
432
|
-
` enabled: ${yesNo(enabled)}`,
|
|
433
|
-
` endpoint: ${binding.hostMode} ${binding.host}:${binding.port}`,
|
|
434
|
-
` reachable: ${yesNo(reachable)}`,
|
|
435
|
-
].join('\n'));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
export async function renderControlPlaneStatus(runtime: CliCommandRuntime): Promise<string> {
|
|
323
|
+
export async function buildControlPlaneStatusResult(runtime: CliCommandRuntime): Promise<ControlPlaneStatusResult> {
|
|
439
324
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'controlPlane');
|
|
440
|
-
const
|
|
325
|
+
const enabled = runtime.configManager.get('controlPlane.enabled');
|
|
326
|
+
const reachable = enabled === true ? await probeTcp(binding.host, binding.port) : false;
|
|
441
327
|
const auth = readAuthPaths(runtime);
|
|
442
|
-
const
|
|
443
|
-
enabled: runtime.configManager.get('
|
|
328
|
+
const service = {
|
|
329
|
+
enabled: runtime.configManager.get('service.enabled'),
|
|
330
|
+
autostart: runtime.configManager.get('service.autostart'),
|
|
331
|
+
restartOnFailure: runtime.configManager.get('service.restartOnFailure'),
|
|
332
|
+
};
|
|
333
|
+
const issues: string[] = [];
|
|
334
|
+
if (enabled === true && !reachable) issues.push(`Control plane is enabled but not reachable on ${binding.host}:${binding.port}.`);
|
|
335
|
+
if (enabled === true && service.enabled !== true) issues.push('Control plane is enabled but service mode is off.');
|
|
336
|
+
if (enabled === true && service.autostart !== true) issues.push('Control plane is enabled but service autostart is off.');
|
|
337
|
+
if (enabled === true && service.restartOnFailure !== true) issues.push('Control plane is enabled but service restart-on-failure is off.');
|
|
338
|
+
if (isNetworkFacing(enabled, binding) && !auth.userStorePresent) issues.push('Network-facing control plane has no local auth user store.');
|
|
339
|
+
if (isNetworkFacing(enabled, binding) && auth.bootstrapCredentialPresent) issues.push('Network-facing control plane still has a bootstrap credential file.');
|
|
340
|
+
return {
|
|
341
|
+
enabled,
|
|
444
342
|
...binding,
|
|
343
|
+
posture: classifyBindPosture(binding),
|
|
445
344
|
reachable,
|
|
446
345
|
auth,
|
|
346
|
+
service,
|
|
347
|
+
issues,
|
|
447
348
|
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function formatControlPlaneStatus(runtime: CliCommandRuntime, value: ControlPlaneStatusResult): string {
|
|
448
352
|
return formatJsonOrText(runtime.cli)(value, [
|
|
449
353
|
'GoodVibes control-plane status',
|
|
450
354
|
` enabled: ${yesNo(value.enabled)}`,
|
|
451
|
-
` bind: ${
|
|
452
|
-
`
|
|
453
|
-
`
|
|
454
|
-
`
|
|
455
|
-
`
|
|
355
|
+
` bind: ${value.hostMode} ${value.host}:${value.port}`,
|
|
356
|
+
` bind posture: ${value.posture.label}`,
|
|
357
|
+
` reachable: ${yesNo(value.reachable)}`,
|
|
358
|
+
` service: enabled=${yesNo(value.service.enabled)} autostart=${yesNo(value.service.autostart)} restartOnFailure=${yesNo(value.service.restartOnFailure)}`,
|
|
359
|
+
` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
|
|
360
|
+
` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
|
|
361
|
+
` operator tokens: ${value.auth.operatorTokenPresent ? 'present' : 'missing'}`,
|
|
362
|
+
value.issues.length === 0 ? ' readiness: ready' : ' readiness: needs attention',
|
|
363
|
+
...value.issues.map((issue) => ` - ${issue}`),
|
|
456
364
|
].join('\n'));
|
|
457
365
|
}
|
|
458
366
|
|
|
367
|
+
export async function renderControlPlaneStatus(runtime: CliCommandRuntime): Promise<string> {
|
|
368
|
+
return formatControlPlaneStatus(runtime, await buildControlPlaneStatusResult(runtime));
|
|
369
|
+
}
|
|
370
|
+
|
|
459
371
|
export async function renderPairing(runtime: CliCommandRuntime): Promise<string> {
|
|
460
372
|
const daemonHomeDir = join(runtime.homeDirectory, '.goodvibes', 'daemon');
|
|
461
373
|
const tokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
@@ -471,68 +383,6 @@ export async function renderPairing(runtime: CliCommandRuntime): Promise<string>
|
|
|
471
383
|
return [formatConnectionBlock(info, payload), '', qr].join('\n');
|
|
472
384
|
}
|
|
473
385
|
|
|
474
|
-
export async function handleBundle(runtime: CliCommandRuntime): Promise<string> {
|
|
475
|
-
const [sub = 'inspect', ...rest] = runtime.cli.commandArgs;
|
|
476
|
-
const shellPaths = createShellPathService({
|
|
477
|
-
workingDirectory: runtime.workingDirectory,
|
|
478
|
-
homeDirectory: runtime.homeDirectory,
|
|
479
|
-
});
|
|
480
|
-
if (sub === 'inspect') {
|
|
481
|
-
const path = rest[0];
|
|
482
|
-
if (!path) return 'Usage: goodvibes bundle inspect <path>';
|
|
483
|
-
const sourcePath = shellPaths.resolveWorkspacePath(path);
|
|
484
|
-
const parsed = JSON.parse(readFileSync(sourcePath, 'utf-8')) as Record<string, unknown>;
|
|
485
|
-
return [
|
|
486
|
-
'GoodVibes bundle',
|
|
487
|
-
` type: ${String(parsed['type'] ?? 'unknown')}`,
|
|
488
|
-
` version: ${String(parsed['version'] ?? 'unknown')}`,
|
|
489
|
-
` path: ${sourcePath}`,
|
|
490
|
-
` capturedAt: ${parsed['capturedAt'] ? new Date(Number(parsed['capturedAt'])).toISOString() : 'n/a'}`,
|
|
491
|
-
` configKeys: ${parsed['config'] && typeof parsed['config'] === 'object' ? CONFIG_SCHEMA.filter((setting) => getNestedValue(parsed['config'], setting.key) !== undefined).length : 0}`,
|
|
492
|
-
].join('\n');
|
|
493
|
-
}
|
|
494
|
-
if (sub === 'export') {
|
|
495
|
-
const outputPath = rest[0] ?? 'goodvibes-bundle.json';
|
|
496
|
-
const secrets = new SecretsManager({
|
|
497
|
-
projectRoot: runtime.workingDirectory,
|
|
498
|
-
globalHome: runtime.homeDirectory,
|
|
499
|
-
configManager: runtime.configManager,
|
|
500
|
-
});
|
|
501
|
-
const bundle = {
|
|
502
|
-
version: 1,
|
|
503
|
-
type: 'goodvibes.setup',
|
|
504
|
-
capturedAt: Date.now(),
|
|
505
|
-
workingDirectory: runtime.workingDirectory,
|
|
506
|
-
config: runtime.configManager.getRaw(),
|
|
507
|
-
secrets: await secrets.inspect(),
|
|
508
|
-
onboarding: {
|
|
509
|
-
projectMarker: existsSync(shellPaths.resolveProjectPath('tui', 'onboarding.json')),
|
|
510
|
-
userMarker: existsSync(shellPaths.resolveUserPath('tui', 'onboarding.json')),
|
|
511
|
-
},
|
|
512
|
-
};
|
|
513
|
-
const targetPath = shellPaths.resolveWorkspacePath(outputPath);
|
|
514
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
515
|
-
writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
|
|
516
|
-
return `Bundle exported: ${targetPath}`;
|
|
517
|
-
}
|
|
518
|
-
if (sub === 'import') {
|
|
519
|
-
const path = rest[0];
|
|
520
|
-
if (!path) return 'Usage: goodvibes bundle import <path>';
|
|
521
|
-
const sourcePath = shellPaths.resolveWorkspacePath(path);
|
|
522
|
-
const parsed = JSON.parse(readFileSync(sourcePath, 'utf-8')) as { config?: GoodVibesConfig };
|
|
523
|
-
if (!parsed.config || typeof parsed.config !== 'object') return 'Bundle has no config object to import.';
|
|
524
|
-
let count = 0;
|
|
525
|
-
for (const setting of CONFIG_SCHEMA) {
|
|
526
|
-
const value = getNestedValue(parsed.config, setting.key);
|
|
527
|
-
if (value === undefined) continue;
|
|
528
|
-
runtime.configManager.setDynamic(setting.key, value);
|
|
529
|
-
count++;
|
|
530
|
-
}
|
|
531
|
-
return `Bundle imported: ${count} config value${count === 1 ? '' : 's'} applied.`;
|
|
532
|
-
}
|
|
533
|
-
return 'Usage: goodvibes bundle export [path]|inspect <path>|import <path>';
|
|
534
|
-
}
|
|
535
|
-
|
|
536
386
|
export async function renderRemote(runtime: CliCommandRuntime, label: 'remote' | 'bridge'): Promise<string> {
|
|
537
387
|
return await withRuntimeServices(runtime, (services) => {
|
|
538
388
|
const pools = services.remoteRunnerRegistry.listPools?.() ?? [];
|
package/src/cli/management.ts
CHANGED
|
@@ -21,10 +21,14 @@ import { inspectProviderAuth } from '@pellux/goodvibes-sdk/platform/runtime/auth
|
|
|
21
21
|
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing/index';
|
|
22
22
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
|
|
23
23
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
24
|
+
import { classifyProviderSetup } from './provider-classification.ts';
|
|
24
25
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
25
26
|
import { applyRuntimeEndpointFlagOverrides } from './config-overrides.ts';
|
|
26
27
|
import type { RuntimeEndpointId } from './endpoints.ts';
|
|
27
|
-
import {
|
|
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';
|
|
28
32
|
|
|
29
33
|
export interface CliCommandRuntime {
|
|
30
34
|
readonly cli: GoodVibesCliParseResult;
|
|
@@ -40,23 +44,6 @@ interface CliCommandResult {
|
|
|
40
44
|
|
|
41
45
|
type Formatter = (value: unknown, text: string) => string;
|
|
42
46
|
|
|
43
|
-
export const SURFACE_CONFIGS = [
|
|
44
|
-
['slack', 'Slack', ['surfaces.slack.signingSecret', 'surfaces.slack.botToken']],
|
|
45
|
-
['discord', 'Discord', ['surfaces.discord.publicKey', 'surfaces.discord.botToken', 'surfaces.discord.applicationId']],
|
|
46
|
-
['telegram', 'Telegram', ['surfaces.telegram.botToken']],
|
|
47
|
-
['webhook', 'Webhook', ['surfaces.webhook.secret']],
|
|
48
|
-
['ntfy', 'ntfy', ['surfaces.ntfy.baseUrl', 'surfaces.ntfy.topic']],
|
|
49
|
-
['googleChat', 'Google Chat', ['surfaces.googleChat.webhookUrl']],
|
|
50
|
-
['signal', 'Signal', ['surfaces.signal.bridgeUrl', 'surfaces.signal.account']],
|
|
51
|
-
['whatsapp', 'WhatsApp', ['surfaces.whatsapp.accessToken', 'surfaces.whatsapp.phoneNumberId']],
|
|
52
|
-
['imessage', 'iMessage', ['surfaces.imessage.bridgeUrl', 'surfaces.imessage.account']],
|
|
53
|
-
['msteams', 'Microsoft Teams', ['surfaces.msteams.appId', 'surfaces.msteams.appPassword']],
|
|
54
|
-
['bluebubbles', 'BlueBubbles', ['surfaces.bluebubbles.serverUrl', 'surfaces.bluebubbles.password']],
|
|
55
|
-
['mattermost', 'Mattermost', ['surfaces.mattermost.baseUrl', 'surfaces.mattermost.botToken']],
|
|
56
|
-
['matrix', 'Matrix', ['surfaces.matrix.homeserverUrl', 'surfaces.matrix.accessToken', 'surfaces.matrix.userId']],
|
|
57
|
-
] as const;
|
|
58
|
-
|
|
59
|
-
|
|
60
47
|
export function yesNo(value: unknown): string {
|
|
61
48
|
return value === true ? 'yes' : 'no';
|
|
62
49
|
}
|
|
@@ -424,9 +411,16 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
424
411
|
if (!provider) return 'Usage: goodvibes providers inspect <provider>';
|
|
425
412
|
const snapshot = snapshots.find((candidate) => candidate.providerId === provider);
|
|
426
413
|
if (!snapshot) return `No provider found: ${provider}`;
|
|
427
|
-
|
|
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 }, [
|
|
428
421
|
`Provider ${snapshot.providerId}`,
|
|
429
422
|
` active: ${yesNo(snapshot.active)}`,
|
|
423
|
+
` setup: ${setup.setupLabel}`,
|
|
430
424
|
` configured: ${yesNo(snapshot.runtime.auth?.configured ?? true)}`,
|
|
431
425
|
` via: ${snapshot.runtime.auth?.mode ?? 'unknown'}`,
|
|
432
426
|
` models: ${snapshot.modelCount}`,
|
|
@@ -435,6 +429,12 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
435
429
|
}
|
|
436
430
|
if (sub !== 'list') return 'Usage: goodvibes providers [list|current|inspect <provider>|use <provider> [modelRegistryKey]]';
|
|
437
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
438
|
provider: snapshot.providerId,
|
|
439
439
|
active: snapshot.active,
|
|
440
440
|
configured: snapshot.runtime.auth?.configured ?? true,
|
|
@@ -446,7 +446,7 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
446
446
|
return formatJsonOrText(runtime.cli)(value, [
|
|
447
447
|
'GoodVibes providers',
|
|
448
448
|
...value.map((provider) =>
|
|
449
|
-
` ${provider.current ? '*' : ' '} ${provider.provider.padEnd(18)} configured=${yesNo(provider.configured)} via=${provider.configuredVia ?? 'n/a'} models=${provider.models} ${provider.detail ?? ''}`.trimEnd(),
|
|
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
450
|
),
|
|
451
451
|
].join('\n'));
|
|
452
452
|
});
|
|
@@ -456,19 +456,35 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
456
456
|
return await withRuntimeServices(runtime, async (services) => {
|
|
457
457
|
const [subOrFilter, ...rest] = runtime.cli.commandArgs;
|
|
458
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
|
+
};
|
|
459
469
|
if (subOrFilter === 'current') {
|
|
460
470
|
const model = services.providerRegistry.getCurrentModel();
|
|
471
|
+
const setup = classifyModelProvider(model.provider);
|
|
472
|
+
const providerSnapshot = providerSnapshots.find((candidate) => candidate.providerId === model.provider);
|
|
461
473
|
const value = {
|
|
462
474
|
registryKey: model.registryKey,
|
|
463
475
|
provider: model.provider,
|
|
464
476
|
id: model.id,
|
|
465
477
|
displayName: model.displayName,
|
|
466
478
|
contextWindow: services.providerRegistry.getContextWindowForModel(model),
|
|
479
|
+
providerConfigured: providerSnapshot?.runtime.auth?.configured ?? true,
|
|
480
|
+
setup,
|
|
467
481
|
};
|
|
468
482
|
return formatJsonOrText(runtime.cli)(value, [
|
|
469
483
|
'GoodVibes current model',
|
|
470
484
|
` model: ${model.registryKey}`,
|
|
471
485
|
` provider: ${model.provider}`,
|
|
486
|
+
` setup: ${setup.setupLabel}`,
|
|
487
|
+
` provider configured: ${yesNo(value.providerConfigured)}`,
|
|
472
488
|
` context: ${value.contextWindow.toLocaleString()}`,
|
|
473
489
|
].join('\n'));
|
|
474
490
|
}
|
|
@@ -519,6 +535,7 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
519
535
|
const value = models.map((model) => ({
|
|
520
536
|
registryKey: model.registryKey,
|
|
521
537
|
provider: model.provider,
|
|
538
|
+
...classifyModelProvider(model.provider),
|
|
522
539
|
id: model.id,
|
|
523
540
|
displayName: model.displayName,
|
|
524
541
|
contextWindow: services.providerRegistry.getContextWindowForModel(model),
|
|
@@ -526,7 +543,7 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
526
543
|
}));
|
|
527
544
|
return formatJsonOrText(runtime.cli)(value, [
|
|
528
545
|
`GoodVibes models${filter ? ` (${filter})` : ''}`,
|
|
529
|
-
...value.map((model) => ` ${model.current ? '*' : ' '} ${model.registryKey.padEnd(42)} ctx=${model.contextWindow.toLocaleString()} ${model.displayName}`),
|
|
546
|
+
...value.map((model) => ` ${model.current ? '*' : ' '} ${model.registryKey.padEnd(42)} setup=${model.setupClass} ctx=${model.contextWindow.toLocaleString()} ${model.displayName}`),
|
|
530
547
|
].join('\n'));
|
|
531
548
|
});
|
|
532
549
|
}
|
|
@@ -622,6 +639,11 @@ export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Pro
|
|
|
622
639
|
case 'web':
|
|
623
640
|
console.log(renderWeb(runtime));
|
|
624
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
|
+
}
|
|
625
647
|
case 'providers': {
|
|
626
648
|
const output = await renderProviders(runtime);
|
|
627
649
|
console.log(output);
|
|
@@ -659,23 +681,27 @@ export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Pro
|
|
|
659
681
|
return { handled: true, exitCode: exitCodeForText(output) };
|
|
660
682
|
}
|
|
661
683
|
case 'surfaces': {
|
|
662
|
-
const
|
|
663
|
-
console.log(output);
|
|
664
|
-
return { handled: true, exitCode:
|
|
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 };
|
|
665
697
|
}
|
|
666
|
-
case 'listener':
|
|
667
|
-
console.log(await renderListenerTest(runtime));
|
|
668
|
-
return { handled: true, exitCode: 0 };
|
|
669
|
-
case 'control-plane':
|
|
670
|
-
console.log(await renderControlPlaneStatus(runtime));
|
|
671
|
-
return { handled: true, exitCode: 0 };
|
|
672
698
|
case 'pair':
|
|
673
699
|
console.log(await renderPairing(runtime));
|
|
674
700
|
return { handled: true, exitCode: 0 };
|
|
675
701
|
case 'bundle': {
|
|
676
|
-
const
|
|
677
|
-
console.log(output);
|
|
678
|
-
return { handled: true, exitCode:
|
|
702
|
+
const result = await handleBundleCommand(runtime);
|
|
703
|
+
console.log(result.output);
|
|
704
|
+
return { handled: true, exitCode: result.exitCode };
|
|
679
705
|
}
|
|
680
706
|
case 'remote':
|
|
681
707
|
console.log(await renderRemote(runtime, 'remote'));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RuntimeEndpointBinding } from './endpoints.ts';
|
|
2
|
+
|
|
3
|
+
export type BindPostureKind = 'local' | 'local-network' | 'custom-network';
|
|
4
|
+
|
|
5
|
+
export interface BindPosture {
|
|
6
|
+
readonly kind: BindPostureKind;
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly networkFacing: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isLoopbackHost(host: string): boolean {
|
|
12
|
+
const normalized = host.trim().toLowerCase();
|
|
13
|
+
return normalized === 'localhost'
|
|
14
|
+
|| normalized === '::1'
|
|
15
|
+
|| normalized === '0:0:0:0:0:0:0:1'
|
|
16
|
+
|| normalized.startsWith('127.');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function classifyBindPosture(binding: Pick<RuntimeEndpointBinding, 'hostMode' | 'host'>): BindPosture {
|
|
20
|
+
if (binding.hostMode === 'local' || isLoopbackHost(binding.host)) {
|
|
21
|
+
return {
|
|
22
|
+
kind: 'local',
|
|
23
|
+
label: 'Local only',
|
|
24
|
+
networkFacing: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (binding.hostMode === 'network' || binding.host === '0.0.0.0' || binding.host === '::') {
|
|
28
|
+
return {
|
|
29
|
+
kind: 'local-network',
|
|
30
|
+
label: 'Local Network',
|
|
31
|
+
networkFacing: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
kind: 'custom-network',
|
|
36
|
+
label: 'Custom network',
|
|
37
|
+
networkFacing: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isNetworkFacing(
|
|
42
|
+
enabled: unknown,
|
|
43
|
+
binding: Pick<RuntimeEndpointBinding, 'hostMode' | 'host'>,
|
|
44
|
+
): boolean {
|
|
45
|
+
return enabled === true && classifyBindPosture(binding).networkFacing;
|
|
46
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export interface PackageCliBinVerification {
|
|
6
|
+
readonly command: 'goodvibes' | 'goodvibes-daemon';
|
|
7
|
+
readonly target: string;
|
|
8
|
+
readonly exists: boolean;
|
|
9
|
+
readonly executable: boolean;
|
|
10
|
+
readonly hasLocalPlatformBuildFallback: boolean;
|
|
11
|
+
readonly hasLocalBuildFallback: boolean;
|
|
12
|
+
readonly hasVendoredBinaryFallback: boolean;
|
|
13
|
+
readonly hasSourceFallback: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PackageCliVerificationReport {
|
|
17
|
+
readonly packageName: string;
|
|
18
|
+
readonly version: string;
|
|
19
|
+
readonly bins: readonly PackageCliBinVerification[];
|
|
20
|
+
readonly tarball: {
|
|
21
|
+
readonly entryCount: number;
|
|
22
|
+
readonly unpackedSize: number;
|
|
23
|
+
readonly requiredPathsPresent: readonly string[];
|
|
24
|
+
readonly forbiddenPaths: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
readonly issues: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const REQUIRED_BIN_COMMANDS = ['goodvibes', 'goodvibes-daemon'] as const;
|
|
30
|
+
const REQUIRED_TARBALL_PATHS = [
|
|
31
|
+
'README.md',
|
|
32
|
+
'CHANGELOG.md',
|
|
33
|
+
'package.json',
|
|
34
|
+
'src/main.ts',
|
|
35
|
+
'src/daemon/cli.ts',
|
|
36
|
+
'bin/goodvibes',
|
|
37
|
+
'bin/goodvibes-daemon',
|
|
38
|
+
'scripts/postinstall.js',
|
|
39
|
+
'.goodvibes/GOODVIBES.md',
|
|
40
|
+
] as const;
|
|
41
|
+
const FORBIDDEN_TARBALL_PREFIXES = ['.github/', 'src/test/', 'src/.test/', '.goodvibes/memory/', 'vendor/'] as const;
|
|
42
|
+
|
|
43
|
+
function readPackageJson(root: string): Record<string, unknown> {
|
|
44
|
+
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')) as Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasExecutableBit(path: string): boolean {
|
|
48
|
+
return existsSync(path) && (statSync(path).mode & 0o111) !== 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function verifyBin(root: string, command: typeof REQUIRED_BIN_COMMANDS[number], target: string | undefined): PackageCliBinVerification {
|
|
52
|
+
const binPath = target ? join(root, target) : '';
|
|
53
|
+
const source = target && existsSync(binPath) ? readFileSync(binPath, 'utf-8') : '';
|
|
54
|
+
const expectedLocalBuild = command === 'goodvibes' ? "dist', 'goodvibes'" : "dist', 'goodvibes-daemon'";
|
|
55
|
+
const expectedSource = command === 'goodvibes' ? "src', 'main.ts'" : "src', 'daemon', 'cli.ts'";
|
|
56
|
+
return {
|
|
57
|
+
command,
|
|
58
|
+
target: target ?? '',
|
|
59
|
+
exists: Boolean(target) && existsSync(binPath),
|
|
60
|
+
executable: Boolean(target) && hasExecutableBit(binPath),
|
|
61
|
+
hasLocalPlatformBuildFallback: source.includes("dist', artifactName"),
|
|
62
|
+
hasLocalBuildFallback: source.includes(expectedLocalBuild),
|
|
63
|
+
hasVendoredBinaryFallback: source.includes('vendor'),
|
|
64
|
+
hasSourceFallback: source.includes(expectedSource) && source.includes("'bun'"),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function npmPackDryRun(root: string): { readonly files: readonly string[]; readonly entryCount: number; readonly unpackedSize: number } {
|
|
69
|
+
const raw = execSync('npm pack --json --dry-run', {
|
|
70
|
+
cwd: root,
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
73
|
+
});
|
|
74
|
+
const [packResult] = JSON.parse(raw) as Array<{ files?: Array<{ path?: string }>; entryCount?: number; unpackedSize?: number }>;
|
|
75
|
+
return {
|
|
76
|
+
files: Array.isArray(packResult?.files) ? packResult.files.map((entry) => String(entry.path ?? '')) : [],
|
|
77
|
+
entryCount: Number(packResult?.entryCount ?? 0),
|
|
78
|
+
unpackedSize: Number(packResult?.unpackedSize ?? 0),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function verifyPackageCliInstall(root: string): PackageCliVerificationReport {
|
|
83
|
+
const pkg = readPackageJson(root);
|
|
84
|
+
const bin = pkg.bin && typeof pkg.bin === 'object' ? pkg.bin as Record<string, string | undefined> : {};
|
|
85
|
+
const bins = REQUIRED_BIN_COMMANDS.map((command) => verifyBin(root, command, bin[command]));
|
|
86
|
+
const pack = npmPackDryRun(root);
|
|
87
|
+
const requiredPathsPresent = REQUIRED_TARBALL_PATHS.filter((path) => pack.files.includes(path));
|
|
88
|
+
const forbiddenPaths = pack.files.filter((path) => FORBIDDEN_TARBALL_PREFIXES.some((prefix) => path.startsWith(prefix)));
|
|
89
|
+
const issues: string[] = [];
|
|
90
|
+
|
|
91
|
+
for (const item of bins) {
|
|
92
|
+
if (!item.target) issues.push(`package.json bin is missing ${item.command}.`);
|
|
93
|
+
if (!item.exists) issues.push(`bin target does not exist: ${item.command} -> ${item.target}`);
|
|
94
|
+
if (!item.executable) issues.push(`bin target is not executable: ${item.command} -> ${item.target}`);
|
|
95
|
+
if (!item.hasLocalPlatformBuildFallback) issues.push(`bin target lacks local platform dist fallback: ${item.command}`);
|
|
96
|
+
if (!item.hasLocalBuildFallback) issues.push(`bin target lacks local dist fallback: ${item.command}`);
|
|
97
|
+
if (!item.hasVendoredBinaryFallback) issues.push(`bin target lacks vendored binary fallback: ${item.command}`);
|
|
98
|
+
if (!item.hasSourceFallback) issues.push(`bin target lacks Bun source fallback: ${item.command}`);
|
|
99
|
+
}
|
|
100
|
+
for (const path of REQUIRED_TARBALL_PATHS) {
|
|
101
|
+
if (!pack.files.includes(path)) issues.push(`npm tarball missing required path: ${path}`);
|
|
102
|
+
}
|
|
103
|
+
for (const path of forbiddenPaths) {
|
|
104
|
+
issues.push(`npm tarball includes forbidden path: ${path}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
packageName: String(pkg.name ?? ''),
|
|
109
|
+
version: String(pkg.version ?? ''),
|
|
110
|
+
bins,
|
|
111
|
+
tarball: {
|
|
112
|
+
entryCount: pack.entryCount,
|
|
113
|
+
unpackedSize: pack.unpackedSize,
|
|
114
|
+
requiredPathsPresent,
|
|
115
|
+
forbiddenPaths,
|
|
116
|
+
},
|
|
117
|
+
issues,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/cli/parser.ts
CHANGED