@pellux/goodvibes-tui 0.19.24 → 0.19.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/bin/goodvibes +10 -0
  4. package/bin/goodvibes-daemon +10 -0
  5. package/docs/foundation-artifacts/operator-contract.json +1 -1
  6. package/package.json +3 -2
  7. package/src/cli/bundle-command.ts +225 -0
  8. package/src/cli/completion.ts +90 -0
  9. package/src/cli/config-overrides.ts +159 -0
  10. package/src/cli/endpoints.ts +63 -0
  11. package/src/cli/entrypoint.ts +169 -0
  12. package/src/cli/help.ts +301 -0
  13. package/src/cli/index.ts +11 -0
  14. package/src/cli/management-commands.ts +426 -0
  15. package/src/cli/management.ts +719 -0
  16. package/src/cli/network-posture.ts +46 -0
  17. package/src/cli/package-verification.ts +119 -0
  18. package/src/cli/parser.ts +369 -0
  19. package/src/cli/provider-classification.ts +107 -0
  20. package/src/cli/redaction.ts +105 -0
  21. package/src/cli/service-command.ts +45 -0
  22. package/src/cli/service-posture.ts +247 -0
  23. package/src/cli/status.ts +382 -0
  24. package/src/cli/surface-command.ts +248 -0
  25. package/src/cli/tui-startup.ts +32 -0
  26. package/src/cli/types.ts +69 -0
  27. package/src/cli-flags.ts +18 -55
  28. package/src/config/index.ts +1 -1
  29. package/src/config/secrets.ts +44 -0
  30. package/src/daemon/cli.ts +62 -11
  31. package/src/input/command-registry.ts +3 -0
  32. package/src/input/commands/guidance-runtime.ts +9 -4
  33. package/src/input/commands/local-runtime.ts +21 -7
  34. package/src/input/commands/local-setup.ts +31 -38
  35. package/src/input/commands/onboarding-runtime.ts +14 -0
  36. package/src/input/commands/runtime-services.ts +9 -0
  37. package/src/input/commands.ts +2 -0
  38. package/src/input/feed-context-factory.ts +8 -1
  39. package/src/input/handler-feed.ts +13 -8
  40. package/src/input/handler-interactions.ts +266 -0
  41. package/src/input/handler-modal-stack.ts +23 -3
  42. package/src/input/handler-modal-token-routes.ts +23 -1
  43. package/src/input/handler-onboarding.ts +696 -0
  44. package/src/input/handler-picker-routes.ts +15 -7
  45. package/src/input/handler-ui-state.ts +58 -0
  46. package/src/input/handler.ts +120 -246
  47. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  48. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  49. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  50. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  51. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  52. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  53. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  54. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  55. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  56. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  57. package/src/main.ts +32 -39
  58. package/src/panels/builtin/operations.ts +0 -10
  59. package/src/panels/index.ts +0 -1
  60. package/src/renderer/conversation-overlays.ts +6 -0
  61. package/src/renderer/help-overlay.ts +1 -1
  62. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  63. package/src/runtime/bootstrap-core.ts +1 -0
  64. package/src/runtime/bootstrap.ts +123 -0
  65. package/src/runtime/onboarding/apply.ts +685 -0
  66. package/src/runtime/onboarding/derivation.ts +495 -0
  67. package/src/runtime/onboarding/index.ts +7 -0
  68. package/src/runtime/onboarding/markers.ts +161 -0
  69. package/src/runtime/onboarding/snapshot.ts +400 -0
  70. package/src/runtime/onboarding/state.ts +140 -0
  71. package/src/runtime/onboarding/types.ts +402 -0
  72. package/src/runtime/onboarding/verify.ts +233 -0
  73. package/src/runtime/ui-services.ts +16 -0
  74. package/src/shell/ui-openers.ts +12 -2
  75. package/src/version.ts +1 -1
  76. package/src/panels/welcome-panel.ts +0 -64
@@ -0,0 +1,69 @@
1
+ export type GoodVibesCliCommand =
2
+ | 'tui'
3
+ | 'run'
4
+ | 'serve'
5
+ | 'web'
6
+ | 'service'
7
+ | 'status'
8
+ | 'doctor'
9
+ | 'onboarding'
10
+ | 'models'
11
+ | 'providers'
12
+ | 'auth'
13
+ | 'subscription'
14
+ | 'secrets'
15
+ | 'sessions'
16
+ | 'tasks'
17
+ | 'pair'
18
+ | 'surfaces'
19
+ | 'listener'
20
+ | 'control-plane'
21
+ | 'bundle'
22
+ | 'remote'
23
+ | 'bridge'
24
+ | 'completion'
25
+ | 'help'
26
+ | 'version'
27
+ | 'unknown';
28
+
29
+ export type GoodVibesCliOutputFormat = 'text' | 'json' | 'stream-json';
30
+
31
+ export interface CliCommandOutput {
32
+ readonly output: string;
33
+ readonly exitCode: number;
34
+ }
35
+
36
+ export interface GoodVibesCliFlags {
37
+ readonly provider: string | undefined;
38
+ readonly model: string | undefined;
39
+ readonly daemonHome: string | undefined;
40
+ readonly workingDir: string | undefined;
41
+ readonly help: boolean;
42
+ readonly version: boolean;
43
+ readonly prompt: string | undefined;
44
+ readonly print: boolean;
45
+ readonly outputFormat: GoodVibesCliOutputFormat;
46
+ readonly configOverrides: readonly string[];
47
+ readonly enableFeatures: readonly string[];
48
+ readonly disableFeatures: readonly string[];
49
+ readonly noAltScreen: boolean;
50
+ readonly port: number | undefined;
51
+ readonly hostname: string | undefined;
52
+ readonly open: boolean;
53
+ readonly continueLast: boolean;
54
+ readonly resume: string | undefined;
55
+ readonly session: string | undefined;
56
+ readonly fork: boolean;
57
+ readonly rawOutput: boolean;
58
+ readonly acceptRawOutputRisk: boolean;
59
+ }
60
+
61
+ export interface GoodVibesCliParseResult {
62
+ readonly binary: string;
63
+ readonly command: GoodVibesCliCommand;
64
+ readonly rawCommand: string | undefined;
65
+ readonly commandArgs: readonly string[];
66
+ readonly positionals: readonly string[];
67
+ readonly flags: GoodVibesCliFlags;
68
+ readonly errors: readonly string[];
69
+ }
package/src/cli-flags.ts CHANGED
@@ -1,58 +1,21 @@
1
- // ---------------------------------------------------------------------------
2
- // Shared CLI flag parsing for TUI shell and daemon entrypoints.
3
- // ---------------------------------------------------------------------------
1
+ // Compatibility wrapper for older imports. New CLI code lives in src/cli.
2
+ export type { GoodVibesCliFlags as CliFlags } from './cli/types.ts';
3
+ export {
4
+ applyRuntimeConfigOverrides,
5
+ applyRuntimeConfigValue,
6
+ applyRuntimeCommandEndpointFlagOverrides,
7
+ applyRuntimeEndpointFlagOverrides,
8
+ applyRuntimeFeatureFlagOverrides,
9
+ handleGoodVibesCliCommand,
10
+ parseGoodVibesCli,
11
+ renderGoodVibesCommandHelp,
12
+ renderGoodVibesHelp,
13
+ renderGoodVibesVersion,
14
+ } from './cli/index.ts';
4
15
 
5
- export type CliFlags = {
6
- readonly provider: string | undefined;
7
- readonly model: string | undefined;
8
- readonly daemonHome: string | undefined;
9
- readonly workingDir: string | undefined;
10
- };
16
+ import { parseGoodVibesCli } from './cli/parser.ts';
17
+ import type { GoodVibesCliFlags } from './cli/types.ts';
11
18
 
12
- /**
13
- * Parse `--provider` / `--model` / `--daemon-home` / `--working-dir` / `--help` flags from an argv slice.
14
- *
15
- * @param argv - argv array (pass `process.argv.slice(2)`)
16
- * @param binary - binary name shown in the --help usage line (e.g. "goodvibes" or "goodvibes-daemon")
17
- */
18
- export function parseCliFlags(argv: readonly string[], binary = 'goodvibes'): CliFlags {
19
- let provider: string | undefined;
20
- let model: string | undefined;
21
- let daemonHome: string | undefined;
22
- let workingDir: string | undefined;
23
-
24
- for (let i = 0; i < argv.length; i++) {
25
- const arg = argv[i];
26
- if (arg === '--help' || arg === '-h') {
27
- // eslint-disable-next-line no-console
28
- console.log([
29
- `Usage: ${binary} [options]`,
30
- '',
31
- 'Options:',
32
- ' --provider <id> Override the provider from settings.json at startup',
33
- ' --model <registryKey> Override the model from settings.json at startup',
34
- ' Format: provider:modelId (e.g. inception:mercury-2)',
35
- ' If provider:modelId format is used, --provider is inferred',
36
- ' --daemon-home=<path> Override daemon home (precedence: flag > GOODVIBES_DAEMON_HOME env > ~/.goodvibes/daemon)',
37
- ' --working-dir=<path> Override working directory (precedence: flag > GOODVIBES_WORKING_DIR env > <cwd>)',
38
- ' --help, -h Show this help message',
39
- ].join('\n'));
40
- process.exit(0);
41
- }
42
- if (arg === '--provider' && argv[i + 1] !== undefined) {
43
- provider = argv[++i];
44
- } else if (arg === '--model' && argv[i + 1] !== undefined) {
45
- model = argv[++i];
46
- // Infer provider from registryKey format (provider:modelId) if --provider not given
47
- if (typeof model === 'string' && model.includes(':') && provider === undefined) {
48
- provider = model.split(':')[0];
49
- }
50
- } else if (arg.startsWith('--daemon-home=')) {
51
- daemonHome = arg.slice('--daemon-home='.length);
52
- } else if (arg.startsWith('--working-dir=')) {
53
- workingDir = arg.slice('--working-dir='.length);
54
- }
55
- }
56
-
57
- return { provider, model, daemonHome, workingDir };
19
+ export function parseCliFlags(argv: readonly string[], binary = 'goodvibes'): GoodVibesCliFlags {
20
+ return parseGoodVibesCli(argv, binary).flags;
58
21
  }
@@ -8,7 +8,7 @@
8
8
 
9
9
  export { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
10
10
  export type { DeepReadonly } from '@pellux/goodvibes-sdk/platform/config/manager';
11
- export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig } from '@pellux/goodvibes-sdk/platform/config/schema';
11
+ export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig, PersistedFlagState } from '@pellux/goodvibes-sdk/platform/config/schema';
12
12
  export { DEFAULT_CONFIG, CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config/schema';
13
13
  export { ConfigError } from '@pellux/goodvibes-sdk/platform/types/errors';
14
14
 
@@ -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
  }