happy-stacks 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +93 -40
  2. package/bin/happys.mjs +158 -16
  3. package/docs/codex-mcp-resume.md +130 -0
  4. package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
  5. package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
  6. package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
  7. package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
  8. package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
  9. package/docs/happy-development.md +3 -4
  10. package/docs/isolated-linux-vm.md +82 -0
  11. package/docs/mobile-ios.md +112 -54
  12. package/docs/monorepo-migration.md +286 -0
  13. package/docs/server-flavors.md +19 -3
  14. package/docs/stacks.md +35 -0
  15. package/package.json +5 -1
  16. package/scripts/auth.mjs +32 -10
  17. package/scripts/build.mjs +55 -8
  18. package/scripts/daemon.mjs +166 -10
  19. package/scripts/dev.mjs +198 -50
  20. package/scripts/doctor.mjs +0 -4
  21. package/scripts/edison.mjs +6 -4
  22. package/scripts/env.mjs +150 -0
  23. package/scripts/env_cmd.test.mjs +128 -0
  24. package/scripts/init.mjs +8 -3
  25. package/scripts/install.mjs +207 -69
  26. package/scripts/lint.mjs +24 -4
  27. package/scripts/migrate.mjs +3 -12
  28. package/scripts/mobile.mjs +88 -104
  29. package/scripts/mobile_dev_client.mjs +83 -0
  30. package/scripts/monorepo.mjs +1096 -0
  31. package/scripts/monorepo_port.test.mjs +1470 -0
  32. package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
  33. package/scripts/review.mjs +908 -0
  34. package/scripts/review_pr.mjs +353 -0
  35. package/scripts/run.mjs +101 -21
  36. package/scripts/service.mjs +2 -2
  37. package/scripts/setup.mjs +189 -68
  38. package/scripts/setup_pr.mjs +586 -38
  39. package/scripts/stack.mjs +990 -196
  40. package/scripts/stack_archive_cmd.test.mjs +91 -0
  41. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
  42. package/scripts/stack_env_cmd.test.mjs +87 -0
  43. package/scripts/stack_happy_cmd.test.mjs +126 -0
  44. package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
  45. package/scripts/stack_monorepo_defaults.test.mjs +62 -0
  46. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
  47. package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
  48. package/scripts/stack_shorthand_cmd.test.mjs +55 -0
  49. package/scripts/stack_wt_list.test.mjs +128 -0
  50. package/scripts/tailscale.mjs +37 -1
  51. package/scripts/test.mjs +45 -8
  52. package/scripts/tui.mjs +395 -39
  53. package/scripts/typecheck.mjs +24 -4
  54. package/scripts/utils/auth/daemon_gate.mjs +55 -0
  55. package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
  56. package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
  57. package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
  58. package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
  59. package/scripts/utils/auth/login_ux.mjs +32 -13
  60. package/scripts/utils/auth/sources.mjs +26 -0
  61. package/scripts/utils/auth/stack_guided_login.mjs +353 -0
  62. package/scripts/utils/cli/cli_registry.mjs +43 -4
  63. package/scripts/utils/cli/cwd_scope.mjs +136 -0
  64. package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
  65. package/scripts/utils/cli/log_forwarder.mjs +157 -0
  66. package/scripts/utils/cli/prereqs.mjs +75 -0
  67. package/scripts/utils/cli/prereqs.test.mjs +34 -0
  68. package/scripts/utils/cli/progress.mjs +126 -0
  69. package/scripts/utils/cli/verbosity.mjs +12 -0
  70. package/scripts/utils/cli/wizard.mjs +17 -9
  71. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
  72. package/scripts/utils/dev/daemon.mjs +61 -4
  73. package/scripts/utils/dev/expo_dev.mjs +430 -0
  74. package/scripts/utils/dev/expo_dev.test.mjs +76 -0
  75. package/scripts/utils/dev/server.mjs +36 -42
  76. package/scripts/utils/dev_auth_key.mjs +169 -0
  77. package/scripts/utils/edison/git_roots.mjs +29 -0
  78. package/scripts/utils/edison/git_roots.test.mjs +36 -0
  79. package/scripts/utils/env/env.mjs +7 -3
  80. package/scripts/utils/env/env_file.mjs +4 -2
  81. package/scripts/utils/env/env_file.test.mjs +44 -0
  82. package/scripts/utils/expo/command.mjs +52 -0
  83. package/scripts/utils/expo/expo.mjs +20 -1
  84. package/scripts/utils/expo/metro_ports.mjs +114 -0
  85. package/scripts/utils/git/git.mjs +67 -0
  86. package/scripts/utils/git/worktrees.mjs +80 -25
  87. package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
  88. package/scripts/utils/handy_master_secret.mjs +94 -0
  89. package/scripts/utils/mobile/config.mjs +31 -0
  90. package/scripts/utils/mobile/dev_client_links.mjs +60 -0
  91. package/scripts/utils/mobile/identifiers.mjs +47 -0
  92. package/scripts/utils/mobile/identifiers.test.mjs +42 -0
  93. package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
  94. package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
  95. package/scripts/utils/net/lan_ip.mjs +24 -0
  96. package/scripts/utils/net/ports.mjs +9 -1
  97. package/scripts/utils/net/tcp_forward.mjs +162 -0
  98. package/scripts/utils/net/url.mjs +30 -0
  99. package/scripts/utils/net/url.test.mjs +20 -0
  100. package/scripts/utils/paths/localhost_host.mjs +50 -3
  101. package/scripts/utils/paths/paths.mjs +159 -40
  102. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  103. package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
  104. package/scripts/utils/proc/commands.mjs +2 -3
  105. package/scripts/utils/proc/parallel.mjs +25 -0
  106. package/scripts/utils/proc/pm.mjs +176 -22
  107. package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
  108. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
  109. package/scripts/utils/proc/proc.mjs +136 -4
  110. package/scripts/utils/proc/proc.test.mjs +77 -0
  111. package/scripts/utils/review/base_ref.mjs +74 -0
  112. package/scripts/utils/review/base_ref.test.mjs +54 -0
  113. package/scripts/utils/review/chunks.mjs +55 -0
  114. package/scripts/utils/review/chunks.test.mjs +51 -0
  115. package/scripts/utils/review/findings.mjs +165 -0
  116. package/scripts/utils/review/findings.test.mjs +85 -0
  117. package/scripts/utils/review/head_slice.mjs +153 -0
  118. package/scripts/utils/review/head_slice.test.mjs +91 -0
  119. package/scripts/utils/review/instructions/deep.md +20 -0
  120. package/scripts/utils/review/runners/coderabbit.mjs +61 -0
  121. package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
  122. package/scripts/utils/review/runners/codex.mjs +61 -0
  123. package/scripts/utils/review/runners/codex.test.mjs +35 -0
  124. package/scripts/utils/review/slices.mjs +140 -0
  125. package/scripts/utils/review/slices.test.mjs +32 -0
  126. package/scripts/utils/review/targets.mjs +24 -0
  127. package/scripts/utils/review/targets.test.mjs +36 -0
  128. package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
  129. package/scripts/utils/server/flavor_scripts.mjs +98 -0
  130. package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
  131. package/scripts/utils/server/mobile_api_url.mjs +61 -0
  132. package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
  133. package/scripts/utils/server/prisma_import.mjs +37 -0
  134. package/scripts/utils/server/prisma_import.test.mjs +70 -0
  135. package/scripts/utils/server/ui_env.mjs +14 -0
  136. package/scripts/utils/server/ui_env.test.mjs +46 -0
  137. package/scripts/utils/server/urls.mjs +14 -4
  138. package/scripts/utils/server/validate.mjs +53 -16
  139. package/scripts/utils/server/validate.test.mjs +89 -0
  140. package/scripts/utils/service/autostart_darwin.mjs +42 -2
  141. package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
  142. package/scripts/utils/stack/context.mjs +2 -2
  143. package/scripts/utils/stack/editor_workspace.mjs +6 -6
  144. package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
  145. package/scripts/utils/stack/pr_stack_name.mjs +16 -0
  146. package/scripts/utils/stack/runtime_state.mjs +2 -1
  147. package/scripts/utils/stack/startup.mjs +120 -13
  148. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
  149. package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
  150. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
  151. package/scripts/utils/stack/stop.mjs +15 -4
  152. package/scripts/utils/stack_context.mjs +23 -0
  153. package/scripts/utils/stack_runtime_state.mjs +104 -0
  154. package/scripts/utils/stacks.mjs +38 -0
  155. package/scripts/utils/tailscale/ip.mjs +116 -0
  156. package/scripts/utils/ui/ansi.mjs +39 -0
  157. package/scripts/utils/ui/qr.mjs +17 -0
  158. package/scripts/utils/validate.mjs +88 -0
  159. package/scripts/where.mjs +2 -2
  160. package/scripts/worktrees.mjs +755 -179
  161. package/scripts/worktrees_archive_cmd.test.mjs +245 -0
  162. package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
  163. package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
  164. package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
  165. package/scripts/utils/dev/expo_web.mjs +0 -112
package/scripts/setup.mjs CHANGED
@@ -4,8 +4,8 @@ import { existsSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { parseArgs } from './utils/cli/args.mjs';
6
6
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
7
- import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
8
- import { isTty, promptSelect, withRl } from './utils/cli/wizard.mjs';
7
+ import { getHappyStacksHomeDir, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
8
+ import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
9
9
  import { getCanonicalHomeDir } from './utils/env/config.mjs';
10
10
  import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
11
11
  import { run, runCapture } from './utils/proc/proc.mjs';
@@ -23,6 +23,44 @@ import { openUrlInBrowser } from './utils/ui/browser.mjs';
23
23
  import { commandExists } from './utils/proc/commands.mjs';
24
24
  import { readEnvValueFromFile } from './utils/env/read.mjs';
25
25
  import { readServerPortFromEnvFile, resolveServerPortFromEnv } from './utils/server/port.mjs';
26
+ import { guidedStackWebSignupThenLogin } from './utils/auth/guided_stack_web_login.mjs';
27
+ import { getVerbosityLevel } from './utils/cli/verbosity.mjs';
28
+ import { runCommandLogged } from './utils/cli/progress.mjs';
29
+ import { bold, cyan, dim, green } from './utils/ui/ansi.mjs';
30
+ import { expandHome } from './utils/paths/canonical_home.mjs';
31
+
32
+ function resolveWorkspaceDirDefault() {
33
+ const explicit = (process.env.HAPPY_STACKS_WORKSPACE_DIR ?? process.env.HAPPY_LOCAL_WORKSPACE_DIR ?? '').toString().trim();
34
+ if (explicit) return expandHome(explicit);
35
+ return join(getHappyStacksHomeDir(process.env), 'workspace');
36
+ }
37
+
38
+ function normalizeWorkspaceDirInput(raw, { homeDir }) {
39
+ const trimmed = String(raw ?? '').trim();
40
+ const expanded = expandHome(trimmed);
41
+ if (!expanded) return '';
42
+ // If relative, treat it as relative to the home dir (same rule as init.mjs).
43
+ return expanded.startsWith('/') ? expanded : join(homeDir, expanded);
44
+ }
45
+
46
+ async function resolveMainWebappUrlForAuth({ rootDir, port }) {
47
+ try {
48
+ const raw = await runCapture(process.execPath, [join(rootDir, 'scripts', 'auth.mjs'), 'login', '--print', '--json'], {
49
+ cwd: rootDir,
50
+ env: {
51
+ ...process.env,
52
+ HAPPY_STACKS_SERVER_PORT: String(port),
53
+ HAPPY_LOCAL_SERVER_PORT: String(port),
54
+ },
55
+ });
56
+ const parsed = JSON.parse(String(raw ?? '').trim());
57
+ const cmd = typeof parsed?.cmd === 'string' ? parsed.cmd : '';
58
+ const m = cmd.match(/HAPPY_WEBAPP_URL="([^"]+)"/);
59
+ return m?.[1] ? String(m[1]) : '';
60
+ } catch {
61
+ return '';
62
+ }
63
+ }
26
64
 
27
65
  async function resolveMainServerPort() {
28
66
  // Priority:
@@ -39,14 +77,18 @@ async function resolveMainServerPort() {
39
77
  }
40
78
 
41
79
  async function ensureSetupConfigPersisted({ rootDir, profile, serverComponent, tailscaleWanted, menubarMode }) {
80
+ const repoSourceForProfile =
81
+ profile === 'selfhost' ? (serverComponent === 'happy-server-light' ? 'forks' : 'upstream') : null;
42
82
  const updates = [
43
83
  { key: 'HAPPY_STACKS_SERVER_COMPONENT', value: serverComponent },
44
84
  { key: 'HAPPY_LOCAL_SERVER_COMPONENT', value: serverComponent },
45
- // Default for selfhost: upstream.
46
- ...(profile === 'selfhost'
85
+ // Default for selfhost:
86
+ // - full server: upstream (slopus/*)
87
+ // - server-light: forks (sqlite server-light is not available upstream today)
88
+ ...(repoSourceForProfile
47
89
  ? [
48
- { key: 'HAPPY_STACKS_REPO_SOURCE', value: 'upstream' },
49
- { key: 'HAPPY_LOCAL_REPO_SOURCE', value: 'upstream' },
90
+ { key: 'HAPPY_STACKS_REPO_SOURCE', value: repoSourceForProfile },
91
+ { key: 'HAPPY_LOCAL_REPO_SOURCE', value: repoSourceForProfile },
50
92
  ]
51
93
  : []),
52
94
  { key: 'HAPPY_STACKS_MENUBAR_MODE', value: menubarMode },
@@ -248,6 +290,7 @@ async function cmdSetup({ rootDir, argv }) {
248
290
  flags: [
249
291
  '--profile=selfhost|dev',
250
292
  '--server=happy-server-light|happy-server',
293
+ '--workspace-dir=/absolute/path # dev profile only',
251
294
  '--install-path',
252
295
  '--start-now',
253
296
  '--auth|--no-auth',
@@ -262,7 +305,8 @@ async function cmdSetup({ rootDir, argv }) {
262
305
  ' happys setup',
263
306
  ' happys setup --profile=selfhost',
264
307
  ' happys setup --profile=dev',
265
- ' happys setup pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>]',
308
+ ' happys setup --profile=dev --workspace-dir=~/Development/happy',
309
+ ' happys setup pr --happy=<pr-url|number> [--happy-server-light=<pr-url|number>]',
266
310
  ' happys setup --auth',
267
311
  ' happys setup --no-auth',
268
312
  '',
@@ -280,10 +324,10 @@ async function cmdSetup({ rootDir, argv }) {
280
324
  if (!profile && interactive) {
281
325
  profile = await withRl(async (rl) => {
282
326
  return await promptSelect(rl, {
283
- title: 'What is your goal?',
327
+ title: bold(`✨ ${cyan('Happy Stacks')} setup ✨\n\nWhat is your goal?`),
284
328
  options: [
285
- { label: 'Use Happy on this machine (self-host)', value: 'selfhost' },
286
- { label: 'Develop Happy (worktrees/stacks)', value: 'dev' },
329
+ { label: `${cyan('Self-host')}: use Happy on this machine`, value: 'selfhost' },
330
+ { label: `${cyan('Development')}: worktrees + stacks + contributor workflows`, value: 'dev' },
287
331
  ],
288
332
  defaultIndex: 0,
289
333
  });
@@ -293,6 +337,71 @@ async function cmdSetup({ rootDir, argv }) {
293
337
  profile = 'selfhost';
294
338
  }
295
339
 
340
+ const verbosity = getVerbosityLevel(process.env);
341
+ const quietUi = interactive && verbosity === 0 && !json;
342
+
343
+ async function runNodeScriptMaybeQuiet({ label, rel, args = [], env = process.env }) {
344
+ if (!quietUi) {
345
+ await run(process.execPath, [join(rootDir, rel), ...args], { cwd: rootDir, env });
346
+ return;
347
+ }
348
+ const baseLogDir = join(getHappyStacksHomeDir(process.env), 'logs', 'setup');
349
+ const logPath = join(baseLogDir, `${label.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.${Date.now()}.log`);
350
+ try {
351
+ await runCommandLogged({
352
+ label,
353
+ cmd: process.execPath,
354
+ args: [join(rootDir, rel), ...args],
355
+ cwd: rootDir,
356
+ env,
357
+ logPath,
358
+ quiet: true,
359
+ showSteps: true,
360
+ });
361
+ } catch (e) {
362
+ const lp = e?.logPath ? String(e.logPath) : logPath;
363
+ // eslint-disable-next-line no-console
364
+ console.error(`[setup] failed: ${label}`);
365
+ // eslint-disable-next-line no-console
366
+ console.error(`${dim('log:')} ${lp}`);
367
+ throw e;
368
+ }
369
+ }
370
+
371
+ function printProfileIntro({ profile }) {
372
+ if (!process.stdout.isTTY || json) return;
373
+ const header = profile === 'selfhost' ? `${cyan('Self-host')} setup` : `${cyan('Development')} setup`;
374
+ const lines = [
375
+ '',
376
+ bold(header),
377
+ profile === 'selfhost'
378
+ ? dim('Run Happy locally (optionally with Tailscale + autostart).')
379
+ : dim('Prepare a contributor workspace (components + worktrees + stacks).'),
380
+ '',
381
+ bold('What will happen:'),
382
+ profile === 'selfhost'
383
+ ? [
384
+ `- ${cyan('init')}: set up Happy Stacks home + shims`,
385
+ `- ${cyan('bootstrap')}: clone/install components`,
386
+ `- ${cyan('start')}: (optional) start Happy now`,
387
+ `- ${cyan('login')}: (optional) authenticate`,
388
+ ]
389
+ : [
390
+ `- ${cyan('workspace')}: choose where components + worktrees live`,
391
+ `- ${cyan('init')}: set up Happy Stacks home + shims`,
392
+ `- ${cyan('bootstrap')}: clone/install components + dev tooling`,
393
+ `- ${cyan('stacks')}: (optional) create an isolated dev stack`,
394
+ ],
395
+ '',
396
+ ].flat();
397
+ // eslint-disable-next-line no-console
398
+ console.log(lines.join('\n'));
399
+ }
400
+
401
+ if (interactive) {
402
+ printProfileIntro({ profile });
403
+ }
404
+
296
405
  const platform = process.platform;
297
406
  const supportsAutostart = platform === 'darwin' || platform === 'linux';
298
407
  const supportsMenubar = platform === 'darwin';
@@ -302,7 +411,7 @@ async function cmdSetup({ rootDir, argv }) {
302
411
  if (profile === 'selfhost' && interactive && !serverFromArg) {
303
412
  serverComponent = await withRl(async (rl) => {
304
413
  const picked = await promptSelect(rl, {
305
- title: 'Select server flavor:',
414
+ title: bold('Server flavor'),
306
415
  options: [
307
416
  { label: 'happy-server-light (recommended; simplest local install)', value: 'happy-server-light' },
308
417
  { label: 'happy-server (full server; managed infra via Docker)', value: 'happy-server' },
@@ -313,6 +422,34 @@ async function cmdSetup({ rootDir, argv }) {
313
422
  });
314
423
  }
315
424
 
425
+ // Dev profile: pick where to store components + worktrees.
426
+ const workspaceDirFlagRaw = (kv.get('--workspace-dir') ?? '').toString().trim();
427
+ const homeDirForWorkspace = getHappyStacksHomeDir(process.env);
428
+ let workspaceDirWanted = workspaceDirFlagRaw ? normalizeWorkspaceDirInput(workspaceDirFlagRaw, { homeDir: homeDirForWorkspace }) : '';
429
+ if (profile === 'dev' && interactive && !workspaceDirWanted) {
430
+ const defaultWorkspaceDir = resolveWorkspaceDirDefault();
431
+ const suggested = defaultWorkspaceDir;
432
+ const helpLines = [
433
+ bold('Workspace location'),
434
+ dim('This is where Happy Stacks will keep:'),
435
+ `- ${dim('components')}: ${cyan(join(suggested, 'components'))}`,
436
+ `- ${dim('worktrees')}: ${cyan(join(suggested, 'components', '.worktrees'))}`,
437
+ '',
438
+ dim('Pick a stable folder that is easy to open in your editor (example: ~/Development/happy).'),
439
+ '',
440
+ ].join('\n');
441
+ // eslint-disable-next-line no-console
442
+ console.log(helpLines);
443
+ const raw = await withRl(async (rl) => {
444
+ return await prompt(rl, `Workspace dir (default: ${suggested}): `, { defaultValue: suggested });
445
+ });
446
+ workspaceDirWanted = normalizeWorkspaceDirInput(raw, { homeDir: homeDirForWorkspace });
447
+ }
448
+ if (profile === 'dev' && workspaceDirWanted) {
449
+ // eslint-disable-next-line no-console
450
+ console.log(`${dim('Workspace:')} ${cyan(workspaceDirWanted)}`);
451
+ }
452
+
316
453
  const defaultTailscale = false;
317
454
  const defaultAutostart = false;
318
455
  const defaultMenubar = false;
@@ -337,7 +474,7 @@ async function cmdSetup({ rootDir, argv }) {
337
474
  if (profile === 'selfhost') {
338
475
  tailscaleWanted = await withRl(async (rl) => {
339
476
  const v = await promptSelect(rl, {
340
- title: 'Enable remote access with Tailscale Serve (recommended for mobile)?',
477
+ title: bold('Remote access'),
341
478
  options: [
342
479
  { label: 'no (default)', value: false },
343
480
  { label: 'yes', value: true },
@@ -350,7 +487,7 @@ async function cmdSetup({ rootDir, argv }) {
350
487
  if (supportsAutostart) {
351
488
  autostartWanted = await withRl(async (rl) => {
352
489
  const v = await promptSelect(rl, {
353
- title: 'Enable autostart at login?',
490
+ title: bold('Autostart'),
354
491
  options: [
355
492
  { label: 'no (default)', value: false },
356
493
  { label: 'yes', value: true },
@@ -366,7 +503,7 @@ async function cmdSetup({ rootDir, argv }) {
366
503
  if (supportsMenubar) {
367
504
  menubarWanted = await withRl(async (rl) => {
368
505
  const v = await promptSelect(rl, {
369
- title: 'Install the macOS menubar (SwiftBar) control panel?',
506
+ title: bold('Menu bar (macOS)'),
370
507
  options: [
371
508
  { label: 'no (default)', value: false },
372
509
  { label: 'yes', value: true },
@@ -381,7 +518,7 @@ async function cmdSetup({ rootDir, argv }) {
381
518
 
382
519
  startNow = await withRl(async (rl) => {
383
520
  const v = await promptSelect(rl, {
384
- title: 'Start Happy now?',
521
+ title: bold('Start now'),
385
522
  options: [
386
523
  { label: 'yes (default)', value: true },
387
524
  { label: 'no', value: false },
@@ -393,7 +530,7 @@ async function cmdSetup({ rootDir, argv }) {
393
530
 
394
531
  authWanted = await withRl(async (rl) => {
395
532
  const v = await promptSelect(rl, {
396
- title: 'Authenticate now? (recommended)',
533
+ title: bold('Authentication'),
397
534
  options: [
398
535
  { label: 'yes (default) — enables Happy UI + mobile access', value: true },
399
536
  { label: 'no — I will authenticate later', value: false },
@@ -412,7 +549,7 @@ async function cmdSetup({ rootDir, argv }) {
412
549
  // If you choose to auth now, we’ll also start Happy in the background so login can complete.
413
550
  const authNow = await withRl(async (rl) => {
414
551
  const v = await promptSelect(rl, {
415
- title: 'Complete authentication now? (optional)',
552
+ title: bold('Authentication (optional)'),
416
553
  options: [
417
554
  { label: 'no (default) — I will do this later', value: false },
418
555
  { label: 'yes — start Happy in background and login', value: true },
@@ -429,10 +566,10 @@ async function cmdSetup({ rootDir, argv }) {
429
566
 
430
567
  installPath = await withRl(async (rl) => {
431
568
  const v = await promptSelect(rl, {
432
- title: `Add ${join(getCanonicalHomeDir(), 'bin')} to your shell PATH?`,
569
+ title: bold('Shell PATH'),
433
570
  options: [
434
- { label: 'no (default)', value: false },
435
- { label: 'yes', value: true },
571
+ { label: `no (default) — you can run via npx / full path`, value: false },
572
+ { label: `yes — add ${join(getCanonicalHomeDir(), 'bin')} to your PATH`, value: true },
436
573
  ],
437
574
  defaultIndex: installPath ? 1 : 0,
438
575
  });
@@ -465,11 +602,12 @@ async function cmdSetup({ rootDir, argv }) {
465
602
  }
466
603
 
467
604
  // 1) Ensure plumbing exists (runtime + shims + pointer env). Avoid auto-bootstrap here; setup drives bootstrap explicitly.
468
- await runNodeScript({
469
- rootDir,
605
+ await runNodeScriptMaybeQuiet({
606
+ label: 'init happy-stacks home',
470
607
  rel: 'scripts/init.mjs',
471
608
  args: [
472
609
  '--no-bootstrap',
610
+ ...(profile === 'dev' && workspaceDirWanted ? [`--workspace-dir=${workspaceDirWanted}`] : []),
473
611
  ...(installPath ? ['--install-path'] : []),
474
612
  ],
475
613
  env: { ...process.env, HAPPY_STACKS_SETUP_CHILD: '1' },
@@ -487,13 +625,13 @@ async function cmdSetup({ rootDir, argv }) {
487
625
  // 3) Bootstrap components. Selfhost defaults to upstream; dev defaults to existing bootstrap wizard (forks by default).
488
626
  if (profile === 'dev') {
489
627
  // Developer setup: keep the existing bootstrap wizard.
490
- await runNodeScript({ rootDir, rel: 'scripts/install.mjs', args: ['--interactive'] });
628
+ await runNodeScriptMaybeQuiet({ label: 'bootstrap components', rootDir, rel: 'scripts/install.mjs', args: ['--interactive'] });
491
629
 
492
630
  // Optional: offer to create a dedicated dev stack (keeps main stable).
493
631
  if (interactive) {
494
632
  const createStack = await withRl(async (rl) => {
495
633
  return await promptSelect(rl, {
496
- title: 'Create an additional isolated stack for development?',
634
+ title: bold('Stacks'),
497
635
  options: [
498
636
  { label: 'no (default)', value: false },
499
637
  { label: 'yes', value: true },
@@ -502,7 +640,7 @@ async function cmdSetup({ rootDir, argv }) {
502
640
  });
503
641
  });
504
642
  if (createStack) {
505
- await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: ['new', '--interactive'] });
643
+ await runNodeScriptMaybeQuiet({ label: 'create dev stack', rootDir, rel: 'scripts/stack.mjs', args: ['new', '--interactive'] });
506
644
  }
507
645
 
508
646
  // Guided maintainer-friendly auth defaults (dev key → main → legacy).
@@ -510,10 +648,12 @@ async function cmdSetup({ rootDir, argv }) {
510
648
  }
511
649
  } else {
512
650
  // Selfhost setup: run non-interactively and keep it simple.
513
- await runNodeScript({
651
+ const repoFlag = serverComponent === 'happy-server-light' ? '--forks' : '--upstream';
652
+ await runNodeScriptMaybeQuiet({
653
+ label: 'bootstrap components',
514
654
  rootDir,
515
655
  rel: 'scripts/install.mjs',
516
- args: [`--server=${serverComponent}`, '--upstream'],
656
+ args: [`--server=${serverComponent}`, repoFlag],
517
657
  });
518
658
  }
519
659
 
@@ -592,46 +732,20 @@ async function cmdSetup({ rootDir, argv }) {
592
732
  // eslint-disable-next-line no-console
593
733
  console.log('[setup] auth: already configured (access.key exists)');
594
734
  } else {
595
- // Before starting an interactive login, offer the best available shortcut in this order:
596
- // For selfhost profile:
597
- // - prefer reusing legacy ~/.happy creds if present (maintainers often already have a local install)
598
- // - otherwise, run the normal login flow
599
- let reused = false;
600
735
  if (interactive) {
601
- const legacyAccessKey = join(homedir(), '.happy', 'cli', 'access.key');
602
- const allowLegacy = !isSandboxed() || sandboxAllowsGlobalSideEffects();
603
- const hasLegacy = allowLegacy && existsSync(legacyAccessKey);
604
-
605
- if (hasLegacy) {
606
- const options = [];
607
- if (hasLegacy) {
608
- options.push({ label: 'reuse legacy ~/.happy (symlink; stays up to date)', value: 'legacy-link' });
609
- options.push({ label: 'reuse legacy ~/.happy (copy; more isolated)', value: 'legacy-copy' });
610
- }
611
- options.push({ label: 'do login flow instead', value: 'login' });
612
-
613
- const choice = await withRl(async (rl) => {
614
- return await promptSelect(rl, {
615
- title:
616
- 'We found existing credentials on this machine. How should Happy Stacks main authenticate?',
617
- options,
618
- defaultIndex: 0,
619
- });
620
- });
621
-
622
- if (choice === 'legacy-link') {
623
- await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args: ['copy-from', 'legacy', '--allow-main', '--link'] });
624
- reused = existsSync(accessKey);
625
- } else if (choice === 'legacy-copy') {
626
- await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args: ['copy-from', 'legacy', '--allow-main'] });
627
- reused = existsSync(accessKey);
628
- }
629
- }
630
- }
631
-
632
- if (!reused) {
633
- await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args: ['login', `--context=${ctx}`] });
736
+ const webappUrl = await resolveMainWebappUrlForAuth({ rootDir, port });
737
+ await guidedStackWebSignupThenLogin({ webappUrl, stackName: 'main' });
634
738
  }
739
+ await runNodeScript({
740
+ rootDir,
741
+ rel: 'scripts/auth.mjs',
742
+ args: ['login', `--context=${ctx}`, '--quiet'],
743
+ env: {
744
+ ...process.env,
745
+ HAPPY_STACKS_SERVER_PORT: String(port),
746
+ HAPPY_LOCAL_SERVER_PORT: String(port),
747
+ },
748
+ });
635
749
 
636
750
  if (!existsSync(accessKey)) {
637
751
  // eslint-disable-next-line no-console
@@ -662,7 +776,11 @@ async function cmdSetup({ rootDir, argv }) {
662
776
  // Final tips (keep short).
663
777
  if (profile === 'selfhost') {
664
778
  // eslint-disable-next-line no-console
665
- console.log('[setup] done. Useful commands:');
779
+ console.log('');
780
+ // eslint-disable-next-line no-console
781
+ console.log(green('✓ Setup complete'));
782
+ // eslint-disable-next-line no-console
783
+ console.log(dim('Useful commands:'));
666
784
  // eslint-disable-next-line no-console
667
785
  console.log(' happys start');
668
786
  // eslint-disable-next-line no-console
@@ -671,7 +789,11 @@ async function cmdSetup({ rootDir, argv }) {
671
789
  console.log(' happys service install # macOS/Linux autostart');
672
790
  } else {
673
791
  // eslint-disable-next-line no-console
674
- console.log('[setup] done. Useful commands:');
792
+ console.log('');
793
+ // eslint-disable-next-line no-console
794
+ console.log(green('✓ Setup complete'));
795
+ // eslint-disable-next-line no-console
796
+ console.log(dim('Useful commands:'));
675
797
  // eslint-disable-next-line no-console
676
798
  console.log(' happys dev');
677
799
  // eslint-disable-next-line no-console
@@ -691,4 +813,3 @@ main().catch((err) => {
691
813
  console.error('[setup] failed:', err);
692
814
  process.exit(1);
693
815
  });
694
-