@pellux/goodvibes-tui 0.19.24 → 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 (68) hide show
  1. package/CHANGELOG.md +5 -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/daemon/cli.ts +62 -11
  23. package/src/input/command-registry.ts +3 -0
  24. package/src/input/commands/guidance-runtime.ts +9 -4
  25. package/src/input/commands/local-runtime.ts +21 -7
  26. package/src/input/commands/local-setup.ts +31 -38
  27. package/src/input/commands/onboarding-runtime.ts +14 -0
  28. package/src/input/commands/runtime-services.ts +9 -0
  29. package/src/input/commands.ts +2 -0
  30. package/src/input/feed-context-factory.ts +8 -1
  31. package/src/input/handler-feed.ts +13 -8
  32. package/src/input/handler-interactions.ts +266 -0
  33. package/src/input/handler-modal-stack.ts +23 -3
  34. package/src/input/handler-modal-token-routes.ts +23 -1
  35. package/src/input/handler-onboarding.ts +696 -0
  36. package/src/input/handler-picker-routes.ts +15 -7
  37. package/src/input/handler-ui-state.ts +58 -0
  38. package/src/input/handler.ts +120 -246
  39. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  40. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  41. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  42. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  43. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  44. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  45. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  46. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  47. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  48. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  49. package/src/main.ts +32 -39
  50. package/src/panels/builtin/operations.ts +0 -10
  51. package/src/panels/index.ts +0 -1
  52. package/src/renderer/conversation-overlays.ts +6 -0
  53. package/src/renderer/help-overlay.ts +1 -1
  54. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  55. package/src/runtime/bootstrap-core.ts +1 -0
  56. package/src/runtime/bootstrap.ts +123 -0
  57. package/src/runtime/onboarding/apply.ts +685 -0
  58. package/src/runtime/onboarding/derivation.ts +495 -0
  59. package/src/runtime/onboarding/index.ts +7 -0
  60. package/src/runtime/onboarding/markers.ts +161 -0
  61. package/src/runtime/onboarding/snapshot.ts +400 -0
  62. package/src/runtime/onboarding/state.ts +140 -0
  63. package/src/runtime/onboarding/types.ts +402 -0
  64. package/src/runtime/onboarding/verify.ts +233 -0
  65. package/src/runtime/ui-services.ts +16 -0
  66. package/src/shell/ui-openers.ts +12 -2
  67. package/src/version.ts +1 -1
  68. 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
  }
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
  }
@@ -18,6 +18,8 @@ import { BookmarkModal } from './bookmark-modal.ts';
18
18
  import { SettingsModal } from './settings-modal.ts';
19
19
  import { SessionPickerModal } from './session-picker-modal.ts';
20
20
  import { ProfilePickerModal } from './profile-picker-modal.ts';
21
+ import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
22
+ import type { OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
21
23
  import {
22
24
  IMAGE_EXTENSIONS,
23
25
  formatFileSize,
@@ -39,6 +41,7 @@ import { handlePanelIntegrationAction } from './panel-integration-actions.ts';
39
41
  import { SelectionManager } from './selection.ts';
40
42
  import type { PanelManager } from '../panels/panel-manager.ts';
41
43
  import type { KeybindingsManager } from './keybindings.ts';
44
+ import type { ModelPickerTarget } from './model-picker.ts';
42
45
 
43
46
  /**
44
47
  * InputFeedContext — The single long-lived context object passed to feedInputTokens
@@ -66,8 +69,8 @@ import type { KeybindingsManager } from './keybindings.ts';
66
69
  * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
67
70
  * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
68
71
  * `profilePickerModal` — modal objects constructed once in InputHandler constructor
69
- * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`, `agentDetailModal`,
70
- * `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
72
+ * - `filePicker`, `modelPicker`, `onboardingWizard`, `processModal`, `liveTailModal`,
73
+ * `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
71
74
  * service objects constructed once
72
75
  * - `panelManager`, `keybindingsManager` — from uiServices, stable for app lifetime
73
76
  * - `modalStack` — reference to the handler's shared array (mutated in place)
@@ -108,6 +111,7 @@ export interface InputFeedContext {
108
111
  autocomplete: AutocompleteEngine | null;
109
112
  readonly filePicker: FilePickerModal;
110
113
  readonly modelPicker: ModelPickerModal;
114
+ readonly onboardingWizard: OnboardingWizardController;
111
115
  readonly processModal: ProcessModal;
112
116
  readonly liveTailModal: LiveTailModal;
113
117
  readonly agentDetailModal: AgentDetailModal;
@@ -148,6 +152,9 @@ export interface InputFeedContext {
148
152
  readonly findMarkerAtPos: (pos: number) => { start: number; end: number } | null;
149
153
  readonly cleanupMarkerRegistry: (text: string) => void;
150
154
  readonly expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
155
+ readonly openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
156
+ readonly onModelPickerCommit: () => boolean;
157
+ readonly onOnboardingAction: (action: OnboardingWizardAction) => void;
151
158
  readonly exitApp: () => void;
152
159
  }
153
160
 
@@ -173,6 +180,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
173
180
  settingsModal: context.settingsModal,
174
181
  sessionPickerModal: context.sessionPickerModal,
175
182
  profilePickerModal: context.profilePickerModal,
183
+ onboardingWizard: context.onboardingWizard,
176
184
  helpOverlayActive: context.helpOverlayActive,
177
185
  helpScrollOffset: context.helpScrollOffset,
178
186
  shortcutsOverlayActive: context.shortcutsOverlayActive,
@@ -204,12 +212,9 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
204
212
  searchManager: context.searchManager,
205
213
  scroll: context.scroll,
206
214
  getScrollTop: context.getScrollTop,
207
- openModelPickerWithTarget: context.commandContext?.openModelPicker
208
- ? (target: import('./model-picker.ts').ModelPickerTarget) => {
209
- context.modelPicker.target = target;
210
- context.commandContext!.openModelPicker!();
211
- }
212
- : undefined,
215
+ openModelPickerWithTarget: context.openModelPickerWithTarget,
216
+ onModelPickerCommit: context.onModelPickerCommit,
217
+ onOnboardingAction: context.onOnboardingAction,
213
218
  }, token);
214
219
  context.selectionCallback = modalRoute.selectionCallback;
215
220
  context.helpOverlayActive = modalRoute.helpOverlayActive;