@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/bin/goodvibes +10 -0
  4. package/bin/goodvibes-daemon +10 -0
  5. package/docs/foundation-artifacts/operator-contract.json +1 -1
  6. package/package.json +3 -2
  7. package/src/cli/bundle-command.ts +225 -0
  8. package/src/cli/completion.ts +90 -0
  9. package/src/cli/config-overrides.ts +159 -0
  10. package/src/cli/endpoints.ts +63 -0
  11. package/src/cli/entrypoint.ts +169 -0
  12. package/src/cli/help.ts +301 -0
  13. package/src/cli/index.ts +11 -0
  14. package/src/cli/management-commands.ts +426 -0
  15. package/src/cli/management.ts +719 -0
  16. package/src/cli/network-posture.ts +46 -0
  17. package/src/cli/package-verification.ts +119 -0
  18. package/src/cli/parser.ts +369 -0
  19. package/src/cli/provider-classification.ts +107 -0
  20. package/src/cli/redaction.ts +105 -0
  21. package/src/cli/service-command.ts +45 -0
  22. package/src/cli/service-posture.ts +247 -0
  23. package/src/cli/status.ts +382 -0
  24. package/src/cli/surface-command.ts +248 -0
  25. package/src/cli/tui-startup.ts +32 -0
  26. package/src/cli/types.ts +69 -0
  27. package/src/cli-flags.ts +18 -55
  28. package/src/config/index.ts +1 -1
  29. package/src/config/secrets.ts +44 -0
  30. package/src/daemon/cli.ts +62 -11
  31. package/src/input/command-registry.ts +3 -0
  32. package/src/input/commands/guidance-runtime.ts +9 -4
  33. package/src/input/commands/local-runtime.ts +21 -7
  34. package/src/input/commands/local-setup.ts +31 -38
  35. package/src/input/commands/onboarding-runtime.ts +14 -0
  36. package/src/input/commands/runtime-services.ts +9 -0
  37. package/src/input/commands.ts +2 -0
  38. package/src/input/feed-context-factory.ts +8 -1
  39. package/src/input/handler-feed.ts +13 -8
  40. package/src/input/handler-interactions.ts +266 -0
  41. package/src/input/handler-modal-stack.ts +23 -3
  42. package/src/input/handler-modal-token-routes.ts +23 -1
  43. package/src/input/handler-onboarding.ts +696 -0
  44. package/src/input/handler-picker-routes.ts +15 -7
  45. package/src/input/handler-ui-state.ts +58 -0
  46. package/src/input/handler.ts +120 -246
  47. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  48. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  49. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  50. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  51. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  52. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  53. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  54. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  55. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  56. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  57. package/src/main.ts +32 -39
  58. package/src/panels/builtin/operations.ts +0 -10
  59. package/src/panels/index.ts +0 -1
  60. package/src/renderer/conversation-overlays.ts +6 -0
  61. package/src/renderer/help-overlay.ts +1 -1
  62. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  63. package/src/runtime/bootstrap-core.ts +1 -0
  64. package/src/runtime/bootstrap.ts +123 -0
  65. package/src/runtime/onboarding/apply.ts +685 -0
  66. package/src/runtime/onboarding/derivation.ts +495 -0
  67. package/src/runtime/onboarding/index.ts +7 -0
  68. package/src/runtime/onboarding/markers.ts +161 -0
  69. package/src/runtime/onboarding/snapshot.ts +400 -0
  70. package/src/runtime/onboarding/state.ts +140 -0
  71. package/src/runtime/onboarding/types.ts +402 -0
  72. package/src/runtime/onboarding/verify.ts +233 -0
  73. package/src/runtime/ui-services.ts +16 -0
  74. package/src/shell/ui-openers.ts +12 -2
  75. package/src/version.ts +1 -1
  76. package/src/panels/welcome-panel.ts +0 -64
@@ -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
+ }
@@ -0,0 +1,369 @@
1
+ import type {
2
+ GoodVibesCliCommand,
3
+ GoodVibesCliFlags,
4
+ GoodVibesCliOutputFormat,
5
+ GoodVibesCliParseResult,
6
+ } from './types.ts';
7
+
8
+ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
9
+ tui: 'tui',
10
+ app: 'tui',
11
+ run: 'run',
12
+ exec: 'run',
13
+ e: 'run',
14
+ serve: 'serve',
15
+ daemon: 'serve',
16
+ server: 'serve',
17
+ web: 'web',
18
+ service: 'service',
19
+ services: 'service',
20
+ status: 'status',
21
+ doctor: 'doctor',
22
+ onboarding: 'onboarding',
23
+ setup: 'onboarding',
24
+ models: 'models',
25
+ model: 'models',
26
+ providers: 'providers',
27
+ provider: 'providers',
28
+ auth: 'auth',
29
+ subscription: 'subscription',
30
+ subscriptions: 'subscription',
31
+ secrets: 'secrets',
32
+ secret: 'secrets',
33
+ sessions: 'sessions',
34
+ session: 'sessions',
35
+ tasks: 'tasks',
36
+ task: 'tasks',
37
+ pair: 'pair',
38
+ qrcode: 'pair',
39
+ qr: 'pair',
40
+ surfaces: 'surfaces',
41
+ surface: 'surfaces',
42
+ listener: 'listener',
43
+ 'http-listener': 'listener',
44
+ webhook: 'listener',
45
+ 'control-plane': 'control-plane',
46
+ controlplane: 'control-plane',
47
+ cp: 'control-plane',
48
+ bundle: 'bundle',
49
+ bundles: 'bundle',
50
+ remote: 'remote',
51
+ bridge: 'bridge',
52
+ completion: 'completion',
53
+ completions: 'completion',
54
+ help: 'help',
55
+ version: 'version',
56
+ };
57
+
58
+ function createDefaultFlags(): GoodVibesCliFlags {
59
+ return {
60
+ provider: undefined,
61
+ model: undefined,
62
+ daemonHome: undefined,
63
+ workingDir: undefined,
64
+ help: false,
65
+ version: false,
66
+ prompt: undefined,
67
+ print: false,
68
+ outputFormat: 'text',
69
+ configOverrides: [],
70
+ enableFeatures: [],
71
+ disableFeatures: [],
72
+ noAltScreen: false,
73
+ port: undefined,
74
+ hostname: undefined,
75
+ open: false,
76
+ continueLast: false,
77
+ resume: undefined,
78
+ session: undefined,
79
+ fork: false,
80
+ rawOutput: false,
81
+ acceptRawOutputRisk: false,
82
+ };
83
+ }
84
+
85
+ function splitOption(token: string): { readonly name: string; readonly value: string | undefined } {
86
+ const index = token.indexOf('=');
87
+ if (index < 0) return { name: token, value: undefined };
88
+ return {
89
+ name: token.slice(0, index),
90
+ value: token.slice(index + 1),
91
+ };
92
+ }
93
+
94
+ function getValue(
95
+ argv: readonly string[],
96
+ index: number,
97
+ inlineValue: string | undefined,
98
+ optionName: string,
99
+ errors: string[],
100
+ ): { readonly value: string | undefined; readonly nextIndex: number } {
101
+ if (inlineValue !== undefined) return { value: inlineValue, nextIndex: index };
102
+ const next = argv[index + 1];
103
+ if (next === undefined || (next.startsWith('-') && next !== '-')) {
104
+ errors.push(`${optionName} requires a value.`);
105
+ return { value: undefined, nextIndex: index };
106
+ }
107
+ return { value: next, nextIndex: index + 1 };
108
+ }
109
+
110
+ function getOptionalValue(
111
+ argv: readonly string[],
112
+ index: number,
113
+ inlineValue: string | undefined,
114
+ ): { readonly value: string | undefined; readonly nextIndex: number } {
115
+ if (inlineValue !== undefined) return { value: inlineValue, nextIndex: index };
116
+ const next = argv[index + 1];
117
+ if (next === undefined || next.startsWith('-')) return { value: undefined, nextIndex: index };
118
+ return { value: next, nextIndex: index + 1 };
119
+ }
120
+
121
+ function parsePort(value: string | undefined, optionName: string, errors: string[]): number | undefined {
122
+ if (value === undefined) return undefined;
123
+ if (!/^\d+$/.test(value)) {
124
+ errors.push(`${optionName} must be a port number from 1 to 65535.`);
125
+ return undefined;
126
+ }
127
+ const port = Number.parseInt(value, 10);
128
+ if (port < 1 || port > 65535) {
129
+ errors.push(`${optionName} must be a port number from 1 to 65535.`);
130
+ return undefined;
131
+ }
132
+ return port;
133
+ }
134
+
135
+ function normalizeOutputFormat(value: string | undefined, errors: string[]): GoodVibesCliOutputFormat {
136
+ if (value === 'text' || value === 'json' || value === 'stream-json') return value;
137
+ errors.push('--output-format must be one of: text, json, stream-json.');
138
+ return 'text';
139
+ }
140
+
141
+ function inferProviderFromModel(model: string, currentProvider: string | undefined): string | undefined {
142
+ if (currentProvider !== undefined) return currentProvider;
143
+ if (model.includes(':')) return model.split(':')[0];
144
+ if (model.includes('/')) return model.split('/')[0];
145
+ return undefined;
146
+ }
147
+
148
+ function withFlag<K extends keyof GoodVibesCliFlags>(
149
+ flags: GoodVibesCliFlags,
150
+ key: K,
151
+ value: GoodVibesCliFlags[K],
152
+ ): GoodVibesCliFlags {
153
+ return { ...flags, [key]: value };
154
+ }
155
+
156
+ function appendFlagArray<K extends 'configOverrides' | 'enableFeatures' | 'disableFeatures'>(
157
+ flags: GoodVibesCliFlags,
158
+ key: K,
159
+ value: string,
160
+ ): GoodVibesCliFlags {
161
+ return {
162
+ ...flags,
163
+ [key]: [...flags[key], value],
164
+ };
165
+ }
166
+
167
+ export function parseGoodVibesCli(
168
+ argv: readonly string[],
169
+ binary = 'goodvibes',
170
+ ): GoodVibesCliParseResult {
171
+ let flags = createDefaultFlags();
172
+ let command: GoodVibesCliCommand = 'tui';
173
+ let rawCommand: string | undefined;
174
+ const commandArgs: string[] = [];
175
+ const positionals: string[] = [];
176
+ const errors: string[] = [];
177
+ let sawCommand = false;
178
+ let passthrough = false;
179
+
180
+ for (let index = 0; index < argv.length; index += 1) {
181
+ const token = argv[index]!;
182
+
183
+ if (passthrough) {
184
+ if (sawCommand) commandArgs.push(token);
185
+ else positionals.push(token);
186
+ continue;
187
+ }
188
+
189
+ if (token === '--') {
190
+ passthrough = true;
191
+ continue;
192
+ }
193
+
194
+ if (!token.startsWith('-') || token === '-') {
195
+ if (!sawCommand) {
196
+ const normalized = COMMAND_ALIASES[token.toLowerCase()];
197
+ if (normalized) {
198
+ command = normalized;
199
+ rawCommand = token;
200
+ sawCommand = true;
201
+ continue;
202
+ }
203
+ }
204
+ positionals.push(token);
205
+ if (sawCommand) commandArgs.push(token);
206
+ continue;
207
+ }
208
+
209
+ const { name, value: inlineValue } = splitOption(token);
210
+
211
+ if (name === '--help' || name === '-h') {
212
+ flags = withFlag(flags, 'help', true);
213
+ continue;
214
+ }
215
+ if (name === '--version' || name === '-v') {
216
+ flags = withFlag(flags, 'version', true);
217
+ continue;
218
+ }
219
+ if (name === '--print') {
220
+ flags = withFlag(flags, 'print', true);
221
+ if (!sawCommand) command = 'run';
222
+ continue;
223
+ }
224
+ if (name === '--json') {
225
+ flags = withFlag(flags, 'outputFormat', 'json');
226
+ continue;
227
+ }
228
+ if (name === '--no-alt-screen') {
229
+ flags = withFlag(flags, 'noAltScreen', true);
230
+ continue;
231
+ }
232
+ if (name === '--open') {
233
+ flags = withFlag(flags, 'open', true);
234
+ continue;
235
+ }
236
+ if (name === '--continue') {
237
+ flags = withFlag(flags, 'continueLast', true);
238
+ continue;
239
+ }
240
+ if (name === '--fork') {
241
+ flags = withFlag(flags, 'fork', true);
242
+ continue;
243
+ }
244
+ if (name === '--raw-output') {
245
+ flags = withFlag(flags, 'rawOutput', true);
246
+ continue;
247
+ }
248
+ if (name === '--accept-raw-output-risk') {
249
+ flags = withFlag(flags, 'acceptRawOutputRisk', true);
250
+ continue;
251
+ }
252
+
253
+ if (name === '--provider') {
254
+ const consumed = getValue(argv, index, inlineValue, name, errors);
255
+ index = consumed.nextIndex;
256
+ if (consumed.value !== undefined) flags = withFlag(flags, 'provider', consumed.value);
257
+ continue;
258
+ }
259
+ if (name === '--model' || name === '-m') {
260
+ const consumed = getValue(argv, index, inlineValue, name, errors);
261
+ index = consumed.nextIndex;
262
+ if (consumed.value !== undefined) {
263
+ flags = withFlag(flags, 'model', consumed.value);
264
+ flags = withFlag(flags, 'provider', inferProviderFromModel(consumed.value, flags.provider));
265
+ }
266
+ continue;
267
+ }
268
+ if (name === '--daemon-home') {
269
+ const consumed = getValue(argv, index, inlineValue, name, errors);
270
+ index = consumed.nextIndex;
271
+ if (consumed.value !== undefined) flags = withFlag(flags, 'daemonHome', consumed.value);
272
+ continue;
273
+ }
274
+ if (name === '--working-dir' || name === '--cd' || name === '-C') {
275
+ const consumed = getValue(argv, index, inlineValue, name, errors);
276
+ index = consumed.nextIndex;
277
+ if (consumed.value !== undefined) flags = withFlag(flags, 'workingDir', consumed.value);
278
+ continue;
279
+ }
280
+ if (name === '--prompt' || name === '-p') {
281
+ const consumed = getValue(argv, index, inlineValue, name, errors);
282
+ index = consumed.nextIndex;
283
+ if (consumed.value !== undefined) {
284
+ flags = withFlag(flags, 'prompt', consumed.value);
285
+ if (!sawCommand) command = 'run';
286
+ }
287
+ continue;
288
+ }
289
+ if (name === '--output-format' || name === '--output' || name === '-o') {
290
+ const consumed = getValue(argv, index, inlineValue, name, errors);
291
+ index = consumed.nextIndex;
292
+ flags = withFlag(flags, 'outputFormat', normalizeOutputFormat(consumed.value, errors));
293
+ continue;
294
+ }
295
+ if (name === '--config') {
296
+ const consumed = getValue(argv, index, inlineValue, name, errors);
297
+ index = consumed.nextIndex;
298
+ if (consumed.value !== undefined) flags = appendFlagArray(flags, 'configOverrides', consumed.value);
299
+ continue;
300
+ }
301
+ if (name === '-c') {
302
+ const consumed = getValue(argv, index, inlineValue, name, errors);
303
+ index = consumed.nextIndex;
304
+ if (consumed.value !== undefined) flags = appendFlagArray(flags, 'configOverrides', consumed.value);
305
+ continue;
306
+ }
307
+ if (name === '--enable') {
308
+ const consumed = getValue(argv, index, inlineValue, name, errors);
309
+ index = consumed.nextIndex;
310
+ if (consumed.value !== undefined) flags = appendFlagArray(flags, 'enableFeatures', consumed.value);
311
+ continue;
312
+ }
313
+ if (name === '--disable') {
314
+ const consumed = getValue(argv, index, inlineValue, name, errors);
315
+ index = consumed.nextIndex;
316
+ if (consumed.value !== undefined) flags = appendFlagArray(flags, 'disableFeatures', consumed.value);
317
+ continue;
318
+ }
319
+ if (name === '--port') {
320
+ const consumed = getValue(argv, index, inlineValue, name, errors);
321
+ index = consumed.nextIndex;
322
+ flags = withFlag(flags, 'port', parsePort(consumed.value, name, errors));
323
+ continue;
324
+ }
325
+ if (name === '--hostname' || name === '--host') {
326
+ const consumed = getValue(argv, index, inlineValue, name, errors);
327
+ index = consumed.nextIndex;
328
+ if (consumed.value !== undefined) flags = withFlag(flags, 'hostname', consumed.value);
329
+ continue;
330
+ }
331
+ if (name === '--resume' || name === '-r') {
332
+ const consumed = getOptionalValue(argv, index, inlineValue);
333
+ index = consumed.nextIndex;
334
+ flags = withFlag(flags, 'resume', consumed.value ?? 'latest');
335
+ continue;
336
+ }
337
+ if (name === '--session' || name === '-s') {
338
+ const consumed = getValue(argv, index, inlineValue, name, errors);
339
+ index = consumed.nextIndex;
340
+ if (consumed.value !== undefined) flags = withFlag(flags, 'session', consumed.value);
341
+ continue;
342
+ }
343
+
344
+ if (sawCommand) {
345
+ commandArgs.push(token);
346
+ continue;
347
+ }
348
+
349
+ errors.push(`Unknown option: ${name}`);
350
+ }
351
+
352
+ if (flags.prompt === undefined && (command === 'run' || flags.print) && positionals.length > 0) {
353
+ flags = withFlag(flags, 'prompt', positionals.join(' '));
354
+ }
355
+
356
+ if (rawCommand !== undefined && command === 'unknown') {
357
+ errors.push(`Unknown command: ${rawCommand}`);
358
+ }
359
+
360
+ return {
361
+ binary,
362
+ command,
363
+ rawCommand,
364
+ commandArgs,
365
+ positionals,
366
+ flags,
367
+ errors,
368
+ };
369
+ }
@@ -0,0 +1,107 @@
1
+ export type ProviderSetupClass =
2
+ | 'api-key'
3
+ | 'cloud-account'
4
+ | 'local'
5
+ | 'no-key-free'
6
+ | 'self-hosted'
7
+ | 'subscription'
8
+ | 'unknown';
9
+
10
+ export interface ProviderSetupClassificationInput {
11
+ readonly providerId: string;
12
+ readonly authMode?: string;
13
+ readonly configured?: boolean;
14
+ readonly modelCount?: number;
15
+ }
16
+
17
+ export interface ProviderSetupClassification {
18
+ readonly setupClass: ProviderSetupClass;
19
+ readonly setupLabel: string;
20
+ readonly setupDetail: string;
21
+ }
22
+
23
+ const SUBSCRIPTION_PROVIDERS = new Set([
24
+ 'openai-subscriber',
25
+ ]);
26
+
27
+ const LOCAL_PROVIDERS = new Set([
28
+ 'ollama',
29
+ 'synthetic',
30
+ ]);
31
+
32
+ const SELF_HOSTED_PROVIDERS = new Set([
33
+ 'copilot-proxy',
34
+ 'litellm',
35
+ 'sglang',
36
+ ]);
37
+
38
+ const CLOUD_ACCOUNT_PROVIDERS = new Set([
39
+ 'amazon-bedrock',
40
+ 'amazon-bedrock-mantle',
41
+ 'anthropic-vertex',
42
+ 'microsoft-foundry',
43
+ ]);
44
+
45
+ function byClass(setupClass: ProviderSetupClass): ProviderSetupClassification {
46
+ switch (setupClass) {
47
+ case 'api-key':
48
+ return {
49
+ setupClass,
50
+ setupLabel: 'API key',
51
+ setupDetail: 'Requires a provider API key or equivalent secret.',
52
+ };
53
+ case 'cloud-account':
54
+ return {
55
+ setupClass,
56
+ setupLabel: 'Cloud account',
57
+ setupDetail: 'Requires cloud account credentials or workload identity.',
58
+ };
59
+ case 'local':
60
+ return {
61
+ setupClass,
62
+ setupLabel: 'Local/no-key',
63
+ setupDetail: 'Runs locally or through a built-in test/local path without a paid API key.',
64
+ };
65
+ case 'no-key-free':
66
+ return {
67
+ setupClass,
68
+ setupLabel: 'No-key/free',
69
+ setupDetail: 'Can be listed or used without a configured paid API key in this runtime.',
70
+ };
71
+ case 'self-hosted':
72
+ return {
73
+ setupClass,
74
+ setupLabel: 'Self-hosted',
75
+ setupDetail: 'Uses an operator-managed local or self-hosted gateway.',
76
+ };
77
+ case 'subscription':
78
+ return {
79
+ setupClass,
80
+ setupLabel: 'Subscription',
81
+ setupDetail: 'Uses a stored subscription/OAuth session instead of a raw API key.',
82
+ };
83
+ case 'unknown':
84
+ return {
85
+ setupClass,
86
+ setupLabel: 'Unknown',
87
+ setupDetail: 'Setup path is not available from runtime metadata.',
88
+ };
89
+ }
90
+ }
91
+
92
+ export function classifyProviderSetup(input: ProviderSetupClassificationInput): ProviderSetupClassification {
93
+ const providerId = input.providerId.trim().toLowerCase();
94
+ const authMode = input.authMode?.trim().toLowerCase() ?? '';
95
+ const configured = input.configured ?? false;
96
+ const modelCount = input.modelCount ?? 0;
97
+
98
+ if (SUBSCRIPTION_PROVIDERS.has(providerId) || authMode === 'oauth') return byClass('subscription');
99
+ if (LOCAL_PROVIDERS.has(providerId)) return byClass('local');
100
+ if (SELF_HOSTED_PROVIDERS.has(providerId)) return byClass('self-hosted');
101
+ if (CLOUD_ACCOUNT_PROVIDERS.has(providerId)) return byClass('cloud-account');
102
+ if (authMode === 'api-key' || authMode === 'bearer') return byClass('api-key');
103
+ if (authMode === 'anonymous' || authMode === 'none') {
104
+ return configured || modelCount > 0 ? byClass('no-key-free') : byClass('unknown');
105
+ }
106
+ return byClass('unknown');
107
+ }