@pellux/goodvibes-tui 0.20.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion generator for the goodvibes CLI.
|
|
3
|
+
*
|
|
4
|
+
* Data derivation: the internal COMMAND_ALIASES, COMMANDS, OPTIONS, and
|
|
5
|
+
* COMMAND_HELP constants in src/cli/completion.ts and src/cli/help.ts are
|
|
6
|
+
* unexported, so they cannot be imported directly. The GoodVibesCliCommand
|
|
7
|
+
* union type IS exported and is used here to produce a typed completion
|
|
8
|
+
* surface. Flag names are derived from the parser logic visible in
|
|
9
|
+
* src/cli/parser.ts (each `if (name === '--foo')` branch). Subcommands per
|
|
10
|
+
* command are derived from the usage strings documented in help.ts's
|
|
11
|
+
* COMMAND_HELP table (read-only reference; no mutation of existing files).
|
|
12
|
+
*
|
|
13
|
+
* Deferred wiring (one-line changes to existing files — NOT done here):
|
|
14
|
+
* src/cli/completion.ts: replace renderCompletion() body with
|
|
15
|
+
* ```
|
|
16
|
+
* import { generateCompletion } from './completions/generate.ts';
|
|
17
|
+
* const normalized = (shell === 'zsh' || shell === 'fish') ? shell : 'bash';
|
|
18
|
+
* return generateCompletion(normalized, binary);
|
|
19
|
+
* ```
|
|
20
|
+
* src/cli/entrypoint.ts (or wherever 'completion' command is dispatched):
|
|
21
|
+
* ensure `completions <shell>` subcommand routes to generateCompletion.
|
|
22
|
+
* package.json scripts (optional convenience):
|
|
23
|
+
* `"completions": "bun run src/cli/completions/generate.ts"` to write files.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { GoodVibesCliCommand } from '../types.ts';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Typed completion data surface
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/** A flag definition for completion purposes. */
|
|
33
|
+
export interface CompletionFlag {
|
|
34
|
+
/** Primary flag name, e.g. '--model' */
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/** Short alias if any, e.g. '-m' */
|
|
37
|
+
readonly short?: string;
|
|
38
|
+
/** Additional aliases (long), e.g. '--working-dir' */
|
|
39
|
+
readonly aliases?: readonly string[];
|
|
40
|
+
/** Whether the flag takes a value argument */
|
|
41
|
+
readonly takesValue: boolean;
|
|
42
|
+
/** Enumerated completions for the flag value, if applicable */
|
|
43
|
+
readonly valueEnum?: readonly string[];
|
|
44
|
+
/** Human-readable description */
|
|
45
|
+
readonly description: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Per-command completion metadata. */
|
|
49
|
+
export interface CompletionCommand {
|
|
50
|
+
/** The canonical command name (matches GoodVibesCliCommand union) */
|
|
51
|
+
readonly name: GoodVibesCliCommand;
|
|
52
|
+
/** All tokens that map to this command (aliases included) */
|
|
53
|
+
readonly aliases: readonly string[];
|
|
54
|
+
/** One-line description */
|
|
55
|
+
readonly description: string;
|
|
56
|
+
/** Subcommand tokens accepted by this command */
|
|
57
|
+
readonly subcommands: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Full CLI completion surface. */
|
|
61
|
+
export interface CompletionSurface {
|
|
62
|
+
readonly binary: string;
|
|
63
|
+
readonly commands: readonly CompletionCommand[];
|
|
64
|
+
readonly globalFlags: readonly CompletionFlag[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Global flags
|
|
69
|
+
// Derived from parser.ts branch-by-branch inspection (each `if (name === ...)`).
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export const GLOBAL_FLAGS: readonly CompletionFlag[] = [
|
|
73
|
+
{ name: '--help', short: '-h', takesValue: false, description: 'Print help' },
|
|
74
|
+
{ name: '--version', short: '-v', takesValue: false, description: 'Print version' },
|
|
75
|
+
{
|
|
76
|
+
name: '--model',
|
|
77
|
+
short: '-m',
|
|
78
|
+
takesValue: true,
|
|
79
|
+
description: 'Override model (provider:model infers --provider)',
|
|
80
|
+
},
|
|
81
|
+
{ name: '--provider', takesValue: true, description: 'Override provider' },
|
|
82
|
+
{
|
|
83
|
+
name: '--cd',
|
|
84
|
+
short: '-C',
|
|
85
|
+
aliases: ['--working-dir'],
|
|
86
|
+
takesValue: true,
|
|
87
|
+
description: 'Set working directory for this launch',
|
|
88
|
+
},
|
|
89
|
+
{ name: '--daemon-home', takesValue: true, description: 'Override daemon home' },
|
|
90
|
+
{
|
|
91
|
+
name: '--config',
|
|
92
|
+
short: '-c',
|
|
93
|
+
takesValue: true,
|
|
94
|
+
description: 'Override a config value (key=value)',
|
|
95
|
+
},
|
|
96
|
+
{ name: '--enable', takesValue: true, description: 'Enable a feature flag for this launch' },
|
|
97
|
+
{ name: '--disable', takesValue: true, description: 'Disable a feature flag for this launch' },
|
|
98
|
+
{
|
|
99
|
+
name: '--prompt',
|
|
100
|
+
short: '-p',
|
|
101
|
+
takesValue: true,
|
|
102
|
+
description: 'Run a non-interactive prompt',
|
|
103
|
+
},
|
|
104
|
+
{ name: '--print', takesValue: false, description: 'Alias for non-interactive run mode' },
|
|
105
|
+
{
|
|
106
|
+
name: '--output',
|
|
107
|
+
short: '-o',
|
|
108
|
+
aliases: ['--output-format'],
|
|
109
|
+
takesValue: true,
|
|
110
|
+
valueEnum: ['text', 'json', 'stream-json'],
|
|
111
|
+
description: 'Output format: text, json, or stream-json',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: '--json',
|
|
115
|
+
takesValue: false,
|
|
116
|
+
description: 'Alias for --output-format json',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: '--no-alt-screen',
|
|
120
|
+
takesValue: false,
|
|
121
|
+
description: 'Keep output in normal terminal scrollback',
|
|
122
|
+
},
|
|
123
|
+
{ name: '--port', takesValue: true, description: 'Port for server/web commands' },
|
|
124
|
+
{ name: '--hostname', aliases: ['--host'], takesValue: true, description: 'Hostname for server/web commands' },
|
|
125
|
+
{ name: '--open', takesValue: false, description: 'Open browser when supported' },
|
|
126
|
+
{
|
|
127
|
+
name: '--resume',
|
|
128
|
+
short: '-r',
|
|
129
|
+
takesValue: true,
|
|
130
|
+
description: 'Resume saved session (optional id or "latest")',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: '--session',
|
|
134
|
+
short: '-s',
|
|
135
|
+
takesValue: true,
|
|
136
|
+
description: 'Use a specific session',
|
|
137
|
+
},
|
|
138
|
+
{ name: '--continue', takesValue: false, description: 'Continue the latest session' },
|
|
139
|
+
{ name: '--fork', takesValue: false, description: 'Fork session when supported' },
|
|
140
|
+
{ name: '--raw-output', takesValue: false, description: 'Raw output mode' },
|
|
141
|
+
{
|
|
142
|
+
name: '--accept-raw-output-risk',
|
|
143
|
+
takesValue: false,
|
|
144
|
+
description: 'Acknowledge raw output risk',
|
|
145
|
+
},
|
|
146
|
+
] as const;
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Command table
|
|
150
|
+
// Derived from GoodVibesCliCommand union + COMMAND_ALIASES in parser.ts +
|
|
151
|
+
// subcommand usage strings from COMMAND_HELP in help.ts.
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
export const COMPLETION_COMMANDS: readonly CompletionCommand[] = [
|
|
155
|
+
{
|
|
156
|
+
name: 'tui',
|
|
157
|
+
aliases: ['tui', 'app'],
|
|
158
|
+
description: 'Start the interactive TUI (default)',
|
|
159
|
+
subcommands: [],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'run',
|
|
163
|
+
aliases: ['run', 'exec', 'e'],
|
|
164
|
+
description: 'Run non-interactively with text/json/stream-json output',
|
|
165
|
+
subcommands: [],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'serve',
|
|
169
|
+
aliases: ['serve', 'daemon', 'server'],
|
|
170
|
+
description: 'Start the daemon/API host',
|
|
171
|
+
subcommands: [],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'web',
|
|
175
|
+
aliases: ['web'],
|
|
176
|
+
description: 'Show browser surface bind URL and enablement',
|
|
177
|
+
subcommands: [],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'service',
|
|
181
|
+
aliases: ['service', 'services'],
|
|
182
|
+
description: 'Inspect/manage daemon service lifecycle',
|
|
183
|
+
subcommands: ['status', 'check', 'install', 'start', 'stop', 'restart', 'uninstall'],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'status',
|
|
187
|
+
aliases: ['status'],
|
|
188
|
+
description: 'Print config, provider, service, and onboarding posture',
|
|
189
|
+
subcommands: [],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'doctor',
|
|
193
|
+
aliases: ['doctor'],
|
|
194
|
+
description: 'Print status plus setup warnings',
|
|
195
|
+
subcommands: [],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'onboarding',
|
|
199
|
+
aliases: ['onboarding', 'setup'],
|
|
200
|
+
description: 'Open onboarding in the TUI, or print onboarding status',
|
|
201
|
+
subcommands: ['status'],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'models',
|
|
205
|
+
aliases: ['models', 'model'],
|
|
206
|
+
description: 'List/use/pin selectable models and recent model history',
|
|
207
|
+
subcommands: ['current', 'use', 'pin', 'recent'],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'providers',
|
|
211
|
+
aliases: ['providers', 'provider'],
|
|
212
|
+
description: 'List/inspect/use provider config/auth posture',
|
|
213
|
+
subcommands: ['list', 'current', 'inspect', 'use'],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'auth',
|
|
217
|
+
aliases: ['auth'],
|
|
218
|
+
description: 'Inspect and manage local users, sessions, and bootstrap auth',
|
|
219
|
+
subcommands: ['status', 'users', 'sessions', 'add-user', 'clear-bootstrap'],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'subscription',
|
|
223
|
+
aliases: ['subscription', 'subscriptions'],
|
|
224
|
+
description: 'Start/finish/logout provider subscription sessions',
|
|
225
|
+
subcommands: ['list', 'providers', 'inspect', 'login', 'logout'],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'secrets',
|
|
229
|
+
aliases: ['secrets', 'secret'],
|
|
230
|
+
description: 'List, set, link, delete, and test GoodVibes secret refs',
|
|
231
|
+
subcommands: ['list', 'providers', 'test', 'set', 'link', 'delete'],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'sessions',
|
|
235
|
+
aliases: ['sessions', 'session'],
|
|
236
|
+
description: 'List, show, export, or resume saved sessions',
|
|
237
|
+
subcommands: ['list', 'show', 'export', 'resume'],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'tasks',
|
|
241
|
+
aliases: ['tasks', 'task'],
|
|
242
|
+
description: 'List/show in-process tasks or submit a non-interactive task',
|
|
243
|
+
subcommands: ['list', 'show', 'submit'],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'pair',
|
|
247
|
+
aliases: ['pair', 'qrcode', 'qr'],
|
|
248
|
+
description: 'Print companion pairing payload and QR code',
|
|
249
|
+
subcommands: [],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'surfaces',
|
|
253
|
+
aliases: ['surfaces', 'surface'],
|
|
254
|
+
description: 'Inspect/check/enable/disable browser/listener/external surfaces',
|
|
255
|
+
subcommands: ['list', 'check', 'show', 'enable', 'disable'],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'listener',
|
|
259
|
+
aliases: ['listener', 'http-listener', 'webhook'],
|
|
260
|
+
description: 'Test HTTP listener/webhook readiness',
|
|
261
|
+
subcommands: ['test'],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'control-plane',
|
|
265
|
+
aliases: ['control-plane', 'controlplane', 'cp'],
|
|
266
|
+
description: 'Inspect daemon auth, local admin, tokens, and ports',
|
|
267
|
+
subcommands: ['status'],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'bundle',
|
|
271
|
+
aliases: ['bundle', 'bundles'],
|
|
272
|
+
description: 'Move setup/profile/trust/support bundles',
|
|
273
|
+
subcommands: ['export', 'inspect', 'import'],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'remote',
|
|
277
|
+
aliases: ['remote'],
|
|
278
|
+
description: 'Inspect remote runner/node posture',
|
|
279
|
+
subcommands: [],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'bridge',
|
|
283
|
+
aliases: ['bridge'],
|
|
284
|
+
description: 'Bridge CLI operations (alias: remote)',
|
|
285
|
+
subcommands: [],
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: 'completion',
|
|
289
|
+
aliases: ['completion', 'completions'],
|
|
290
|
+
description: 'Generate shell completion script',
|
|
291
|
+
subcommands: ['bash', 'zsh', 'fish'],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'help',
|
|
295
|
+
aliases: ['help'],
|
|
296
|
+
description: 'Print help or command-specific help',
|
|
297
|
+
subcommands: [],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'version',
|
|
301
|
+
aliases: ['version'],
|
|
302
|
+
description: 'Print version',
|
|
303
|
+
subcommands: [],
|
|
304
|
+
},
|
|
305
|
+
] satisfies readonly CompletionCommand[];
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Helpers
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/** Collect every token that can appear as the first positional (all command aliases). */
|
|
312
|
+
export function allCommandTokens(commands: readonly CompletionCommand[]): readonly string[] {
|
|
313
|
+
const seen = new Set<string>();
|
|
314
|
+
const result: string[] = [];
|
|
315
|
+
for (const cmd of commands) {
|
|
316
|
+
for (const alias of cmd.aliases) {
|
|
317
|
+
if (!seen.has(alias)) {
|
|
318
|
+
seen.add(alias);
|
|
319
|
+
result.push(alias);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Collect all flag names (primary + short + aliases) without duplicates. */
|
|
327
|
+
export function allFlagTokens(flags: readonly CompletionFlag[]): readonly string[] {
|
|
328
|
+
const seen = new Set<string>();
|
|
329
|
+
const result: string[] = [];
|
|
330
|
+
for (const flag of flags) {
|
|
331
|
+
for (const token of [
|
|
332
|
+
flag.name,
|
|
333
|
+
...(flag.short !== undefined ? [flag.short] : []),
|
|
334
|
+
...(flag.aliases ?? []),
|
|
335
|
+
]) {
|
|
336
|
+
if (!seen.has(token)) {
|
|
337
|
+
seen.add(token);
|
|
338
|
+
result.push(token);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Return the CompletionSurface for the given binary name.
|
|
347
|
+
* Callers use this to drive all three shell generator functions.
|
|
348
|
+
*/
|
|
349
|
+
export function buildCompletionSurface(binary = 'goodvibes'): CompletionSurface {
|
|
350
|
+
return {
|
|
351
|
+
binary,
|
|
352
|
+
commands: COMPLETION_COMMANDS,
|
|
353
|
+
globalFlags: GLOBAL_FLAGS,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Bash generator
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
export function generateBash(surface: CompletionSurface): string {
|
|
362
|
+
const { binary, commands, globalFlags } = surface;
|
|
363
|
+
const safeBin = binary.replace(/-/g, '_');
|
|
364
|
+
const cmdTokens = allCommandTokens(commands);
|
|
365
|
+
const flagTokens = allFlagTokens(globalFlags);
|
|
366
|
+
|
|
367
|
+
const lines: string[] = [];
|
|
368
|
+
|
|
369
|
+
lines.push(`# bash completion for ${binary}`);
|
|
370
|
+
lines.push(`# Source this file or add to /etc/bash_completion.d/${binary}`);
|
|
371
|
+
lines.push(`# Generated by: goodvibes completion bash`);
|
|
372
|
+
lines.push('');
|
|
373
|
+
|
|
374
|
+
// Subcommand-specific completion functions
|
|
375
|
+
for (const cmd of commands) {
|
|
376
|
+
if (cmd.subcommands.length === 0) continue;
|
|
377
|
+
const fnName = `_${safeBin}_cmd_${cmd.name.replace(/-/g, '_')}`;
|
|
378
|
+
lines.push(`${fnName}() {`);
|
|
379
|
+
lines.push(` local cur="\${COMP_WORDS[COMP_CWORD]}";`);
|
|
380
|
+
lines.push(` local subs="${cmd.subcommands.join(' ')}";`);
|
|
381
|
+
lines.push(` COMPREPLY=( $(compgen -W "$subs" -- "$cur") );`);
|
|
382
|
+
lines.push(`}`);
|
|
383
|
+
lines.push('');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Output-format completion helper
|
|
387
|
+
lines.push(`_${safeBin}_output_formats() {`);
|
|
388
|
+
lines.push(` local cur="\${COMP_WORDS[COMP_CWORD]}";`);
|
|
389
|
+
lines.push(` COMPREPLY=( $(compgen -W "text json stream-json" -- "$cur") );`);
|
|
390
|
+
lines.push(`}`);
|
|
391
|
+
lines.push('');
|
|
392
|
+
|
|
393
|
+
// Main completion function
|
|
394
|
+
lines.push(`_${safeBin}() {`);
|
|
395
|
+
lines.push(` local cur prev words cword;`);
|
|
396
|
+
lines.push(` _init_completion 2>/dev/null || {`);
|
|
397
|
+
lines.push(` cur="\${COMP_WORDS[COMP_CWORD]}";`);
|
|
398
|
+
lines.push(` prev="\${COMP_WORDS[COMP_CWORD-1]}";`);
|
|
399
|
+
lines.push(` words=("\${COMP_WORDS[@]}");`);
|
|
400
|
+
lines.push(` cword=$COMP_CWORD;`);
|
|
401
|
+
lines.push(` };`);
|
|
402
|
+
lines.push('');
|
|
403
|
+
|
|
404
|
+
// Flag-value completions
|
|
405
|
+
lines.push(` case "$prev" in`);
|
|
406
|
+
lines.push(` --output|--output-format|-o)`);
|
|
407
|
+
lines.push(` COMPREPLY=( $(compgen -W "text json stream-json" -- "$cur") );`);
|
|
408
|
+
lines.push(` return;;`);
|
|
409
|
+
lines.push(` --provider)`);
|
|
410
|
+
lines.push(` return;;`);
|
|
411
|
+
lines.push(` --model|-m)`);
|
|
412
|
+
lines.push(` return;;`);
|
|
413
|
+
lines.push(` --cd|--working-dir|-C|--daemon-home)`);
|
|
414
|
+
lines.push(` _filedir -d;`);
|
|
415
|
+
lines.push(` return;;`);
|
|
416
|
+
lines.push(` --port)`);
|
|
417
|
+
lines.push(` return;;`);
|
|
418
|
+
lines.push(` --hostname|--host)`);
|
|
419
|
+
lines.push(` return;;`);
|
|
420
|
+
lines.push(` --session|-s|--resume|-r)`);
|
|
421
|
+
lines.push(` return;;`);
|
|
422
|
+
lines.push(` esac`);
|
|
423
|
+
lines.push('');
|
|
424
|
+
|
|
425
|
+
// Subcommand routing
|
|
426
|
+
lines.push(` local cmd="";`);
|
|
427
|
+
lines.push(` local i;`);
|
|
428
|
+
lines.push(` for i in "\${!words[@]}"; do`);
|
|
429
|
+
lines.push(` if [[ $i -gt 0 ]] && [[ "\${words[$i]}" != -* ]]; then`);
|
|
430
|
+
lines.push(` cmd="\${words[$i]}";`);
|
|
431
|
+
lines.push(` break;`);
|
|
432
|
+
lines.push(` fi;`);
|
|
433
|
+
lines.push(` done;`);
|
|
434
|
+
lines.push('');
|
|
435
|
+
lines.push(` case "$cmd" in`);
|
|
436
|
+
|
|
437
|
+
for (const cmd of commands) {
|
|
438
|
+
if (cmd.subcommands.length === 0) continue;
|
|
439
|
+
const pattern = cmd.aliases.join('|');
|
|
440
|
+
const fnName = `_${safeBin}_cmd_${cmd.name.replace(/-/g, '_')}`;
|
|
441
|
+
lines.push(` ${pattern}) ${fnName}; return;;`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
lines.push(` esac`);
|
|
445
|
+
lines.push('');
|
|
446
|
+
|
|
447
|
+
// Top-level: commands + flags
|
|
448
|
+
const all = [...cmdTokens, ...flagTokens].join(' ');
|
|
449
|
+
lines.push(` if [[ "$cur" == -* ]]; then`);
|
|
450
|
+
lines.push(` COMPREPLY=( $(compgen -W "${flagTokens.join(' ')}" -- "$cur") );`);
|
|
451
|
+
lines.push(` else`);
|
|
452
|
+
lines.push(` COMPREPLY=( $(compgen -W "${cmdTokens.join(' ')}" -- "$cur") );`);
|
|
453
|
+
lines.push(` fi;`);
|
|
454
|
+
lines.push(` # Allow file path completions to fall through when no match`);
|
|
455
|
+
lines.push(` [[ "\${#COMPREPLY[@]}" -eq 0 ]] && COMPREPLY=( $(compgen -W "${all}" -- "$cur") );`);
|
|
456
|
+
lines.push(`}`);
|
|
457
|
+
lines.push('');
|
|
458
|
+
lines.push(`complete -F _${safeBin} ${binary}`);
|
|
459
|
+
|
|
460
|
+
return lines.join('\n');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Zsh generator
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
export function generateZsh(surface: CompletionSurface): string {
|
|
468
|
+
const { binary, commands, globalFlags } = surface;
|
|
469
|
+
const safeBin = binary.replace(/-/g, '_');
|
|
470
|
+
|
|
471
|
+
const lines: string[] = [];
|
|
472
|
+
lines.push(`#compdef ${binary}`);
|
|
473
|
+
lines.push(`# zsh completion for ${binary}`);
|
|
474
|
+
lines.push(`# Source or symlink to a dir on $fpath, e.g. /usr/local/share/zsh/site-functions/_${binary}`);
|
|
475
|
+
lines.push(`# Generated by: goodvibes completion zsh`);
|
|
476
|
+
lines.push('');
|
|
477
|
+
|
|
478
|
+
// Arguments spec
|
|
479
|
+
const cmdArgs = allCommandTokens(commands)
|
|
480
|
+
.map((tok) => {
|
|
481
|
+
const cmd = commands.find((c) => c.aliases.includes(tok));
|
|
482
|
+
const desc = cmd?.description ?? '';
|
|
483
|
+
return `'${tok}:${desc.replace(/'/g, '')}'`;
|
|
484
|
+
})
|
|
485
|
+
.join(' ');
|
|
486
|
+
|
|
487
|
+
lines.push(`_${safeBin}() {`);
|
|
488
|
+
lines.push(` local state;`);
|
|
489
|
+
lines.push('');
|
|
490
|
+
lines.push(` _arguments \\`);
|
|
491
|
+
lines.push(` '(-h --help)'{-h,--help}'[Print help]' \\`);
|
|
492
|
+
lines.push(` '(-v --version)'{-v,--version}'[Print version]' \\`);
|
|
493
|
+
lines.push(` '(-m --model)'{-m,--model}'[Override model]:model:' \\`);
|
|
494
|
+
lines.push(` '--provider[Override provider]:provider:' \\`);
|
|
495
|
+
lines.push(` '(-C --cd --working-dir)'{-C,--cd}'[Set working directory]:dir:_files -/' \\`);
|
|
496
|
+
lines.push(` '--working-dir[Alias for --cd]:dir:_files -/' \\`);
|
|
497
|
+
lines.push(` '--daemon-home[Override daemon home]:dir:_files -/' \\`);
|
|
498
|
+
lines.push(` '(-c --config)'{-c,--config}'[Override config value]:key=value:' \\`);
|
|
499
|
+
lines.push(` '--enable[Enable feature flag]:feature:' \\`);
|
|
500
|
+
lines.push(` '--disable[Disable feature flag]:feature:' \\`);
|
|
501
|
+
lines.push(` '(-p --prompt)'{-p,--prompt}'[Run non-interactive prompt]:text:' \\`);
|
|
502
|
+
lines.push(` '--print[Non-interactive run mode]' \\`);
|
|
503
|
+
lines.push(` '(-o --output --output-format)'{-o,--output}'[Output format]:format:(text json stream-json)' \\`);
|
|
504
|
+
lines.push(` '--output-format[Output format alias]:format:(text json stream-json)' \\`);
|
|
505
|
+
lines.push(` '--json[Alias for --output-format json]' \\`);
|
|
506
|
+
lines.push(` '--no-alt-screen[Keep output in normal terminal scrollback]' \\`);
|
|
507
|
+
lines.push(` '--port[Port for server/web commands]:port:' \\`);
|
|
508
|
+
lines.push(` '--hostname[Hostname for server/web commands]:host:' \\`);
|
|
509
|
+
lines.push(` '--host[Hostname alias]:host:' \\`);
|
|
510
|
+
lines.push(` '--open[Open browser when supported]' \\`);
|
|
511
|
+
lines.push(` '(-r --resume)'{-r,--resume}'[Resume saved session]:id:' \\`);
|
|
512
|
+
lines.push(` '(-s --session)'{-s,--session}'[Use a specific session]:id:' \\`);
|
|
513
|
+
lines.push(` '--continue[Continue the latest session]' \\`);
|
|
514
|
+
lines.push(` '--fork[Fork session when supported]' \\`);
|
|
515
|
+
lines.push(` '--raw-output[Raw output mode]' \\`);
|
|
516
|
+
lines.push(` '--accept-raw-output-risk[Acknowledge raw output risk]' \\`);
|
|
517
|
+
lines.push(` '1:command:->cmd' \\`);
|
|
518
|
+
lines.push(` '*:args:->args';`);
|
|
519
|
+
lines.push('');
|
|
520
|
+
lines.push(` case $state in`);
|
|
521
|
+
lines.push(` cmd)`);
|
|
522
|
+
lines.push(` local cmds;`);
|
|
523
|
+
lines.push(` cmds=(`);
|
|
524
|
+
|
|
525
|
+
for (const cmd of commands) {
|
|
526
|
+
const desc = cmd.description.replace(/'/g, '');
|
|
527
|
+
lines.push(` '${cmd.name}:${desc}'`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
lines.push(` );`);
|
|
531
|
+
lines.push(` _describe 'command' cmds;`);
|
|
532
|
+
lines.push(` ;;`);
|
|
533
|
+
lines.push(` args)`);
|
|
534
|
+
lines.push(` case $words[2] in`);
|
|
535
|
+
|
|
536
|
+
for (const cmd of commands) {
|
|
537
|
+
if (cmd.subcommands.length === 0) continue;
|
|
538
|
+
const pattern = cmd.aliases.join('|');
|
|
539
|
+
const subs = cmd.subcommands.map((s) => `'${s}'`).join(' ');
|
|
540
|
+
lines.push(` ${pattern})`);
|
|
541
|
+
lines.push(` local sub_cmds=(${subs});`);
|
|
542
|
+
lines.push(` _describe 'subcommand' sub_cmds;;`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
lines.push(` esac;`);
|
|
546
|
+
lines.push(` ;;`);
|
|
547
|
+
lines.push(` esac;`);
|
|
548
|
+
lines.push(`}`);
|
|
549
|
+
lines.push('');
|
|
550
|
+
lines.push(`_${safeBin} "$@"`);
|
|
551
|
+
|
|
552
|
+
return lines.join('\n');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
// Fish generator
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
export function generateFish(surface: CompletionSurface): string {
|
|
560
|
+
const { binary, commands, globalFlags } = surface;
|
|
561
|
+
const lines: string[] = [];
|
|
562
|
+
|
|
563
|
+
lines.push(`# fish completion for ${binary}`);
|
|
564
|
+
lines.push(`# Place in ~/.config/fish/completions/${binary}.fish or the fish data dir`);
|
|
565
|
+
lines.push(`# Generated by: goodvibes completion fish`);
|
|
566
|
+
lines.push('');
|
|
567
|
+
|
|
568
|
+
// Disable default file completion
|
|
569
|
+
lines.push(`complete -c ${binary} -f`);
|
|
570
|
+
lines.push('');
|
|
571
|
+
|
|
572
|
+
// Top-level commands (no subcommand context guard)
|
|
573
|
+
for (const cmd of commands) {
|
|
574
|
+
const desc = cmd.description.replace(/'/g, '').replace(/"/g, '');
|
|
575
|
+
// Primary name
|
|
576
|
+
lines.push(`complete -c ${binary} -n '__fish_use_subcommand' -a ${JSON.stringify(cmd.name)} -d ${JSON.stringify(desc)}`);
|
|
577
|
+
// Aliases (excluding canonical which was just added)
|
|
578
|
+
for (const alias of cmd.aliases) {
|
|
579
|
+
if (alias === cmd.name) continue;
|
|
580
|
+
lines.push(`complete -c ${binary} -n '__fish_use_subcommand' -a ${JSON.stringify(alias)} -d ${JSON.stringify(`Alias for ${cmd.name}`)}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
lines.push('');
|
|
585
|
+
|
|
586
|
+
// Global flags
|
|
587
|
+
for (const flag of globalFlags) {
|
|
588
|
+
const longName = flag.name.replace(/^--/, '');
|
|
589
|
+
const shortPart = flag.short !== undefined ? ` -s ${flag.short.replace(/^-/, '')}` : '';
|
|
590
|
+
const valuePart = flag.takesValue ? '' : ' -f';
|
|
591
|
+
// For flags with enumerated values, use -x (exclusive/no-files) and attach
|
|
592
|
+
// -a per value directly on the same declaration line so fish can offer them
|
|
593
|
+
// after the flag. __fish_seen_subcommand_from matches subcommand words, not
|
|
594
|
+
// option flags, so a separate condition-gated line would be inert.
|
|
595
|
+
const enumSuffix =
|
|
596
|
+
flag.valueEnum !== undefined
|
|
597
|
+
? flag.valueEnum.map((v) => ` -a ${JSON.stringify(v)}`).join('')
|
|
598
|
+
: '';
|
|
599
|
+
const exclusivePart = flag.valueEnum !== undefined ? ' -x' : valuePart;
|
|
600
|
+
|
|
601
|
+
lines.push(
|
|
602
|
+
`complete -c ${binary} -l ${longName}${shortPart}${exclusivePart} -d ${JSON.stringify(flag.description)}${enumSuffix}`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
lines.push('');
|
|
607
|
+
|
|
608
|
+
// Subcommand completions
|
|
609
|
+
for (const cmd of commands) {
|
|
610
|
+
if (cmd.subcommands.length === 0) continue;
|
|
611
|
+
const allAliases = cmd.aliases.join(' ');
|
|
612
|
+
for (const sub of cmd.subcommands) {
|
|
613
|
+
lines.push(
|
|
614
|
+
`complete -c ${binary} -n '__fish_seen_subcommand_from ${allAliases}' -a ${JSON.stringify(sub)}`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return lines.join('\n');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
// Dispatcher — mirrors the existing renderCompletion(shell) API
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Generate a completion script for the given shell.
|
|
628
|
+
* Returns the script text. Throws if shell is not recognized.
|
|
629
|
+
*/
|
|
630
|
+
export function generateCompletion(
|
|
631
|
+
shell: 'bash' | 'zsh' | 'fish',
|
|
632
|
+
binary = 'goodvibes',
|
|
633
|
+
): string {
|
|
634
|
+
const surface = buildCompletionSurface(binary);
|
|
635
|
+
switch (shell) {
|
|
636
|
+
case 'bash': return generateBash(surface);
|
|
637
|
+
case 'zsh': return generateZsh(surface);
|
|
638
|
+
case 'fish': return generateFish(surface);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
// CLI entry-point (run via: bun run src/cli/completions/generate.ts [bash|zsh|fish])
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
|
|
646
|
+
if (import.meta.main) {
|
|
647
|
+
const { mkdirSync, writeFileSync } = await import('node:fs');
|
|
648
|
+
const { join } = await import('node:path');
|
|
649
|
+
|
|
650
|
+
const shells = ['bash', 'zsh', 'fish'] as const;
|
|
651
|
+
const outDir = 'completions';
|
|
652
|
+
mkdirSync(outDir, { recursive: true });
|
|
653
|
+
|
|
654
|
+
for (const shell of shells) {
|
|
655
|
+
const script = generateCompletion(shell);
|
|
656
|
+
const ext = shell === 'bash' ? 'bash' : shell === 'zsh' ? 'zsh' : 'fish';
|
|
657
|
+
const filename = shell === 'zsh' ? `_goodvibes` : `goodvibes.${ext}`;
|
|
658
|
+
const outPath = join(outDir, filename);
|
|
659
|
+
writeFileSync(outPath, script + '\n', 'utf-8');
|
|
660
|
+
process.stdout.write(`Wrote ${outPath}\n`);
|
|
661
|
+
}
|
|
662
|
+
}
|