@pellux/goodvibes-tui 0.19.23 → 0.19.25

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 (72) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +5 -5
  3. package/bin/goodvibes +5 -0
  4. package/bin/goodvibes-daemon +5 -0
  5. package/docs/foundation-artifacts/operator-contract.json +1 -1
  6. package/package.json +2 -2
  7. package/src/cli/completion.ts +89 -0
  8. package/src/cli/config-overrides.ts +159 -0
  9. package/src/cli/endpoints.ts +63 -0
  10. package/src/cli/entrypoint.ts +155 -0
  11. package/src/cli/help.ts +122 -0
  12. package/src/cli/index.ts +8 -0
  13. package/src/cli/management-commands.ts +576 -0
  14. package/src/cli/management.ts +693 -0
  15. package/src/cli/parser.ts +367 -0
  16. package/src/cli/status.ts +112 -0
  17. package/src/cli/tui-startup.ts +32 -0
  18. package/src/cli/types.ts +63 -0
  19. package/src/cli-flags.ts +17 -55
  20. package/src/config/index.ts +1 -1
  21. package/src/config/secrets.ts +44 -0
  22. package/src/core/conversation.ts +36 -13
  23. package/src/daemon/cli.ts +62 -11
  24. package/src/input/command-registry.ts +3 -0
  25. package/src/input/commands/guidance-runtime.ts +9 -4
  26. package/src/input/commands/local-runtime.ts +21 -7
  27. package/src/input/commands/local-setup.ts +31 -38
  28. package/src/input/commands/onboarding-runtime.ts +14 -0
  29. package/src/input/commands/runtime-services.ts +9 -0
  30. package/src/input/commands.ts +2 -0
  31. package/src/input/feed-context-factory.ts +8 -1
  32. package/src/input/handler-feed.ts +13 -8
  33. package/src/input/handler-interactions.ts +266 -0
  34. package/src/input/handler-modal-stack.ts +23 -3
  35. package/src/input/handler-modal-token-routes.ts +23 -1
  36. package/src/input/handler-onboarding.ts +696 -0
  37. package/src/input/handler-picker-routes.ts +15 -7
  38. package/src/input/handler-ui-state.ts +58 -0
  39. package/src/input/handler.ts +120 -246
  40. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  41. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  42. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  43. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  44. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  45. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  46. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  47. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  48. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  49. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  50. package/src/main.ts +32 -39
  51. package/src/panels/builtin/operations.ts +0 -10
  52. package/src/panels/index.ts +0 -1
  53. package/src/panels/panel-manager.ts +6 -2
  54. package/src/renderer/conversation-overlays.ts +6 -0
  55. package/src/renderer/help-overlay.ts +1 -1
  56. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  57. package/src/renderer/panel-composite.ts +42 -5
  58. package/src/renderer/panel-workspace-bar.ts +5 -1
  59. package/src/runtime/bootstrap-core.ts +1 -0
  60. package/src/runtime/bootstrap.ts +123 -0
  61. package/src/runtime/onboarding/apply.ts +685 -0
  62. package/src/runtime/onboarding/derivation.ts +495 -0
  63. package/src/runtime/onboarding/index.ts +7 -0
  64. package/src/runtime/onboarding/markers.ts +161 -0
  65. package/src/runtime/onboarding/snapshot.ts +400 -0
  66. package/src/runtime/onboarding/state.ts +140 -0
  67. package/src/runtime/onboarding/types.ts +402 -0
  68. package/src/runtime/onboarding/verify.ts +233 -0
  69. package/src/runtime/ui-services.ts +16 -0
  70. package/src/shell/ui-openers.ts +12 -2
  71. package/src/version.ts +1 -1
  72. package/src/panels/welcome-panel.ts +0 -64
@@ -13,9 +13,38 @@ import {
13
13
  SecretsManager as SdkSecretsManager,
14
14
  type SecretsManagerOptions as SdkSecretsManagerOptions,
15
15
  } from '@pellux/goodvibes-sdk/platform/config/secrets';
16
+ import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
16
17
 
17
18
  export type SecretsManagerOptions = Omit<SdkSecretsManagerOptions, 'surfaceRoot'>;
18
19
 
20
+ const RAW_SECRET_LITERAL_PREFIX = '__GOODVIBES_LITERAL_V1__';
21
+
22
+ function isGoodVibesSecretRefInput(value: string): boolean {
23
+ const normalized = value.trim();
24
+ return normalized.startsWith('goodvibes://secrets/') && isSecretRefInput(normalized);
25
+ }
26
+
27
+ function shouldStoreAsLiteral(value: string): boolean {
28
+ return value.startsWith(RAW_SECRET_LITERAL_PREFIX)
29
+ || (isSecretRefInput(value) && !isGoodVibesSecretRefInput(value));
30
+ }
31
+
32
+ function encodeLiteralSecret(value: string): string {
33
+ return `${RAW_SECRET_LITERAL_PREFIX}${Buffer.from(JSON.stringify({ value }), 'utf-8').toString('base64url')}`;
34
+ }
35
+
36
+ function decodeLiteralSecret(value: string): string | null {
37
+ if (!value.startsWith(RAW_SECRET_LITERAL_PREFIX)) return null;
38
+ try {
39
+ const decoded = Buffer.from(value.slice(RAW_SECRET_LITERAL_PREFIX.length), 'base64url').toString('utf-8');
40
+ const parsed = JSON.parse(decoded) as unknown;
41
+ if (!parsed || typeof parsed !== 'object' || typeof (parsed as { value?: unknown }).value !== 'string') return null;
42
+ return (parsed as { value: string }).value;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
19
48
  export class SecretsManager extends SdkSecretsManager {
20
49
  constructor(options: SecretsManagerOptions) {
21
50
  super({
@@ -23,4 +52,19 @@ export class SecretsManager extends SdkSecretsManager {
23
52
  surfaceRoot: 'tui',
24
53
  });
25
54
  }
55
+
56
+ override async get(key: string): Promise<string | null> {
57
+ const envValue = process.env[key];
58
+ if (envValue !== undefined && shouldStoreAsLiteral(envValue)) {
59
+ return decodeLiteralSecret(envValue) ?? envValue;
60
+ }
61
+
62
+ const value = await super.get(key);
63
+ if (value === null) return null;
64
+ return decodeLiteralSecret(value) ?? value;
65
+ }
66
+
67
+ override async set(key: string, value: string, options?: Parameters<SdkSecretsManager['set']>[2]): Promise<void> {
68
+ await super.set(key, shouldStoreAsLiteral(value) ? encodeLiteralSecret(value) : value, options);
69
+ }
26
70
  }
@@ -74,6 +74,13 @@ export class ConversationManager extends SdkConversationManager {
74
74
  private errorLineRegistry: number[] = [];
75
75
  /** Streaming block start line in history buffer (for incremental streaming update). */
76
76
  private streamingStartLine = -1;
77
+ /**
78
+ * Message index at the time of the last clearDisplay() call.
79
+ * rebuildHistory() renders only messages at or after this index, so the
80
+ * display stays blank for messages added before the clear while LLM history
81
+ * is fully preserved. Reset to 0 on resetAll() or rebuildHistory() width change.
82
+ */
83
+ private _displayFromMessageIndex = 0;
77
84
 
78
85
  public suppressSplash: boolean = false;
79
86
  public splashOptions: SplashOptions = {};
@@ -215,6 +222,7 @@ export class ConversationManager extends SdkConversationManager {
215
222
  this.messageLineRegistry = [];
216
223
  this.errorLineRegistry = [];
217
224
  this.streamingStartLine = -1;
225
+ this._displayFromMessageIndex = 0; // full reset — show everything on next render
218
226
  }
219
227
 
220
228
  /**
@@ -293,20 +301,27 @@ export class ConversationManager extends SdkConversationManager {
293
301
  this.lastRenderedWidth = width;
294
302
  this.dirty = false;
295
303
 
304
+ const snapshot = this.getMessageSnapshot();
305
+ // When _displayFromMessageIndex > 0, clearDisplay() was called. Only render
306
+ // messages added after the clear — the pre-clear history stays off-screen.
307
+ // On a full rebuild (e.g. width change), reset the display-start to 0 so the
308
+ // user can scroll back to the full history if needed.
309
+ const displayStart = this._displayFromMessageIndex;
310
+ const visibleSnapshot = displayStart > 0 ? snapshot.slice(displayStart) : snapshot;
311
+
296
312
  // Tool messages ARE rendered (as collapsed blocks); this filter is only
297
313
  // for determining whether to show the splash screen (tool-only messages
298
314
  // don't count as visible conversation content for splash purposes).
299
- const snapshot = this.getMessageSnapshot();
300
- const displayMessages = snapshot.filter(
315
+ const displayMessages = visibleSnapshot.filter(
301
316
  (m) => m.role !== 'tool' && m.role !== 'system',
302
317
  );
303
318
 
304
- if (displayMessages.length === 0 && !this.suppressSplash) {
319
+ if (displayMessages.length === 0 && displayStart === 0 && !this.suppressSplash) {
305
320
  this.addSplashScreen(width);
306
321
  return;
307
322
  }
308
323
 
309
- this.appendMessages(snapshot, width);
324
+ this.appendMessages(visibleSnapshot, width);
310
325
  this.appendedUpTo = snapshot.length;
311
326
  }
312
327
 
@@ -509,19 +524,27 @@ export class ConversationManager extends SdkConversationManager {
509
524
 
510
525
  /**
511
526
  * clearDisplay - Clear the visual history buffer without touching the LLM context messages.
512
- * The next render will show a blank conversation area.
527
+ * The next render will show a blank conversation area. Subsequent message additions
528
+ * rebuild the display incrementally from that point forward.
529
+ *
530
+ * Contract:
531
+ * - getDisplayBlocks() returns an empty array immediately after this call.
532
+ * - getMessageSnapshot() is unaffected — full LLM history is preserved.
533
+ * - resetAll() (which clears both display and messages) continues to work.
534
+ * - rebuildHistory() can be called by callers that need a full display rebuild.
513
535
  */
514
536
  public clearDisplay(): void {
515
537
  this.history.clear();
516
- this.appendedUpTo = 0;
517
- this.dirty = true;
518
- // Re-render from existing messages to rebuild buffer
519
- const width = this._getWidth();
520
- this.lastRenderedWidth = width;
538
+ this.blockRegistry = [];
539
+ this.messageLineRegistry = [];
540
+ this.errorLineRegistry = [];
541
+ // Advance _displayFromMessageIndex to exclude all current messages from display.
542
+ // rebuildHistory() will only render messages added AFTER this point.
543
+ this._displayFromMessageIndex = this.getMessageSnapshot().length;
544
+ this.appendedUpTo = this._displayFromMessageIndex;
521
545
  this.dirty = false;
522
- const snapshot = this.getMessageSnapshot();
523
- this.appendMessages(snapshot, width);
524
- this.appendedUpTo = snapshot.length;
546
+ // Do NOT re-render here — display stays blank until the next message is added.
547
+ // The lastRenderedWidth is kept so subsequent appends use the correct width.
525
548
  }
526
549
  }
527
550
 
package/src/daemon/cli.ts CHANGED
@@ -25,7 +25,15 @@ import {
25
25
  persistProviders,
26
26
  } from '@pellux/goodvibes-sdk/platform/discovery/index';
27
27
 
28
- import { parseCliFlags } from '../cli-flags.ts';
28
+ import {
29
+ parseGoodVibesCli,
30
+ renderGoodVibesDaemonHelp,
31
+ renderGoodVibesVersion,
32
+ applyRuntimeConfigOverrides,
33
+ applyRuntimeConfigValue,
34
+ applyRuntimeFeatureFlagOverrides,
35
+ applyRuntimeEndpointFlagOverrides,
36
+ } from '../cli/index.ts';
29
37
  type DaemonCliOwnership = {
30
38
  readonly workingDirectory: string;
31
39
  readonly homeDirectory: string;
@@ -39,13 +47,17 @@ type DaemonCliTokens = {
39
47
  };
40
48
 
41
49
  function getLocalNetworkIp(): string {
42
- const nets = networkInterfaces();
43
- for (const name of Object.keys(nets)) {
44
- for (const net of nets[name] ?? []) {
45
- if (net.family === 'IPv4' && !net.internal) {
46
- return net.address;
50
+ try {
51
+ const nets = networkInterfaces();
52
+ for (const name of Object.keys(nets)) {
53
+ for (const net of nets[name] ?? []) {
54
+ if (net.family === 'IPv4' && !net.internal) {
55
+ return net.address;
56
+ }
47
57
  }
48
58
  }
59
+ } catch {
60
+ return 'localhost';
49
61
  }
50
62
  return 'localhost';
51
63
  }
@@ -82,7 +94,22 @@ function readDaemonCliTokens(env: NodeJS.ProcessEnv): DaemonCliTokens {
82
94
  async function main(): Promise<void> {
83
95
  // Parse CLI flags first so --daemon-home and --working-dir env vars are set
84
96
  // before resolveDaemonCliOwnership() reads them.
85
- const cliFlags = parseCliFlags(process.argv.slice(2), 'goodvibes-daemon');
97
+ const cli = parseGoodVibesCli(process.argv.slice(2), 'goodvibes-daemon');
98
+ if (cli.errors.length > 0) {
99
+ console.error(cli.errors.join('\n'));
100
+ console.error('');
101
+ console.error(renderGoodVibesDaemonHelp('goodvibes-daemon'));
102
+ process.exit(2);
103
+ }
104
+ if (cli.flags.help || cli.command === 'help') {
105
+ console.log(renderGoodVibesDaemonHelp('goodvibes-daemon'));
106
+ process.exit(0);
107
+ }
108
+ if (cli.flags.version || cli.command === 'version') {
109
+ console.log(renderGoodVibesVersion('goodvibes-daemon'));
110
+ process.exit(0);
111
+ }
112
+ const cliFlags = cli.flags;
86
113
  if (cliFlags.daemonHome !== undefined) {
87
114
  process.env['GOODVIBES_DAEMON_HOME'] = cliFlags.daemonHome;
88
115
  logger.info('daemon: --daemon-home flag applied', { daemonHome: cliFlags.daemonHome });
@@ -96,15 +123,36 @@ async function main(): Promise<void> {
96
123
  const config = new ConfigManager({ workingDir, homeDir: homeDirectory, surfaceRoot: 'tui' });
97
124
  new GlobalNetworkTransportInstaller().install(config);
98
125
 
99
- // Apply remaining CLI flags — override settings.json before the provider registry is constructed
126
+ const overrideErrors = applyRuntimeConfigOverrides(config, cliFlags.configOverrides);
127
+ if (overrideErrors.length > 0) {
128
+ console.error(overrideErrors.join('\n'));
129
+ process.exit(2);
130
+ }
131
+ applyRuntimeFeatureFlagOverrides(config, {
132
+ enableFeatures: cliFlags.enableFeatures,
133
+ disableFeatures: cliFlags.disableFeatures,
134
+ });
135
+
136
+ // Apply remaining CLI flags before the provider registry is constructed.
137
+ // These are runtime-only overrides; they must not rewrite settings.json.
100
138
  if (cliFlags.provider !== undefined) {
101
- config.set('provider.provider', cliFlags.provider);
139
+ applyRuntimeConfigValue(config, 'provider.provider', cliFlags.provider);
102
140
  logger.info('daemon: --provider flag applied', { provider: cliFlags.provider });
103
141
  }
104
142
  if (cliFlags.model !== undefined) {
105
- config.set('provider.model', cliFlags.model);
143
+ applyRuntimeConfigValue(config, 'provider.model', cliFlags.model);
106
144
  logger.info('daemon: --model flag applied', { model: cliFlags.model });
107
145
  }
146
+ const endpointOverrideErrors = applyRuntimeEndpointFlagOverrides(config, 'controlPlane', cliFlags);
147
+ if (endpointOverrideErrors.length > 0) {
148
+ console.error(endpointOverrideErrors.join('\n'));
149
+ process.exit(2);
150
+ }
151
+ if (cliFlags.port !== undefined) logger.info('daemon: --port flag applied', { port: cliFlags.port });
152
+ if (cliFlags.hostname !== undefined) {
153
+ process.env['GOODVIBES_DAEMON_HOST'] = cliFlags.hostname;
154
+ logger.info('daemon: --hostname flag applied', { hostname: cliFlags.hostname });
155
+ }
108
156
  const runtimeBus = new RuntimeEventBus();
109
157
  const runtimeStore = createRuntimeStore();
110
158
  const runtimeServices = createRuntimeServices({
@@ -209,7 +257,10 @@ async function main(): Promise<void> {
209
257
  // Print companion connection info + QR code to stdout.
210
258
  // Use the config-driven control plane port, not a hardcoded default.
211
259
  const daemonPort = config.get('controlPlane.port');
212
- const daemonHost = String(process.env.GOODVIBES_DAEMON_HOST ?? getLocalNetworkIp());
260
+ const configuredDaemonHost = String(process.env.GOODVIBES_DAEMON_HOST ?? getLocalNetworkIp());
261
+ const daemonHost = configuredDaemonHost === '0.0.0.0' || configuredDaemonHost === '::'
262
+ ? getLocalNetworkIp()
263
+ : configuredDaemonHost;
213
264
  const daemonUrl = `http://${daemonHost}:${daemonPort}`;
214
265
  const bootstrapPassword = readBootstrapPassword(userAuth.getBootstrapCredentialPath());
215
266
  const connectionInfo = buildCompanionConnectionInfo({
@@ -9,6 +9,8 @@ import type { SelectionItem, SelectionResult, SelectionAction } from './selectio
9
9
  import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state/file-undo';
10
10
  import type { PanelManager } from '../panels/panel-manager.ts';
11
11
  import type { KeybindingsManager } from './keybindings.ts';
12
+ import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
13
+ import type { OpenOnboardingWizardOptions } from './handler-ui-state.ts';
12
14
  import type { KnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge/knowledge-api';
13
15
  import type { HookApi } from '@pellux/goodvibes-sdk/platform/hooks/hook-api';
14
16
  import type { McpApi } from '@pellux/goodvibes-sdk/platform/mcp/mcp-api';
@@ -72,6 +74,7 @@ export interface CommandUiActions {
72
74
 
73
75
  export interface CommandShellUiOpeners {
74
76
  reloadSystemPrompt?: () => string;
77
+ openOnboardingWizard?: (modeOrOptions?: OnboardingWizardMode | OpenOnboardingWizardOptions) => void;
75
78
  openModelPicker?: () => void;
76
79
  openProviderPicker?: () => void;
77
80
  openContextInspector?: () => void;
@@ -2,24 +2,29 @@ import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core/
2
2
  import { evaluateSessionMaintenance, formatSessionMaintenanceLines, getGuidanceMode } from '@pellux/goodvibes-sdk/platform/runtime/session-maintenance';
3
3
  import { dismissGuidance, evaluateContextualGuidance, formatGuidanceItems, resetGuidance } from '@pellux/goodvibes-sdk/platform/runtime/guidance';
4
4
  import type { CommandRegistry } from '../command-registry.ts';
5
- import { openCommandPanel, requireProviderApi, requireReadModels, requireSessionMemoryStore, requireShellPaths } from './runtime-services.ts';
5
+ import { requireProviderApi, requireReadModels, requireSessionMemoryStore, requireShellPaths } from './runtime-services.ts';
6
6
 
7
7
  export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void {
8
8
  registry.register({
9
9
  name: 'welcome',
10
10
  aliases: ['guide'],
11
- description: 'Open the guided start surface for setup, security, marketplace, remote, and operator workflows',
11
+ description: 'Open the product entry surface for the onboarding wizard, security, marketplace, remote, and operator workflows',
12
12
  usage: '[open|print]',
13
13
  handler(args, ctx) {
14
14
  const sub = args[0] ?? 'open';
15
15
  if (sub === 'open' || sub === 'panel') {
16
- openCommandPanel(ctx, 'welcome');
16
+ if (ctx.openOnboardingWizard) {
17
+ ctx.openOnboardingWizard({ mode: 'edit' });
18
+ return;
19
+ }
20
+ ctx.print('Use /onboarding to open the setup wizard.');
17
21
  return;
18
22
  }
19
23
  if (sub === 'print') {
20
24
  ctx.print([
21
25
  'Welcome To GoodVibes',
22
- ' /setup onboarding - first-run checklist and health flows',
26
+ ' /onboarding - open the onboarding wizard with current settings preloaded',
27
+ ' /setup onboarding - open the same onboarding wizard from setup workflows',
23
28
  ' /health review - unified startup, service, and sandbox posture',
24
29
  ' /sandbox review - inspect VM isolation posture',
25
30
  ' /marketplace open - browse curated plugins, skills, hook packs, and policy packs',
@@ -11,6 +11,16 @@ import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, r
11
11
  import { openCommandPanel, requireBookmarkManager, requireProviderApi, requireSecretsManager } from './runtime-services.ts';
12
12
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
13
13
 
14
+ function isGoodVibesSecretRefInput(value: string): boolean {
15
+ const normalized = value.trim();
16
+ return normalized.startsWith('goodvibes://secrets/') && isSecretRefInput(normalized);
17
+ }
18
+
19
+ function isMalformedGoodVibesSecretRefInput(value: string): boolean {
20
+ const normalized = value.trim();
21
+ return normalized.startsWith('goodvibes://') && !isGoodVibesSecretRefInput(normalized);
22
+ }
23
+
14
24
  function toggleBlocks(typeFilter: string, collapsed: boolean, ctx: CommandContext): void {
15
25
  const VALID_TYPES = ['all', 'thinking', 'tool', 'code'] as const;
16
26
  if (!VALID_TYPES.includes(typeFilter as typeof VALID_TYPES[number])) {
@@ -157,11 +167,11 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
157
167
  ...BUILTIN_SECRET_PROVIDER_SOURCES.map((source) => ` ${source}`),
158
168
  '',
159
169
  'Examples:',
160
- ' /secrets link OPENAI_API_KEY secret://env/OPENAI_API_KEY',
161
- ' /secrets link SLACK_BOT_TOKEN bw://GoodVibes%20Slack/password?sessionEnv=BW_SESSION',
162
- ' /secrets link SLACK_BOT_TOKEN vaultwarden://GoodVibes%20Slack/password?server=https%3A%2F%2Fvault.example.test',
163
- ' /secrets link STRIPE_TOKEN bws://00000000-0000-0000-0000-000000000000/value?accessTokenEnv=BWS_ACCESS_TOKEN',
164
- ' /secrets link OPENAI_API_KEY op://Private/GoodVibes%20OpenAI/API%20Key',
170
+ ' /secrets link OPENAI_API_KEY goodvibes://secrets/env/OPENAI_API_KEY',
171
+ ' /secrets link SLACK_BOT_TOKEN goodvibes://secrets/bitwarden?item=GoodVibes%20Slack&field=password&sessionEnv=BW_SESSION',
172
+ ' /secrets link SLACK_BOT_TOKEN goodvibes://secrets/vaultwarden?item=GoodVibes%20Slack&field=password&server=https%3A%2F%2Fvault.example.test',
173
+ ' /secrets link STRIPE_TOKEN goodvibes://secrets/bws/00000000-0000-0000-0000-000000000000?field=value&accessTokenEnv=BWS_ACCESS_TOKEN',
174
+ ' /secrets link OPENAI_API_KEY goodvibes://secrets/1password?vault=Private&item=GoodVibes%20OpenAI&field=API%20Key',
165
175
  ].join('\n'));
166
176
  return;
167
177
  }
@@ -171,7 +181,7 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
171
181
  ctx.print('[secrets] Usage: /secrets test <secret-ref>');
172
182
  return;
173
183
  }
174
- if (!isSecretRefInput(refText)) {
184
+ if (!isGoodVibesSecretRefInput(refText)) {
175
185
  ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
176
186
  return;
177
187
  }
@@ -192,7 +202,11 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
192
202
  return;
193
203
  }
194
204
  const value = rawValueParts.join(' ');
195
- if (sub === 'link' && !isSecretRefInput(value)) {
205
+ if (sub === 'link' && !isGoodVibesSecretRefInput(value)) {
206
+ ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
207
+ return;
208
+ }
209
+ if (sub === 'set' && isMalformedGoodVibesSecretRefInput(value)) {
196
210
  ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
197
211
  return;
198
212
  }
@@ -1,10 +1,9 @@
1
- import { dirname, join, resolve } from 'path';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
- import type { CommandRegistry, CommandContext } from '../command-registry.ts';
1
+ import { dirname, join } from 'path';
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import type { CommandRegistry } from '../command-registry.ts';
4
4
  import type { ConfigKey } from '../../config/index.ts';
5
5
  import { CONFIG_SCHEMA } from '../../config/index.ts';
6
6
  import { listHookPointContracts } from '@pellux/goodvibes-sdk/platform/hooks/index';
7
- import { isRunningInWsl } from '@pellux/goodvibes-sdk/platform/runtime/sandbox/manager';
8
7
  import { renderQemuWrapperTemplate } from '@pellux/goodvibes-sdk/platform/runtime/sandbox/qemu-wrapper-template';
9
8
  import type { SetupTransferBundle } from './local-setup-transfer.ts';
10
9
  import {
@@ -15,20 +14,29 @@ import {
15
14
  parseSetupLink,
16
15
  } from './local-setup-transfer.ts';
17
16
  import { buildSetupReviewSnapshot, exportSetupSupportBundle, renderSetupSandboxReview } from './local-setup-review.ts';
18
- import { requirePanelManager, requireShellPaths } from './runtime-services.ts';
17
+ import { openOnboardingWizard, requirePanelManager, requireShellPaths } from './runtime-services.ts';
19
18
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
20
19
 
20
+ type SetupSnapshot = Awaited<ReturnType<typeof buildSetupReviewSnapshot>>;
21
+
21
22
  export function registerLocalSetupCommands(registry: CommandRegistry): void {
22
23
  registry.register({
23
24
  name: 'setup',
24
25
  aliases: ['startup'],
25
- description: 'Review startup readiness, ecosystem posture, sandbox bring-up, and service configuration',
26
+ description: 'Launch the onboarding wizard and review startup readiness, service posture, and sandbox bring-up',
26
27
  usage: '[review|doctor|services|hooks|remote|sandbox|onboarding|support-bundle <dir>|export <path>|transfer <export|inspect|import> <path>|link <surface> [target]|open-link <uri>]',
27
28
  async handler(args, ctx) {
28
- const shellPaths = requireShellPaths(ctx);
29
29
  const sub = args[0] ?? 'review';
30
- const snapshot = await buildSetupReviewSnapshot(ctx);
30
+ let shellPaths: ReturnType<typeof requireShellPaths> | null = null;
31
+ let snapshotPromise: Promise<SetupSnapshot> | null = null;
32
+ const getShellPaths = () => (shellPaths ??= requireShellPaths(ctx));
33
+ const getSnapshot = async (): Promise<SetupSnapshot> => {
34
+ snapshotPromise ??= buildSetupReviewSnapshot(ctx);
35
+ return snapshotPromise;
36
+ };
37
+
31
38
  if (sub === 'review') {
39
+ const snapshot = await getSnapshot();
32
40
  ctx.print([
33
41
  'Startup Readiness Review',
34
42
  ` session: ${snapshot.sessionId}`,
@@ -58,6 +66,7 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
58
66
  }
59
67
 
60
68
  if (sub === 'doctor') {
69
+ const snapshot = await getSnapshot();
61
70
  ctx.print([
62
71
  'Startup Doctor',
63
72
  ...snapshot.issues.map((issue) => ` [${issue.severity.toUpperCase()}] ${issue.area}: ${issue.message}`),
@@ -75,6 +84,7 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
75
84
  }
76
85
 
77
86
  if (sub === 'services') {
87
+ const snapshot = await getSnapshot();
78
88
  ctx.print([
79
89
  'Startup Services',
80
90
  ` configured: ${snapshot.serviceCount}`,
@@ -91,6 +101,7 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
91
101
  }
92
102
 
93
103
  if (sub === 'hooks') {
104
+ const snapshot = await getSnapshot();
94
105
  const contracts = listHookPointContracts();
95
106
  ctx.print([
96
107
  'Startup Hooks',
@@ -102,6 +113,7 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
102
113
  }
103
114
 
104
115
  if (sub === 'remote') {
116
+ const snapshot = await getSnapshot();
105
117
  const runners = ctx.ops.remoteRuntime?.listContracts() ?? [];
106
118
  ctx.print([
107
119
  'Startup Remote',
@@ -112,40 +124,19 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
112
124
  }
113
125
 
114
126
  if (sub === 'sandbox') {
127
+ const snapshot = await getSnapshot();
115
128
  ctx.print(renderSetupSandboxReview(ctx, snapshot));
116
129
  return;
117
130
  }
118
131
 
119
132
  if (sub === 'onboarding') {
120
- ctx.print([
121
- 'Onboarding Checklist',
122
- ` providers: ${snapshot.providerCount > 0 ? '[ready]' : '[needs setup]'}`,
123
- ` services: ${(snapshot.serviceCount > 0 || snapshot.oauthProviderCount > 0 || snapshot.builtinSubscriptionProviderCount > 0) ? '[ready]' : '[optional]'}`,
124
- ` subscriptions: ${snapshot.activeSubscriptionCount > 0 ? '[ready]' : (snapshot.oauthProviderCount + snapshot.builtinSubscriptionProviderCount) > 0 ? '[available]' : '[optional]'}`,
125
- ` hooks: ${(snapshot.managedHookCount + snapshot.managedHookChainCount) > 0 ? '[ready]' : '[optional]'}`,
126
- ` remote: ${snapshot.remoteRunnerCount > 0 ? '[ready]' : '[optional]'}`,
127
- ` sandbox: ${`${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'local' ? '[local default]' : (snapshot.sandboxSecureModeReady ? '[qemu ready]' : '[host blocked]')}`,
128
- ` plugins: ${snapshot.pluginCount > 0 ? '[ready]' : '[optional]'}`,
129
- ` skills: ${snapshot.skillCount > 0 ? '[ready]' : '[optional]'}`,
130
- '',
131
- 'Recommended next commands:',
132
- ' /health review',
133
- ' /provider',
134
- ' /services doctor',
135
- ' /subscription review',
136
- ' /hooks scaffold <name> <match> <type>',
137
- ' /setup sandbox',
138
- ' /sandbox recommend',
139
- ' /sandbox qemu bootstrap .goodvibes/tui/sandbox 20',
140
- ...(process.platform === 'win32' && !isRunningInWsl() ? [' Run GoodVibes inside WSL before enabling QEMU sandboxing'] : []),
141
- ' /remote setup',
142
- ' /plugin browse',
143
- ' /skills browse',
144
- ].join('\n'));
133
+ openOnboardingWizard(ctx, { mode: 'edit', reset: true });
134
+ ctx.print('Opening onboarding wizard.');
145
135
  return;
146
136
  }
147
137
 
148
138
  if (sub === 'support-bundle') {
139
+ const snapshot = await getSnapshot();
149
140
  const dirArg = args[1];
150
141
  if (!dirArg) {
151
142
  ctx.print('Usage: /setup support-bundle <dir>');
@@ -167,12 +158,13 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
167
158
  }
168
159
 
169
160
  if (sub === 'export') {
161
+ const snapshot = await getSnapshot();
170
162
  const pathArg = args[1];
171
163
  if (!pathArg) {
172
164
  ctx.print('Usage: /setup export <path>');
173
165
  return;
174
166
  }
175
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
167
+ const targetPath = getShellPaths().resolveWorkspacePath(pathArg);
176
168
  mkdirSync(dirname(targetPath), { recursive: true });
177
169
  writeFileSync(targetPath, JSON.stringify(snapshot, null, 2) + '\n', 'utf-8');
178
170
  ctx.print(`Exported startup review to ${targetPath}`);
@@ -186,8 +178,9 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
186
178
  ctx.print('Usage: /setup transfer <export|inspect|import> <path>');
187
179
  return;
188
180
  }
189
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
181
+ const targetPath = getShellPaths().resolveWorkspacePath(pathArg);
190
182
  if (mode === 'export') {
183
+ const snapshot = await getSnapshot();
191
184
  const bundle = buildSetupTransferBundle(ctx, snapshot);
192
185
  ctx.print(`Exported setup transfer bundle to ${exportSetupTransferBundle(ctx, pathArg, bundle)}`);
193
186
  return;
@@ -210,17 +203,17 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
210
203
  }
211
204
  }
212
205
  if (bundle.services) {
213
- const servicesPath = shellPaths.resolveProjectPath('tui', 'services.json');
206
+ const servicesPath = getShellPaths().resolveProjectPath('tui', 'services.json');
214
207
  mkdirSync(dirname(servicesPath), { recursive: true });
215
208
  writeFileSync(servicesPath, JSON.stringify(bundle.services, null, 2) + '\n', 'utf-8');
216
209
  }
217
210
  if (bundle.ecosystem?.plugins) {
218
- const pluginsPath = shellPaths.resolveProjectPath('tui', 'ecosystem', 'plugins.json');
211
+ const pluginsPath = getShellPaths().resolveProjectPath('tui', 'ecosystem', 'plugins.json');
219
212
  mkdirSync(dirname(pluginsPath), { recursive: true });
220
213
  writeFileSync(pluginsPath, JSON.stringify(bundle.ecosystem.plugins, null, 2) + '\n', 'utf-8');
221
214
  }
222
215
  if (bundle.ecosystem?.skills) {
223
- const skillsPath = shellPaths.resolveProjectPath('tui', 'ecosystem', 'skills.json');
216
+ const skillsPath = getShellPaths().resolveProjectPath('tui', 'ecosystem', 'skills.json');
224
217
  mkdirSync(dirname(skillsPath), { recursive: true });
225
218
  writeFileSync(skillsPath, JSON.stringify(bundle.ecosystem.skills, null, 2) + '\n', 'utf-8');
226
219
  }
@@ -0,0 +1,14 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+ import { openOnboardingWizard } from './runtime-services.ts';
3
+
4
+ export function registerOnboardingRuntimeCommands(registry: CommandRegistry): void {
5
+ registry.register({
6
+ name: 'onboarding',
7
+ description: 'Open the onboarding wizard with current settings preloaded for review and editing',
8
+ usage: '',
9
+ handler(_args, ctx) {
10
+ openOnboardingWizard(ctx, { mode: 'edit', reset: true });
11
+ ctx.print('Opening onboarding wizard.');
12
+ },
13
+ });
14
+ }
@@ -99,6 +99,15 @@ export function openCommandPanel(
99
99
  showPanel(panelId, pane);
100
100
  }
101
101
 
102
+ export function openOnboardingWizard(
103
+ context: Pick<CommandContext, 'openOnboardingWizard'>,
104
+ modeOrOptions?: import('../onboarding/onboarding-wizard.ts').OnboardingWizardMode
105
+ | import('../handler-ui-state.ts').OpenOnboardingWizardOptions,
106
+ ): void {
107
+ const openWizard = requireContextValue(context.openOnboardingWizard, 'openOnboardingWizard');
108
+ openWizard(modeOrOptions);
109
+ }
110
+
102
111
  export function requireKeybindingsManager(context: CommandContext) {
103
112
  return requireContextValue(context.workspace.keybindingsManager, 'workspace.keybindingsManager');
104
113
  }
@@ -53,6 +53,7 @@ import { registerLocalAuthRuntimeCommands } from './commands/local-auth-runtime.
53
53
  import { registerIntelligenceRuntimeCommands } from './commands/intelligence-runtime.ts';
54
54
  import { registerConversationRuntimeCommands } from './commands/conversation-runtime.ts';
55
55
  import { registerQrcodeRuntimeCommands } from './commands/qrcode-runtime.ts';
56
+ import { registerOnboardingRuntimeCommands } from './commands/onboarding-runtime.ts';
56
57
 
57
58
  /**
58
59
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -100,6 +101,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
100
101
  registerIntelligenceRuntimeCommands(registry);
101
102
  registerConversationRuntimeCommands(registry);
102
103
  registerQrcodeRuntimeCommands(registry);
104
+ registerOnboardingRuntimeCommands(registry);
103
105
  registerLocalRuntimeCommands(registry);
104
106
  registerSessionWorkflowCommands(registry);
105
107
  registerDiscoveryRuntimeCommands(registry);
@@ -31,10 +31,12 @@ import type { BookmarkModal } from './bookmark-modal.ts';
31
31
  import type { SettingsModal } from './settings-modal.ts';
32
32
  import type { SessionPickerModal } from './session-picker-modal.ts';
33
33
  import type { ProfilePickerModal } from './profile-picker-modal.ts';
34
+ import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
34
35
  import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
35
36
  import type { Panel } from '../panels/types.ts';
36
37
  import type { PanelManager } from '../panels/panel-manager.ts';
37
38
  import type { KeybindingsManager } from './keybindings.ts';
39
+ import type { ModelPickerTarget } from './model-picker.ts';
38
40
 
39
41
  /**
40
42
  * Initial mutable scalar values for InputFeedContext.
@@ -77,7 +79,7 @@ export interface FeedContextMutableInit {
77
79
  * `profilePickerModal` — modal objects constructed once
78
80
  * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`,
79
81
  * `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`,
80
- * `searchManager`, `historySearch` — service objects constructed once
82
+ * `searchManager`, `historySearch`, `onboardingWizard` — service objects constructed once
81
83
  * - `panelManager`, `keybindingsManager` — from uiServices, stable
82
84
  * - `modalStack` — reference to the handler's shared array
83
85
  * - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — callbacks
@@ -104,6 +106,7 @@ export interface FeedContextStableRefs {
104
106
  autocomplete: AutocompleteEngine | null;
105
107
  filePicker: FilePickerModal;
106
108
  modelPicker: ModelPickerModal;
109
+ onboardingWizard: OnboardingWizardController;
107
110
  processModal: ProcessModal;
108
111
  liveTailModal: LiveTailModal;
109
112
  agentDetailModal: AgentDetailModal;
@@ -148,6 +151,9 @@ export interface FeedContextClosures {
148
151
  findMarkerAtPos: (pos: number) => { start: number; end: number } | null;
149
152
  cleanupMarkerRegistry: (text: string) => void;
150
153
  expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
154
+ openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
155
+ onModelPickerCommit: () => boolean;
156
+ onOnboardingAction: (action: import('./onboarding/onboarding-wizard.ts').OnboardingWizardAction) => void;
151
157
  }
152
158
 
153
159
  /**
@@ -233,4 +239,5 @@ export function syncFeedContextMutableFields(
233
239
  ctx.nextImageId = fields.nextImageId;
234
240
  ctx.mouseDownRow = fields.mouseDownRow;
235
241
  ctx.mouseDownCol = fields.mouseDownCol;
242
+ ctx.contentWidth = fields.contentWidth;
236
243
  }