@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,426 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { SecretsManager } from '../config/secrets.ts';
|
|
4
|
+
import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, resolveSecretRef } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
|
|
5
|
+
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
|
|
6
|
+
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
|
|
7
|
+
import { inspectProviderAuth } from '@pellux/goodvibes-sdk/platform/runtime/auth/inspection';
|
|
8
|
+
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing/index';
|
|
9
|
+
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
|
|
10
|
+
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
11
|
+
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
13
|
+
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
|
|
14
|
+
|
|
15
|
+
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
16
|
+
return await withRuntimeServices(runtime, async (services) => {
|
|
17
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
18
|
+
const subscriptions = services.subscriptionManager.list();
|
|
19
|
+
const pending = services.subscriptionManager.listPending();
|
|
20
|
+
const available = listAvailableSubscriptionProviders(services.serviceRegistry.getAll());
|
|
21
|
+
if (sub === 'providers') {
|
|
22
|
+
return formatJsonOrText(runtime.cli)(available, [
|
|
23
|
+
'GoodVibes subscription providers',
|
|
24
|
+
...available.map((provider) => ` ${provider.provider} source=${provider.source} redirect=${provider.oauth.redirectUri}`),
|
|
25
|
+
].join('\n'));
|
|
26
|
+
}
|
|
27
|
+
if (sub === 'inspect' || sub === 'show') {
|
|
28
|
+
const provider = rest[0];
|
|
29
|
+
if (!provider) return 'Usage: goodvibes subscription inspect <provider>';
|
|
30
|
+
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
31
|
+
if (!resolved && !services.subscriptionManager.get(provider) && !services.subscriptionManager.getPending(provider)) {
|
|
32
|
+
return `No stored or available subscription provider named ${provider}.`;
|
|
33
|
+
}
|
|
34
|
+
const inspection = await inspectProviderAuth(provider, {
|
|
35
|
+
serviceRegistry: services.serviceRegistry,
|
|
36
|
+
subscriptionManager: services.subscriptionManager,
|
|
37
|
+
secretsManager: services.secretsManager,
|
|
38
|
+
});
|
|
39
|
+
const stored = services.subscriptionManager.get(provider);
|
|
40
|
+
return formatJsonOrText(runtime.cli)({ provider, resolved, inspection, stored }, [
|
|
41
|
+
`GoodVibes subscription ${provider}`,
|
|
42
|
+
` configured: ${yesNo(inspection.configured)}`,
|
|
43
|
+
` freshness: ${inspection.freshness}`,
|
|
44
|
+
` callbackMode: ${inspection.callbackMode}`,
|
|
45
|
+
...(resolved ? [
|
|
46
|
+
` source: ${resolved.source}`,
|
|
47
|
+
` redirectUri: ${resolved.oauth.redirectUri}`,
|
|
48
|
+
] : []),
|
|
49
|
+
...(stored ? [
|
|
50
|
+
` tokenType: ${stored.tokenType}`,
|
|
51
|
+
` expiresAt: ${stored.expiresAt ? new Date(stored.expiresAt).toISOString() : 'n/a'}`,
|
|
52
|
+
` refreshToken: ${stored.refreshToken ? 'present' : 'absent'}`,
|
|
53
|
+
` overrideAmbient: ${yesNo(stored.overrideAmbientApiKeys)}`,
|
|
54
|
+
] : [' stored: no']),
|
|
55
|
+
...inspection.issues.map((issue) => ` issue: ${issue}`),
|
|
56
|
+
...inspection.nextActions.map((action) => ` next: ${action}`),
|
|
57
|
+
].join('\n'));
|
|
58
|
+
}
|
|
59
|
+
if (sub === 'login' || sub === 'start') {
|
|
60
|
+
const provider = sub === 'start' ? rest[0] : rest[0];
|
|
61
|
+
const mode = sub === 'start' ? 'start' : rest[1]?.toLowerCase();
|
|
62
|
+
if (!provider || mode !== 'start') return 'Usage: goodvibes subscription login <provider> start [--open]';
|
|
63
|
+
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
64
|
+
if (!resolved) return `No subscription provider found: ${provider}`;
|
|
65
|
+
if (provider === 'openai' && resolved.source === 'builtin') {
|
|
66
|
+
const started = await beginOpenAICodexLogin();
|
|
67
|
+
services.subscriptionManager.savePending({
|
|
68
|
+
provider,
|
|
69
|
+
state: started.state,
|
|
70
|
+
verifier: started.verifier,
|
|
71
|
+
redirectUri: started.redirectUri,
|
|
72
|
+
createdAt: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
const openResult = runtime.cli.flags.open || hasCommandFlag(rest, '--open') ? openBrowser(started.authorizationUrl) : null;
|
|
75
|
+
return [
|
|
76
|
+
`Subscription OAuth started: ${provider}`,
|
|
77
|
+
` source: ${resolved.source}`,
|
|
78
|
+
` state: ${started.state}`,
|
|
79
|
+
` redirectUri: ${started.redirectUri}`,
|
|
80
|
+
...(openResult ? [` open: ${openResult}`] : []),
|
|
81
|
+
` next: goodvibes subscription login ${provider} finish <code-or-url>`,
|
|
82
|
+
' authorizationUrl:',
|
|
83
|
+
` ${started.authorizationUrl}`,
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
const started = await services.subscriptionManager.beginOAuthLogin(provider, resolved.oauth);
|
|
87
|
+
const openResult = runtime.cli.flags.open || hasCommandFlag(rest, '--open') ? openBrowser(started.authorizationUrl) : null;
|
|
88
|
+
return [
|
|
89
|
+
`Subscription OAuth started: ${provider}`,
|
|
90
|
+
` source: ${resolved.source}`,
|
|
91
|
+
` state: ${started.pending.state}`,
|
|
92
|
+
` redirectUri: ${started.pending.redirectUri}`,
|
|
93
|
+
...(openResult ? [` open: ${openResult}`] : []),
|
|
94
|
+
` next: goodvibes subscription login ${provider} finish <code-or-url>`,
|
|
95
|
+
' authorizationUrl:',
|
|
96
|
+
` ${started.authorizationUrl}`,
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
if (sub === 'finish' || (sub === 'login' && rest[1]?.toLowerCase() === 'finish')) {
|
|
100
|
+
const provider = sub === 'finish' ? rest[0] : rest[0];
|
|
101
|
+
const codeInput = sub === 'finish' ? rest[1] : rest[2];
|
|
102
|
+
if (!provider || !codeInput) return 'Usage: goodvibes subscription login <provider> finish <code-or-url>';
|
|
103
|
+
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
104
|
+
if (!resolved) return `No subscription provider found: ${provider}`;
|
|
105
|
+
const code = extractAuthorizationCode(codeInput);
|
|
106
|
+
if (provider === 'openai' && resolved.source === 'builtin') {
|
|
107
|
+
const pendingLogin = services.subscriptionManager.getPending(provider);
|
|
108
|
+
if (!pendingLogin) return `No pending OAuth login for ${provider}.`;
|
|
109
|
+
const token = await exchangeOpenAICodexCode(code, pendingLogin.verifier);
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const record = services.subscriptionManager.saveSubscription({
|
|
112
|
+
provider,
|
|
113
|
+
accessToken: token.accessToken,
|
|
114
|
+
refreshToken: token.refreshToken,
|
|
115
|
+
tokenType: token.tokenType,
|
|
116
|
+
expiresAt: token.expiresAt,
|
|
117
|
+
...(token.scopes ? { scopes: token.scopes } : {}),
|
|
118
|
+
authMode: 'oauth',
|
|
119
|
+
overrideAmbientApiKeys: false,
|
|
120
|
+
createdAt: services.subscriptionManager.get(provider)?.createdAt ?? now,
|
|
121
|
+
updatedAt: now,
|
|
122
|
+
});
|
|
123
|
+
return `Subscription stored: ${provider} token=${record.tokenType} expires=${record.expiresAt ? new Date(record.expiresAt).toISOString() : 'n/a'}`;
|
|
124
|
+
}
|
|
125
|
+
const record = await services.subscriptionManager.completeOAuthLogin(provider, resolved.oauth, code);
|
|
126
|
+
return `Subscription stored: ${provider} token=${record.tokenType} expires=${record.expiresAt ? new Date(record.expiresAt).toISOString() : 'n/a'}`;
|
|
127
|
+
}
|
|
128
|
+
if (sub === 'refresh') {
|
|
129
|
+
const provider = rest[0];
|
|
130
|
+
if (!provider) return 'Usage: goodvibes subscription refresh <provider>';
|
|
131
|
+
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
132
|
+
if (!resolved) return `No subscription provider found: ${provider}`;
|
|
133
|
+
const record = await services.subscriptionManager.refreshOAuthToken(provider, resolved.oauth);
|
|
134
|
+
return `Subscription refreshed: ${provider} expires=${record.expiresAt ? new Date(record.expiresAt).toISOString() : 'n/a'}`;
|
|
135
|
+
}
|
|
136
|
+
if (sub === 'logout' || sub === 'remove') {
|
|
137
|
+
const provider = rest[0];
|
|
138
|
+
if (!provider) return 'Usage: goodvibes subscription logout <provider>';
|
|
139
|
+
const removed = services.subscriptionManager.logout(provider);
|
|
140
|
+
return removed ? `Subscription removed: ${provider}` : `No stored subscription session existed for ${provider}.`;
|
|
141
|
+
}
|
|
142
|
+
if (sub !== 'list' && sub !== 'status' && sub !== 'review') {
|
|
143
|
+
return 'Usage: goodvibes subscription [list|providers|inspect <provider>|login <provider> start|finish <code-or-url>|refresh <provider>|logout <provider>]';
|
|
144
|
+
}
|
|
145
|
+
const value = {
|
|
146
|
+
subscriptions: subscriptions.map((sub) => ({
|
|
147
|
+
provider: sub.provider,
|
|
148
|
+
tokenType: sub.tokenType,
|
|
149
|
+
expiresAt: sub.expiresAt ?? null,
|
|
150
|
+
overrideAmbientApiKeys: sub.overrideAmbientApiKeys,
|
|
151
|
+
})),
|
|
152
|
+
pending: pending.map((sub) => ({ provider: sub.provider, createdAt: sub.createdAt })),
|
|
153
|
+
};
|
|
154
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
155
|
+
'GoodVibes subscriptions',
|
|
156
|
+
subscriptions.length === 0 ? ' active: none' : ' active:',
|
|
157
|
+
...subscriptions.map((sub) => ` ${sub.provider} token=${sub.tokenType} expires=${sub.expiresAt ? new Date(sub.expiresAt).toISOString() : 'n/a'} overrideAmbient=${yesNo(sub.overrideAmbientApiKeys)}`),
|
|
158
|
+
pending.length === 0 ? ' pending: none' : ' pending:',
|
|
159
|
+
...pending.map((sub) => ` ${sub.provider} created=${new Date(sub.createdAt).toISOString()}`),
|
|
160
|
+
].join('\n'));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function handleSecrets(runtime: CliCommandRuntime): Promise<string> {
|
|
165
|
+
const secrets = new SecretsManager({
|
|
166
|
+
projectRoot: runtime.workingDirectory,
|
|
167
|
+
globalHome: runtime.homeDirectory,
|
|
168
|
+
configManager: runtime.configManager,
|
|
169
|
+
});
|
|
170
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
171
|
+
if (sub === 'providers') {
|
|
172
|
+
const value = { providers: BUILTIN_SECRET_PROVIDER_SOURCES };
|
|
173
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
174
|
+
'GoodVibes secret providers',
|
|
175
|
+
...BUILTIN_SECRET_PROVIDER_SOURCES.map((provider) => ` ${provider}`),
|
|
176
|
+
'',
|
|
177
|
+
'Secret refs use goodvibes://secrets/<source>/... and never embed secret values.',
|
|
178
|
+
].join('\n'));
|
|
179
|
+
}
|
|
180
|
+
if (sub === 'test') {
|
|
181
|
+
const ref = rest.join(' ').trim();
|
|
182
|
+
if (!ref || !ref.startsWith('goodvibes://secrets/') || !isSecretRefInput(ref)) {
|
|
183
|
+
return 'Usage: goodvibes secrets test goodvibes://secrets/<source>/...';
|
|
184
|
+
}
|
|
185
|
+
const resolved = await resolveSecretRef(ref, { resolveLocalSecret: (key) => secrets.get(key) });
|
|
186
|
+
const value = { ref: describeSecretRef(ref), resolved: Boolean(resolved.value) };
|
|
187
|
+
return formatJsonOrText(runtime.cli)(value, `[secrets] ${value.ref}: ${value.resolved ? 'resolved <redacted>' : 'missing'}`);
|
|
188
|
+
}
|
|
189
|
+
if (sub === 'set' || sub === 'link') {
|
|
190
|
+
const flags = new Set(rest.filter((arg) => arg.startsWith('--')));
|
|
191
|
+
const values = rest.filter((arg) => !arg.startsWith('--'));
|
|
192
|
+
const [key, ...rawValueParts] = values;
|
|
193
|
+
const value = rawValueParts.join(' ');
|
|
194
|
+
if (!key || !value) return `Usage: goodvibes secrets ${sub} <KEY> <value> [--user|--project] [--secure|--plaintext]`;
|
|
195
|
+
if (sub === 'link' && (!value.startsWith('goodvibes://secrets/') || !isSecretRefInput(value))) {
|
|
196
|
+
return 'Invalid secret reference. Use goodvibes://secrets/<source>/...';
|
|
197
|
+
}
|
|
198
|
+
await secrets.set(key, value, {
|
|
199
|
+
scope: flags.has('--user') ? 'user' : 'project',
|
|
200
|
+
medium: flags.has('--plaintext') ? 'plaintext' : 'secure',
|
|
201
|
+
});
|
|
202
|
+
return `[secrets] ${sub === 'link' ? 'Linked' : 'Stored'}: ${key}`;
|
|
203
|
+
}
|
|
204
|
+
if (sub === 'delete') {
|
|
205
|
+
const key = rest.find((arg) => !arg.startsWith('--'));
|
|
206
|
+
if (!key) return 'Usage: goodvibes secrets delete <KEY> [--user|--project] [--secure|--plaintext]';
|
|
207
|
+
const flags = new Set(rest.filter((arg) => arg.startsWith('--')));
|
|
208
|
+
await secrets.delete(key, {
|
|
209
|
+
scope: flags.has('--user') ? 'user' : flags.has('--project') ? 'project' : undefined,
|
|
210
|
+
medium: flags.has('--secure') ? 'secure' : flags.has('--plaintext') ? 'plaintext' : undefined,
|
|
211
|
+
});
|
|
212
|
+
return `[secrets] Deleted: ${key}`;
|
|
213
|
+
}
|
|
214
|
+
const [records, review] = await Promise.all([secrets.listDetailed(), secrets.inspect()]);
|
|
215
|
+
const stored = records.filter((record) => record.source !== 'env');
|
|
216
|
+
const value = { policy: review.policy, records: stored, warnings: review.warnings };
|
|
217
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
218
|
+
'GoodVibes secrets',
|
|
219
|
+
` policy: ${review.policy}`,
|
|
220
|
+
` secure available: ${yesNo(review.secureAvailable)}`,
|
|
221
|
+
` stored keys: ${stored.length}`,
|
|
222
|
+
...stored.map((record) => ` ${record.key} (${record.source}${record.refSource ? `, ref:${record.refSource}` : ''}${record.overriddenByEnv ? ', env override' : ''})`),
|
|
223
|
+
...review.warnings.map((warning) => ` warning: ${warning}`),
|
|
224
|
+
].join('\n'));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function handleSessions(runtime: CliCommandRuntime): Promise<string | null> {
|
|
228
|
+
return await withRuntimeServices(runtime, (services) => {
|
|
229
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
230
|
+
const sessions = services.sessionManager.list();
|
|
231
|
+
if (sub === 'list') {
|
|
232
|
+
const value = sessions;
|
|
233
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
234
|
+
`GoodVibes sessions (${sessions.length})`,
|
|
235
|
+
...sessions.slice(0, 50).map((session) => ` ${session.name} messages=${session.messageCount} ${new Date(session.timestamp).toISOString()} ${session.title || '(untitled)'}`),
|
|
236
|
+
].join('\n'));
|
|
237
|
+
}
|
|
238
|
+
if (sub === 'show' || sub === 'info') {
|
|
239
|
+
const target = rest.join(' ').trim();
|
|
240
|
+
if (!target) return 'Usage: goodvibes sessions show <id|name>';
|
|
241
|
+
const found = sessions.find((session) => session.name === target || session.name.startsWith(target) || session.title.toLowerCase() === target.toLowerCase());
|
|
242
|
+
if (!found) return `Session not found: ${target}`;
|
|
243
|
+
return formatJsonOrText(runtime.cli)(found, [
|
|
244
|
+
`Session ${found.name}`,
|
|
245
|
+
` title: ${found.title || '(untitled)'}`,
|
|
246
|
+
` messages: ${found.messageCount}`,
|
|
247
|
+
` provider/model: ${found.provider}/${found.model}`,
|
|
248
|
+
` updated: ${new Date(found.timestamp).toISOString()}`,
|
|
249
|
+
` file: ${found.filePath}`,
|
|
250
|
+
].join('\n'));
|
|
251
|
+
}
|
|
252
|
+
if (sub === 'export') {
|
|
253
|
+
const target = rest[0];
|
|
254
|
+
const outputPath = rest[1];
|
|
255
|
+
if (!target) return 'Usage: goodvibes sessions export <id|name> [path]';
|
|
256
|
+
const found = sessions.find((session) => session.name === target || session.name.startsWith(target) || session.title.toLowerCase() === target.toLowerCase());
|
|
257
|
+
if (!found) return `Session not found: ${target}`;
|
|
258
|
+
const data = services.sessionManager.load(found.name);
|
|
259
|
+
const text = JSON.stringify({ name: found.name, ...data }, null, 2) + '\n';
|
|
260
|
+
if (outputPath) {
|
|
261
|
+
const targetPath = services.shellPaths.resolveWorkspacePath(outputPath);
|
|
262
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
263
|
+
writeFileSync(targetPath, text, 'utf-8');
|
|
264
|
+
return `Session exported: ${targetPath}`;
|
|
265
|
+
}
|
|
266
|
+
return text.trimEnd();
|
|
267
|
+
}
|
|
268
|
+
if (sub === 'resume') {
|
|
269
|
+
const target = rest.join(' ').trim();
|
|
270
|
+
return target ? null : 'Usage: goodvibes sessions resume <id|name>';
|
|
271
|
+
}
|
|
272
|
+
return 'Usage: goodvibes sessions list|show <id>|export <id> [path]|resume <id>';
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function handleTasks(runtime: CliCommandRuntime): Promise<string> {
|
|
277
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
278
|
+
if (sub === 'submit') {
|
|
279
|
+
const prompt = rest.join(' ').trim();
|
|
280
|
+
if (!prompt) return 'Usage: goodvibes tasks submit <prompt>';
|
|
281
|
+
const runCli = {
|
|
282
|
+
...runtime.cli,
|
|
283
|
+
command: 'run' as const,
|
|
284
|
+
flags: { ...runtime.cli.flags, prompt },
|
|
285
|
+
positionals: [prompt],
|
|
286
|
+
};
|
|
287
|
+
const code = await runNonInteractiveAgent({ ...runtime, cli: runCli });
|
|
288
|
+
return code === 0 ? '' : `Task submit failed with exit code ${code}`;
|
|
289
|
+
}
|
|
290
|
+
return await withRuntimeServices(runtime, (services) => {
|
|
291
|
+
const tasks = [...services.runtimeStore.getState().tasks.tasks.values()];
|
|
292
|
+
if (sub === 'list') {
|
|
293
|
+
return tasks.length === 0
|
|
294
|
+
? 'GoodVibes tasks\n No in-process runtime tasks are currently recorded.'
|
|
295
|
+
: ['GoodVibes tasks', ...tasks.map((task) => ` ${task.id} ${task.status} ${task.kind} ${task.title}`)].join('\n');
|
|
296
|
+
}
|
|
297
|
+
if (sub === 'show') {
|
|
298
|
+
if (!rest[0]) return 'Usage: goodvibes tasks show <taskId>';
|
|
299
|
+
const task = tasks.find((candidate) => candidate.id === rest[0]);
|
|
300
|
+
return task ? JSON.stringify(task, null, 2) : `Unknown task: ${rest[0] ?? ''}`;
|
|
301
|
+
}
|
|
302
|
+
return 'Usage: goodvibes tasks list|show <taskId>|submit <prompt>';
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
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;
|
|
319
|
+
};
|
|
320
|
+
readonly issues: readonly string[];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function buildControlPlaneStatusResult(runtime: CliCommandRuntime): Promise<ControlPlaneStatusResult> {
|
|
324
|
+
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'controlPlane');
|
|
325
|
+
const enabled = runtime.configManager.get('controlPlane.enabled');
|
|
326
|
+
const reachable = enabled === true ? await probeTcp(binding.host, binding.port) : false;
|
|
327
|
+
const auth = readAuthPaths(runtime);
|
|
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,
|
|
342
|
+
...binding,
|
|
343
|
+
posture: classifyBindPosture(binding),
|
|
344
|
+
reachable,
|
|
345
|
+
auth,
|
|
346
|
+
service,
|
|
347
|
+
issues,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function formatControlPlaneStatus(runtime: CliCommandRuntime, value: ControlPlaneStatusResult): string {
|
|
352
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
353
|
+
'GoodVibes control-plane status',
|
|
354
|
+
` enabled: ${yesNo(value.enabled)}`,
|
|
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}`),
|
|
364
|
+
].join('\n'));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function renderControlPlaneStatus(runtime: CliCommandRuntime): Promise<string> {
|
|
368
|
+
return formatControlPlaneStatus(runtime, await buildControlPlaneStatusResult(runtime));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function renderPairing(runtime: CliCommandRuntime): Promise<string> {
|
|
372
|
+
const daemonHomeDir = join(runtime.homeDirectory, '.goodvibes', 'daemon');
|
|
373
|
+
const tokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
374
|
+
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'controlPlane');
|
|
375
|
+
const daemonUrl = `http://${urlHostForBindHost(binding.host)}:${binding.port}`;
|
|
376
|
+
const info = buildCompanionConnectionInfo({
|
|
377
|
+
daemonUrl,
|
|
378
|
+
token: tokenRecord.token,
|
|
379
|
+
username: 'admin',
|
|
380
|
+
});
|
|
381
|
+
const payload = encodeConnectionPayload(info);
|
|
382
|
+
const qr = renderQrToString(generateQrMatrix(payload));
|
|
383
|
+
return [formatConnectionBlock(info, payload), '', qr].join('\n');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function renderRemote(runtime: CliCommandRuntime, label: 'remote' | 'bridge'): Promise<string> {
|
|
387
|
+
return await withRuntimeServices(runtime, (services) => {
|
|
388
|
+
const pools = services.remoteRunnerRegistry.listPools?.() ?? [];
|
|
389
|
+
const contracts = services.remoteRunnerRegistry.listContracts();
|
|
390
|
+
const artifacts = services.remoteRunnerRegistry.listArtifacts();
|
|
391
|
+
const value = {
|
|
392
|
+
pools: pools.length,
|
|
393
|
+
contracts: contracts.length,
|
|
394
|
+
artifacts: artifacts.length,
|
|
395
|
+
remoteFetchPrivateHosts: runtime.configManager.get('network.remoteFetch.allowPrivateHosts'),
|
|
396
|
+
};
|
|
397
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
398
|
+
`GoodVibes ${label} status`,
|
|
399
|
+
` runner pools: ${value.pools}`,
|
|
400
|
+
` contracts: ${value.contracts}`,
|
|
401
|
+
` review artifacts: ${value.artifacts}`,
|
|
402
|
+
` private-host remote fetch: ${yesNo(value.remoteFetchPrivateHosts)}`,
|
|
403
|
+
].join('\n'));
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function renderWeb(runtime: CliCommandRuntime): string {
|
|
408
|
+
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'web');
|
|
409
|
+
const publicBaseUrl = String(runtime.configManager.get('web.publicBaseUrl') ?? '');
|
|
410
|
+
const hasEndpointOverride = runtime.cli.flags.hostname !== undefined || runtime.cli.flags.port !== undefined;
|
|
411
|
+
const url = !hasEndpointOverride && publicBaseUrl
|
|
412
|
+
? publicBaseUrl
|
|
413
|
+
: `http://${urlHostForBindHost(binding.host)}:${binding.port}`;
|
|
414
|
+
const value = {
|
|
415
|
+
enabled: runtime.configManager.get('web.enabled'),
|
|
416
|
+
...binding,
|
|
417
|
+
url,
|
|
418
|
+
};
|
|
419
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
420
|
+
'GoodVibes web',
|
|
421
|
+
` enabled: ${yesNo(value.enabled)}`,
|
|
422
|
+
` bind: ${value.hostMode} ${value.host}:${value.port}`,
|
|
423
|
+
` url: ${value.url}`,
|
|
424
|
+
...(runtime.cli.flags.open ? [` open: ${openBrowser(value.url)}`] : []),
|
|
425
|
+
].join('\n'));
|
|
426
|
+
}
|