happy-stacks 0.4.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 (104) hide show
  1. package/README.md +64 -33
  2. package/bin/happys.mjs +44 -1
  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 +1 -2
  10. package/docs/monorepo-migration.md +286 -0
  11. package/docs/server-flavors.md +19 -3
  12. package/docs/stacks.md +35 -0
  13. package/package.json +1 -1
  14. package/scripts/auth.mjs +21 -3
  15. package/scripts/build.mjs +1 -1
  16. package/scripts/dev.mjs +20 -7
  17. package/scripts/doctor.mjs +0 -4
  18. package/scripts/edison.mjs +2 -2
  19. package/scripts/env.mjs +150 -0
  20. package/scripts/env_cmd.test.mjs +128 -0
  21. package/scripts/init.mjs +5 -2
  22. package/scripts/install.mjs +99 -57
  23. package/scripts/migrate.mjs +3 -12
  24. package/scripts/monorepo.mjs +1096 -0
  25. package/scripts/monorepo_port.test.mjs +1470 -0
  26. package/scripts/review.mjs +715 -24
  27. package/scripts/review_pr.mjs +5 -20
  28. package/scripts/run.mjs +21 -15
  29. package/scripts/setup.mjs +147 -25
  30. package/scripts/setup_pr.mjs +19 -28
  31. package/scripts/stack.mjs +493 -157
  32. package/scripts/stack_archive_cmd.test.mjs +91 -0
  33. package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
  34. package/scripts/stack_env_cmd.test.mjs +87 -0
  35. package/scripts/stack_happy_cmd.test.mjs +126 -0
  36. package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
  37. package/scripts/stack_monorepo_defaults.test.mjs +62 -0
  38. package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
  39. package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
  40. package/scripts/stack_shorthand_cmd.test.mjs +55 -0
  41. package/scripts/stack_wt_list.test.mjs +128 -0
  42. package/scripts/tui.mjs +88 -2
  43. package/scripts/utils/cli/cli_registry.mjs +20 -5
  44. package/scripts/utils/cli/cwd_scope.mjs +56 -2
  45. package/scripts/utils/cli/cwd_scope.test.mjs +40 -7
  46. package/scripts/utils/cli/prereqs.mjs +8 -5
  47. package/scripts/utils/cli/prereqs.test.mjs +34 -0
  48. package/scripts/utils/cli/wizard.mjs +17 -9
  49. package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
  50. package/scripts/utils/dev/daemon.mjs +14 -1
  51. package/scripts/utils/dev/expo_dev.mjs +188 -4
  52. package/scripts/utils/dev/server.mjs +21 -17
  53. package/scripts/utils/edison/git_roots.mjs +29 -0
  54. package/scripts/utils/edison/git_roots.test.mjs +36 -0
  55. package/scripts/utils/env/env.mjs +7 -3
  56. package/scripts/utils/env/env_file.mjs +4 -2
  57. package/scripts/utils/env/env_file.test.mjs +44 -0
  58. package/scripts/utils/git/worktrees.mjs +63 -12
  59. package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
  60. package/scripts/utils/net/tcp_forward.mjs +162 -0
  61. package/scripts/utils/paths/paths.mjs +118 -3
  62. package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
  63. package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
  64. package/scripts/utils/proc/commands.mjs +2 -3
  65. package/scripts/utils/proc/pm.mjs +113 -16
  66. package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
  67. package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
  68. package/scripts/utils/proc/proc.mjs +68 -10
  69. package/scripts/utils/proc/proc.test.mjs +77 -0
  70. package/scripts/utils/review/chunks.mjs +55 -0
  71. package/scripts/utils/review/chunks.test.mjs +51 -0
  72. package/scripts/utils/review/findings.mjs +165 -0
  73. package/scripts/utils/review/findings.test.mjs +85 -0
  74. package/scripts/utils/review/head_slice.mjs +153 -0
  75. package/scripts/utils/review/head_slice.test.mjs +91 -0
  76. package/scripts/utils/review/instructions/deep.md +20 -0
  77. package/scripts/utils/review/runners/coderabbit.mjs +56 -14
  78. package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
  79. package/scripts/utils/review/runners/codex.mjs +32 -22
  80. package/scripts/utils/review/runners/codex.test.mjs +35 -0
  81. package/scripts/utils/review/slices.mjs +140 -0
  82. package/scripts/utils/review/slices.test.mjs +32 -0
  83. package/scripts/utils/server/flavor_scripts.mjs +98 -0
  84. package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
  85. package/scripts/utils/server/prisma_import.mjs +37 -0
  86. package/scripts/utils/server/prisma_import.test.mjs +70 -0
  87. package/scripts/utils/server/ui_env.mjs +14 -0
  88. package/scripts/utils/server/ui_env.test.mjs +46 -0
  89. package/scripts/utils/server/validate.mjs +53 -16
  90. package/scripts/utils/server/validate.test.mjs +89 -0
  91. package/scripts/utils/stack/editor_workspace.mjs +4 -4
  92. package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
  93. package/scripts/utils/stack/startup.mjs +113 -13
  94. package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
  95. package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
  96. package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
  97. package/scripts/utils/tailscale/ip.mjs +116 -0
  98. package/scripts/utils/ui/ansi.mjs +39 -0
  99. package/scripts/where.mjs +2 -2
  100. package/scripts/worktrees.mjs +627 -137
  101. package/scripts/worktrees_archive_cmd.test.mjs +245 -0
  102. package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
  103. package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
  104. package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
@@ -15,35 +15,20 @@ import { randomToken } from './utils/crypto/tokens.mjs';
15
15
  import { inferPrStackBaseName } from './utils/stack/pr_stack_name.mjs';
16
16
  import { sanitizeStackName } from './utils/stack/names.mjs';
17
17
  import { listReviewPrSandboxes, reviewPrSandboxPrefixPath, writeReviewPrSandboxMeta } from './utils/sandbox/review_pr_sandbox.mjs';
18
+ import { bold, cyan, dim } from './utils/ui/ansi.mjs';
18
19
 
19
- function supportsAnsi() {
20
- if (!process.stdout.isTTY) return false;
21
- if (process.env.NO_COLOR) return false;
22
- if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
23
- return true;
24
- }
25
-
26
- function bold(s) {
27
- return supportsAnsi() ? `\x1b[1m${s}\x1b[0m` : String(s);
28
- }
29
-
30
- function dim(s) {
31
- return supportsAnsi() ? `\x1b[2m${s}\x1b[0m` : String(s);
32
- }
33
-
34
- function cyan(s) {
35
- return supportsAnsi() ? `\x1b[36m${s}\x1b[0m` : String(s);
36
- }
37
-
38
20
  function usage() {
39
21
  return [
40
22
  '[review-pr] usage:',
41
- ' happys review-pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--happy-server=<pr-url|number>|--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--mobile|--no-mobile] [--forks|--upstream] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack>] [--link-auth|--copy-auth] [--update] [--force] [--keep-sandbox] [--json] [-- <stack dev/start args...>]',
23
+ ' happys review-pr --happy=<pr-url|number> [--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--mobile|--no-mobile] [--forks|--upstream] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack>] [--link-auth|--copy-auth] [--update] [--force] [--keep-sandbox] [--json] [-- <stack dev/start args...>]',
42
24
  '',
43
25
  'What it does:',
44
26
  '- creates a temporary sandbox dir',
45
27
  '- runs `happys setup-pr ...` inside that sandbox (fully isolated state)',
46
28
  '- on exit (including Ctrl+C): stops sandbox processes and deletes the sandbox dir',
29
+ '',
30
+ 'legacy note:',
31
+ '- `--happy-cli` / `--happy-server` are legacy split-repo flags; in monorepo mode, use `--happy` only.',
47
32
  ].join('\n');
48
33
  }
49
34
 
package/scripts/run.mjs CHANGED
@@ -13,6 +13,7 @@ import { maybeResetTailscaleServe } from './tailscale.mjs';
13
13
  import { isDaemonRunning, startLocalDaemonWithAuth, stopLocalDaemon } from './daemon.mjs';
14
14
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
15
15
  import { assertServerComponentDirMatches, assertServerPrismaProviderMatches } from './utils/server/validate.mjs';
16
+ import { resolveServerStartScript } from './utils/server/flavor_scripts.mjs';
16
17
  import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from './utils/server/infra/happy_server_infra.mjs';
17
18
  import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded, resolveAutoCopyFromMainEnabled } from './utils/stack/startup.mjs';
18
19
  import { recordStackRuntimeStart, recordStackRuntimeUpdate } from './utils/stack/runtime_state.mjs';
@@ -20,16 +21,17 @@ import { resolveStackContext } from './utils/stack/context.mjs';
20
21
  import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
21
22
  import { preferStackLocalhostUrl } from './utils/paths/localhost_host.mjs';
22
23
  import { openUrlInBrowser } from './utils/ui/browser.mjs';
23
- import { ensureDevExpoServer } from './utils/dev/expo_dev.mjs';
24
+ import { ensureDevExpoServer, resolveExpoTailscaleEnabled } from './utils/dev/expo_dev.mjs';
24
25
  import { maybeRunInteractiveStackAuthSetup } from './utils/auth/interactive_stack_auth.mjs';
25
26
  import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
26
27
  import { daemonStartGate, formatDaemonAuthRequiredError } from './utils/auth/daemon_gate.mjs';
28
+ import { resolveServerUiEnv } from './utils/server/ui_env.mjs';
27
29
 
28
30
  /**
29
31
  * Run the local stack in "production-like" mode:
30
- * - happy-server-light
32
+ * - server (happy-server-light by default)
31
33
  * - happy-cli daemon
32
- * - serve prebuilt UI via happy-server-light (/)
34
+ * - optionally serve prebuilt UI (via server or gateway)
33
35
  *
34
36
  * Optional: Expo dev-client Metro for mobile reviewers (`--mobile`).
35
37
  */
@@ -41,10 +43,12 @@ async function main() {
41
43
  if (wantsHelp(argv, { flags })) {
42
44
  printResult({
43
45
  json,
44
- data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--no-browser', '--mobile'], json: true },
46
+ data: { flags: ['--server=happy-server|happy-server-light', '--no-ui', '--no-daemon', '--restart', '--no-browser', '--mobile', '--expo-tailscale'], json: true },
45
47
  text: [
46
48
  '[start] usage:',
47
49
  ' happys start [--server=happy-server|happy-server-light] [--restart] [--json]',
50
+ ' happys start --mobile # also start Expo dev-client Metro for mobile',
51
+ ' happys start --expo-tailscale # forward Expo to Tailscale interface for remote access',
48
52
  ' (legacy in a cloned repo): pnpm start [-- --server=happy-server|happy-server-light] [--json]',
49
53
  ' note: --json prints the resolved config (dry-run) and exits.',
50
54
  '',
@@ -89,6 +93,7 @@ async function main() {
89
93
  const serveUiWanted = !flags.has('--no-ui') && (process.env.HAPPY_LOCAL_SERVE_UI ?? '1') !== '0';
90
94
  const serveUi = serveUiWanted;
91
95
  const startMobile = flags.has('--mobile') || flags.has('--with-mobile');
96
+ const expoTailscale = flags.has('--expo-tailscale') || resolveExpoTailscaleEnabled({ env: process.env });
92
97
  const noBrowser = flags.has('--no-browser') || (process.env.HAPPY_STACKS_NO_BROWSER ?? process.env.HAPPY_LOCAL_NO_BROWSER ?? '').toString().trim() === '1';
93
98
  const uiPrefix = process.env.HAPPY_LOCAL_UI_PREFIX?.trim() ? process.env.HAPPY_LOCAL_UI_PREFIX.trim() : '/';
94
99
  const autostart = getDefaultAutostartPaths();
@@ -101,6 +106,7 @@ async function main() {
101
106
  const serverDir = getComponentDir(rootDir, serverComponentName);
102
107
  const cliDir = getComponentDir(rootDir, 'happy-cli');
103
108
  const uiDir = getComponentDir(rootDir, 'happy');
109
+ const serverStartScript = resolveServerStartScript({ serverComponentName, serverDir });
104
110
 
105
111
  assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir });
106
112
  assertServerPrismaProviderMatches({ serverComponentName, serverDir });
@@ -141,12 +147,13 @@ async function main() {
141
147
  return;
142
148
  }
143
149
 
144
- if (serveUi && !(await pathExists(uiBuildDir))) {
150
+ const uiBuildDirExists = await pathExists(uiBuildDir);
151
+ if (serveUi && !uiBuildDirExists) {
145
152
  if (serverComponentName === 'happy-server-light') {
146
153
  throw new Error(`[local] UI build directory not found at ${uiBuildDir}. Run: happys build (legacy in a cloned repo: pnpm build)`);
147
154
  }
148
- // For happy-server, UI serving is optional via the UI gateway.
149
- console.log(`[local] UI build directory not found at ${uiBuildDir}; UI gateway will be disabled`);
155
+ // For happy-server, UI serving is optional.
156
+ console.log(`[local] UI build directory not found at ${uiBuildDir}; UI serving will be disabled`);
150
157
  }
151
158
 
152
159
  const children = [];
@@ -215,12 +222,7 @@ async function main() {
215
222
  // Avoid noisy failures if a previous run left the metrics port busy.
216
223
  // You can override with METRICS_ENABLED=true if you want it.
217
224
  METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
218
- ...(serveUi && serverComponentName === 'happy-server-light'
219
- ? {
220
- HAPPY_SERVER_LIGHT_UI_DIR: uiBuildDir,
221
- HAPPY_SERVER_LIGHT_UI_PREFIX: uiPrefix,
222
- }
223
- : {}),
225
+ ...resolveServerUiEnv({ serveUi, uiBuildDir, uiPrefix, uiBuildDirExists }),
224
226
  };
225
227
  let serverLightAccountCount = null;
226
228
  let happyServerAccountCount = null;
@@ -319,7 +321,7 @@ async function main() {
319
321
  // Default server start (happy-server-light, or happy-server without managed infra).
320
322
  if (!(serverComponentName === 'happy-server' && (baseEnv.HAPPY_STACKS_MANAGED_INFRA ?? baseEnv.HAPPY_LOCAL_MANAGED_INFRA ?? '1') !== '0')) {
321
323
  if (!serverAlreadyRunning || restart) {
322
- const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: 'start', env: serverEnv });
324
+ const server = await pmSpawnScript({ label: 'server', dir: serverDir, script: serverStartScript, env: serverEnv });
323
325
  children.push(server);
324
326
  if (stackMode && runtimeStatePath) {
325
327
  await recordStackRuntimeUpdate(runtimeStatePath, { processes: { serverPid: server.pid } }).catch(() => {});
@@ -433,7 +435,7 @@ async function main() {
433
435
 
434
436
  // Optional: start Expo dev-client Metro for mobile reviewers.
435
437
  if (startMobile) {
436
- await ensureDevExpoServer({
438
+ const expoRes = await ensureDevExpoServer({
437
439
  startUi: false,
438
440
  startMobile: true,
439
441
  uiDir,
@@ -446,7 +448,11 @@ async function main() {
446
448
  stackName,
447
449
  envPath,
448
450
  children,
451
+ expoTailscale,
449
452
  });
453
+ if (expoRes?.tailscale?.ok && expoRes.tailscale.tailscaleIp && expoRes.port) {
454
+ console.log(`[local] expo tailscale: http://${expoRes.tailscale.tailscaleIp}:${expoRes.port}`);
455
+ }
450
456
  }
451
457
 
452
458
  const shutdown = async () => {
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';
@@ -24,6 +24,24 @@ 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
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
+ }
27
45
 
28
46
  async function resolveMainWebappUrlForAuth({ rootDir, port }) {
29
47
  try {
@@ -272,6 +290,7 @@ async function cmdSetup({ rootDir, argv }) {
272
290
  flags: [
273
291
  '--profile=selfhost|dev',
274
292
  '--server=happy-server-light|happy-server',
293
+ '--workspace-dir=/absolute/path # dev profile only',
275
294
  '--install-path',
276
295
  '--start-now',
277
296
  '--auth|--no-auth',
@@ -286,7 +305,8 @@ async function cmdSetup({ rootDir, argv }) {
286
305
  ' happys setup',
287
306
  ' happys setup --profile=selfhost',
288
307
  ' happys setup --profile=dev',
289
- ' 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>]',
290
310
  ' happys setup --auth',
291
311
  ' happys setup --no-auth',
292
312
  '',
@@ -304,10 +324,10 @@ async function cmdSetup({ rootDir, argv }) {
304
324
  if (!profile && interactive) {
305
325
  profile = await withRl(async (rl) => {
306
326
  return await promptSelect(rl, {
307
- title: 'What is your goal?',
327
+ title: bold(`✨ ${cyan('Happy Stacks')} setup ✨\n\nWhat is your goal?`),
308
328
  options: [
309
- { label: 'Use Happy on this machine (self-host)', value: 'selfhost' },
310
- { 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' },
311
331
  ],
312
332
  defaultIndex: 0,
313
333
  });
@@ -317,6 +337,71 @@ async function cmdSetup({ rootDir, argv }) {
317
337
  profile = 'selfhost';
318
338
  }
319
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
+
320
405
  const platform = process.platform;
321
406
  const supportsAutostart = platform === 'darwin' || platform === 'linux';
322
407
  const supportsMenubar = platform === 'darwin';
@@ -326,7 +411,7 @@ async function cmdSetup({ rootDir, argv }) {
326
411
  if (profile === 'selfhost' && interactive && !serverFromArg) {
327
412
  serverComponent = await withRl(async (rl) => {
328
413
  const picked = await promptSelect(rl, {
329
- title: 'Select server flavor:',
414
+ title: bold('Server flavor'),
330
415
  options: [
331
416
  { label: 'happy-server-light (recommended; simplest local install)', value: 'happy-server-light' },
332
417
  { label: 'happy-server (full server; managed infra via Docker)', value: 'happy-server' },
@@ -337,6 +422,34 @@ async function cmdSetup({ rootDir, argv }) {
337
422
  });
338
423
  }
339
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
+
340
453
  const defaultTailscale = false;
341
454
  const defaultAutostart = false;
342
455
  const defaultMenubar = false;
@@ -361,7 +474,7 @@ async function cmdSetup({ rootDir, argv }) {
361
474
  if (profile === 'selfhost') {
362
475
  tailscaleWanted = await withRl(async (rl) => {
363
476
  const v = await promptSelect(rl, {
364
- title: 'Enable remote access with Tailscale Serve (recommended for mobile)?',
477
+ title: bold('Remote access'),
365
478
  options: [
366
479
  { label: 'no (default)', value: false },
367
480
  { label: 'yes', value: true },
@@ -374,7 +487,7 @@ async function cmdSetup({ rootDir, argv }) {
374
487
  if (supportsAutostart) {
375
488
  autostartWanted = await withRl(async (rl) => {
376
489
  const v = await promptSelect(rl, {
377
- title: 'Enable autostart at login?',
490
+ title: bold('Autostart'),
378
491
  options: [
379
492
  { label: 'no (default)', value: false },
380
493
  { label: 'yes', value: true },
@@ -390,7 +503,7 @@ async function cmdSetup({ rootDir, argv }) {
390
503
  if (supportsMenubar) {
391
504
  menubarWanted = await withRl(async (rl) => {
392
505
  const v = await promptSelect(rl, {
393
- title: 'Install the macOS menubar (SwiftBar) control panel?',
506
+ title: bold('Menu bar (macOS)'),
394
507
  options: [
395
508
  { label: 'no (default)', value: false },
396
509
  { label: 'yes', value: true },
@@ -405,7 +518,7 @@ async function cmdSetup({ rootDir, argv }) {
405
518
 
406
519
  startNow = await withRl(async (rl) => {
407
520
  const v = await promptSelect(rl, {
408
- title: 'Start Happy now?',
521
+ title: bold('Start now'),
409
522
  options: [
410
523
  { label: 'yes (default)', value: true },
411
524
  { label: 'no', value: false },
@@ -417,7 +530,7 @@ async function cmdSetup({ rootDir, argv }) {
417
530
 
418
531
  authWanted = await withRl(async (rl) => {
419
532
  const v = await promptSelect(rl, {
420
- title: 'Authenticate now? (recommended)',
533
+ title: bold('Authentication'),
421
534
  options: [
422
535
  { label: 'yes (default) — enables Happy UI + mobile access', value: true },
423
536
  { label: 'no — I will authenticate later', value: false },
@@ -436,7 +549,7 @@ async function cmdSetup({ rootDir, argv }) {
436
549
  // If you choose to auth now, we’ll also start Happy in the background so login can complete.
437
550
  const authNow = await withRl(async (rl) => {
438
551
  const v = await promptSelect(rl, {
439
- title: 'Complete authentication now? (optional)',
552
+ title: bold('Authentication (optional)'),
440
553
  options: [
441
554
  { label: 'no (default) — I will do this later', value: false },
442
555
  { label: 'yes — start Happy in background and login', value: true },
@@ -453,10 +566,10 @@ async function cmdSetup({ rootDir, argv }) {
453
566
 
454
567
  installPath = await withRl(async (rl) => {
455
568
  const v = await promptSelect(rl, {
456
- title: `Add ${join(getCanonicalHomeDir(), 'bin')} to your shell PATH?`,
569
+ title: bold('Shell PATH'),
457
570
  options: [
458
- { label: 'no (default)', value: false },
459
- { 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 },
460
573
  ],
461
574
  defaultIndex: installPath ? 1 : 0,
462
575
  });
@@ -489,11 +602,12 @@ async function cmdSetup({ rootDir, argv }) {
489
602
  }
490
603
 
491
604
  // 1) Ensure plumbing exists (runtime + shims + pointer env). Avoid auto-bootstrap here; setup drives bootstrap explicitly.
492
- await runNodeScript({
493
- rootDir,
605
+ await runNodeScriptMaybeQuiet({
606
+ label: 'init happy-stacks home',
494
607
  rel: 'scripts/init.mjs',
495
608
  args: [
496
609
  '--no-bootstrap',
610
+ ...(profile === 'dev' && workspaceDirWanted ? [`--workspace-dir=${workspaceDirWanted}`] : []),
497
611
  ...(installPath ? ['--install-path'] : []),
498
612
  ],
499
613
  env: { ...process.env, HAPPY_STACKS_SETUP_CHILD: '1' },
@@ -511,13 +625,13 @@ async function cmdSetup({ rootDir, argv }) {
511
625
  // 3) Bootstrap components. Selfhost defaults to upstream; dev defaults to existing bootstrap wizard (forks by default).
512
626
  if (profile === 'dev') {
513
627
  // Developer setup: keep the existing bootstrap wizard.
514
- await runNodeScript({ rootDir, rel: 'scripts/install.mjs', args: ['--interactive'] });
628
+ await runNodeScriptMaybeQuiet({ label: 'bootstrap components', rootDir, rel: 'scripts/install.mjs', args: ['--interactive'] });
515
629
 
516
630
  // Optional: offer to create a dedicated dev stack (keeps main stable).
517
631
  if (interactive) {
518
632
  const createStack = await withRl(async (rl) => {
519
633
  return await promptSelect(rl, {
520
- title: 'Create an additional isolated stack for development?',
634
+ title: bold('Stacks'),
521
635
  options: [
522
636
  { label: 'no (default)', value: false },
523
637
  { label: 'yes', value: true },
@@ -526,7 +640,7 @@ async function cmdSetup({ rootDir, argv }) {
526
640
  });
527
641
  });
528
642
  if (createStack) {
529
- 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'] });
530
644
  }
531
645
 
532
646
  // Guided maintainer-friendly auth defaults (dev key → main → legacy).
@@ -535,7 +649,8 @@ async function cmdSetup({ rootDir, argv }) {
535
649
  } else {
536
650
  // Selfhost setup: run non-interactively and keep it simple.
537
651
  const repoFlag = serverComponent === 'happy-server-light' ? '--forks' : '--upstream';
538
- await runNodeScript({
652
+ await runNodeScriptMaybeQuiet({
653
+ label: 'bootstrap components',
539
654
  rootDir,
540
655
  rel: 'scripts/install.mjs',
541
656
  args: [`--server=${serverComponent}`, repoFlag],
@@ -661,7 +776,11 @@ async function cmdSetup({ rootDir, argv }) {
661
776
  // Final tips (keep short).
662
777
  if (profile === 'selfhost') {
663
778
  // eslint-disable-next-line no-console
664
- 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:'));
665
784
  // eslint-disable-next-line no-console
666
785
  console.log(' happys start');
667
786
  // eslint-disable-next-line no-console
@@ -670,7 +789,11 @@ async function cmdSetup({ rootDir, argv }) {
670
789
  console.log(' happys service install # macOS/Linux autostart');
671
790
  } else {
672
791
  // eslint-disable-next-line no-console
673
- 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:'));
674
797
  // eslint-disable-next-line no-console
675
798
  console.log(' happys dev');
676
799
  // eslint-disable-next-line no-console
@@ -690,4 +813,3 @@ main().catch((err) => {
690
813
  console.error('[setup] failed:', err);
691
814
  process.exit(1);
692
815
  });
693
-
@@ -22,29 +22,8 @@ import { homedir } from 'node:os';
22
22
  import { resolveMobileQrPayload } from './utils/mobile/dev_client_links.mjs';
23
23
  import { renderQrAscii } from './utils/ui/qr.mjs';
24
24
  import { inferPrStackBaseName } from './utils/stack/pr_stack_name.mjs';
25
-
26
- function supportsAnsi() {
27
- if (!process.stdout.isTTY) return false;
28
- if (process.env.NO_COLOR) return false;
29
- if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
30
- return true;
31
- }
32
-
33
- function bold(s) {
34
- return supportsAnsi() ? `\x1b[1m${s}\x1b[0m` : String(s);
35
- }
36
-
37
- function dim(s) {
38
- return supportsAnsi() ? `\x1b[2m${s}\x1b[0m` : String(s);
39
- }
40
-
41
- function cyan(s) {
42
- return supportsAnsi() ? `\x1b[36m${s}\x1b[0m` : String(s);
43
- }
44
-
45
- function green(s) {
46
- return supportsAnsi() ? `\x1b[32m${s}\x1b[0m` : String(s);
47
- }
25
+ import { bold, cyan, dim, green } from './utils/ui/ansi.mjs';
26
+ import { coerceHappyMonorepoRootFromPath, getComponentDir } from './utils/paths/paths.mjs';
48
27
 
49
28
  function pickReviewerMobileSchemeEnv(env) {
50
29
  // For review-pr flows, reviewers typically have the standard Happy dev build on their phone,
@@ -236,12 +215,12 @@ async function main() {
236
215
  json,
237
216
  data: {
238
217
  usage:
239
- 'happys setup-pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--happy-server=<pr-url|number>|--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--mobile] [--deps=none|link|install|link-or-install] [--forks|--upstream] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack>] [--link-auth|--copy-auth] [--update] [--force] [--json] [-- <stack dev/start args...>]',
218
+ 'happys setup-pr --happy=<pr-url|number> [--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--mobile] [--deps=none|link|install|link-or-install] [--forks|--upstream] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack>] [--link-auth|--copy-auth] [--update] [--force] [--json] [-- <stack dev/start args...>]',
240
219
  },
241
220
  text: [
242
221
  '[setup-pr] usage:',
243
- ' happys setup-pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--dev]',
244
- ' happys setup pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--dev] # alias',
222
+ ' happys setup-pr --happy=<pr-url|number> [--dev]',
223
+ ' happys setup pr --happy=<pr-url|number> [--dev] # alias',
245
224
  '',
246
225
  'What it does (idempotent):',
247
226
  '- ensures happy-stacks home exists (init)',
@@ -257,7 +236,11 @@ async function main() {
257
236
  'example:',
258
237
  ' happys setup-pr \\',
259
238
  ' --happy=https://github.com/slopus/happy/pull/123 \\',
260
- ' --happy-cli=https://github.com/slopus/happy-cli/pull/456',
239
+ ' --dev',
240
+ '',
241
+ 'legacy note:',
242
+ ' In the pre-monorepo split-repo era, happy-cli/happy-server had separate PRs.',
243
+ ' In monorepo mode, use --happy only (it covers UI + CLI + server).',
261
244
  ].join('\n'),
262
245
  });
263
246
  return;
@@ -276,6 +259,15 @@ async function main() {
276
259
  throw new Error('[setup-pr] cannot specify both --happy-server and --happy-server-light');
277
260
  }
278
261
 
262
+ const happyMonorepoActive = Boolean(coerceHappyMonorepoRootFromPath(getComponentDir(rootDir, 'happy', process.env)));
263
+ if (happyMonorepoActive && (prCli || prServer)) {
264
+ throw new Error(
265
+ '[setup-pr] this workspace uses the slopus/happy monorepo.\n' +
266
+ 'Fix: use --happy=<pr> only (it covers UI + CLI + server).\n' +
267
+ 'Note: --happy-cli/--happy-server are legacy flags for the pre-monorepo split repos.'
268
+ );
269
+ }
270
+
279
271
  const wantsDev = flags.has('--dev') || (!flags.has('--start') && !flags.has('--prod'));
280
272
  const wantsStart = flags.has('--start') || flags.has('--prod');
281
273
  if (wantsDev && wantsStart) {
@@ -719,4 +711,3 @@ main().catch((err) => {
719
711
  console.error('[setup-pr] failed:', err);
720
712
  process.exit(1);
721
713
  });
722
-