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
@@ -1,8 +1,8 @@
1
1
  import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
3
  import { pathExists } from './utils/fs/fs.mjs';
4
- import { run } from './utils/proc/proc.mjs';
5
- import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
4
+ import { run, runCapture } from './utils/proc/proc.mjs';
5
+ import { getComponentDir, getComponentRepoDir, getRootDir, isHappyMonorepoRoot } from './utils/paths/paths.mjs';
6
6
  import { getServerComponentName } from './utils/server/server.mjs';
7
7
  import { ensureCliBuilt, ensureDepsInstalled, ensureHappyCliLocalNpmLinked } from './utils/proc/pm.mjs';
8
8
  import { dirname, join } from 'node:path';
@@ -12,6 +12,7 @@ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
12
12
  import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
13
13
  import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
14
14
  import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
15
+ import { bold, cyan, dim } from './utils/ui/ansi.mjs';
15
16
 
16
17
  /**
17
18
  * Install/setup the local stack:
@@ -24,8 +25,10 @@ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox
24
25
 
25
26
  const DEFAULT_FORK_REPOS = {
26
27
  serverLight: 'https://github.com/leeroybrun/happy-server-light.git',
27
- // We don't currently maintain a separate fork for full happy-server; default to upstream.
28
- serverFull: 'https://github.com/slopus/happy-server.git',
28
+ // Both server flavors live as branches in the same fork repo:
29
+ // - happy-server-light (sqlite)
30
+ // - happy-server (full)
31
+ serverFull: 'https://github.com/leeroybrun/happy-server-light.git',
29
32
  cli: 'https://github.com/leeroybrun/happy-cli.git',
30
33
  ui: 'https://github.com/leeroybrun/happy.git',
31
34
  };
@@ -33,8 +36,12 @@ const DEFAULT_FORK_REPOS = {
33
36
  const DEFAULT_UPSTREAM_REPOS = {
34
37
  // Upstream for server-light lives in the main happy-server repo.
35
38
  serverLight: 'https://github.com/slopus/happy-server.git',
36
- serverFull: 'https://github.com/slopus/happy-server.git',
37
- cli: 'https://github.com/slopus/happy-cli.git',
39
+ serverFull: 'https://github.com/slopus/happy.git',
40
+ // slopus/happy is now a monorepo that contains:
41
+ // - expo-app/ (UI)
42
+ // - cli/ (happy-cli)
43
+ // - server/ (happy-server)
44
+ cli: 'https://github.com/slopus/happy.git',
38
45
  ui: 'https://github.com/slopus/happy.git',
39
46
  };
40
47
 
@@ -44,15 +51,16 @@ function repoUrlsFromOwners({ forkOwner, upstreamOwner }) {
44
51
  return {
45
52
  forks: {
46
53
  serverLight: fork('happy-server-light'),
47
- serverFull: fork('happy-server') /* best-effort; user can override */,
54
+ // Fork convention: server full is a branch in happy-server-light repo (not a separate repo).
55
+ serverFull: fork('happy-server-light'),
48
56
  cli: fork('happy-cli'),
49
57
  ui: fork('happy'),
50
58
  },
51
59
  upstream: {
52
60
  // server-light upstream lives in happy-server
53
61
  serverLight: up('happy-server'),
54
- serverFull: up('happy-server'),
55
- cli: up('happy-cli'),
62
+ serverFull: up('happy'),
63
+ cli: up('happy'),
56
64
  ui: up('happy'),
57
65
  },
58
66
  };
@@ -77,15 +85,62 @@ function resolveRepoSource({ flags }) {
77
85
 
78
86
  function getRepoUrls({ repoSource }) {
79
87
  const defaults = repoSource === 'upstream' ? DEFAULT_UPSTREAM_REPOS : DEFAULT_FORK_REPOS;
88
+ const ui = process.env.HAPPY_LOCAL_UI_REPO_URL?.trim() || defaults.ui;
80
89
  return {
81
90
  // Backwards compatible: HAPPY_LOCAL_SERVER_REPO_URL historically referred to the server-light component.
82
91
  serverLight: process.env.HAPPY_LOCAL_SERVER_LIGHT_REPO_URL?.trim() || process.env.HAPPY_LOCAL_SERVER_REPO_URL?.trim() || defaults.serverLight,
83
- serverFull: process.env.HAPPY_LOCAL_SERVER_FULL_REPO_URL?.trim() || defaults.serverFull,
84
- cli: process.env.HAPPY_LOCAL_CLI_REPO_URL?.trim() || defaults.cli,
85
- ui: process.env.HAPPY_LOCAL_UI_REPO_URL?.trim() || defaults.ui,
92
+ // Default to the UI repo when using a monorepo (override to keep split repos).
93
+ serverFull: process.env.HAPPY_LOCAL_SERVER_FULL_REPO_URL?.trim() || defaults.serverFull || ui,
94
+ cli: process.env.HAPPY_LOCAL_CLI_REPO_URL?.trim() || defaults.cli || ui,
95
+ ui,
86
96
  };
87
97
  }
88
98
 
99
+ async function ensureGitBranchCheckedOut({ repoDir, branch, label }) {
100
+ if (!(await pathExists(join(repoDir, '.git')))) return;
101
+ const b = String(branch ?? '').trim();
102
+ if (!b) return;
103
+
104
+ try {
105
+ const head = (await runCapture('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoDir })).trim();
106
+ if (head && head === b) return;
107
+ } catch {
108
+ // ignore
109
+ }
110
+
111
+ // Ensure branch exists locally, otherwise fetch it from origin.
112
+ let hasLocal = true;
113
+ try {
114
+ await run('git', ['show-ref', '--verify', '--quiet', `refs/heads/${b}`], { cwd: repoDir });
115
+ } catch {
116
+ hasLocal = false;
117
+ }
118
+ if (!hasLocal) {
119
+ try {
120
+ await run('git', ['fetch', '--quiet', 'origin', b], { cwd: repoDir });
121
+ } catch {
122
+ throw new Error(
123
+ `[local] ${label}: expected branch "${b}" to exist in ${repoDir}.\n` +
124
+ `[local] Fix: use --forks for happy-server-light (sqlite), or use --server=happy-server with --upstream.`
125
+ );
126
+ }
127
+ }
128
+
129
+ try {
130
+ await run('git', ['checkout', '-q', b], { cwd: repoDir });
131
+ } catch {
132
+ // If remote-tracking branch exists but local doesn't, create it.
133
+ try {
134
+ await run('git', ['checkout', '-q', '-B', b, `origin/${b}`], { cwd: repoDir });
135
+ } catch {
136
+ throw new Error(
137
+ `[local] ${label}: failed to checkout branch "${b}" in ${repoDir}.\n` +
138
+ `[local] Fix: re-run with --force in worktree flows, or delete the checkout and re-run install/bootstrap.`
139
+ );
140
+ }
141
+ }
142
+ }
143
+
89
144
  async function ensureComponentPresent({ dir, label, repoUrl, allowClone }) {
90
145
  if (await pathExists(dir)) {
91
146
  return;
@@ -120,31 +175,33 @@ async function ensureUpstreamRemote({ repoDir, upstreamUrl }) {
120
175
  async function interactiveWizard({ rootDir, defaults }) {
121
176
  return await withRl(async (rl) => {
122
177
  const repoSource = await promptSelect(rl, {
123
- title: 'Select repo source:',
178
+ title: `${bold('Repo source')}\n${dim('Where should Happy Stacks clone the component repos from?')}`,
124
179
  options: [
125
- { label: `forks (default, recommended)`, value: 'forks' },
126
- { label: `upstream (slopus/*)`, value: 'upstream' },
180
+ { label: `${cyan('forks')} (default, recommended)`, value: 'forks' },
181
+ { label: `${cyan('upstream')} (slopus/*)`, value: 'upstream' },
127
182
  ],
128
183
  defaultIndex: defaults.repoSource === 'upstream' ? 1 : 0,
129
184
  });
130
185
 
186
+ // eslint-disable-next-line no-console
187
+ console.log(dim('Tip: keep the defaults unless you maintain your own forks.'));
131
188
  const forkOwner = await prompt(rl, `GitHub fork owner (default: ${defaults.forkOwner}): `, { defaultValue: defaults.forkOwner });
132
189
  const upstreamOwner = await prompt(rl, `GitHub upstream owner (default: ${defaults.upstreamOwner}): `, {
133
190
  defaultValue: defaults.upstreamOwner,
134
191
  });
135
192
 
136
193
  const serverMode = await promptSelect(rl, {
137
- title: 'Which server components should be set up?',
194
+ title: `${bold('Server components')}\n${dim('Choose which server repo(s) to clone and install deps for.')}`,
138
195
  options: [
139
- { label: 'happy-server-light only (default)', value: 'happy-server-light' },
140
- { label: 'happy-server only (full server)', value: 'happy-server' },
141
- { label: 'both (server-light + full server)', value: 'both' },
196
+ { label: `${cyan('happy-server-light')} only (default)`, value: 'happy-server-light' },
197
+ { label: `${cyan('happy-server')} only (full server)`, value: 'happy-server' },
198
+ { label: `both (${cyan('server-light')} + ${cyan('full server')})`, value: 'both' },
142
199
  ],
143
200
  defaultIndex: defaults.serverComponentName === 'both' ? 2 : defaults.serverComponentName === 'happy-server' ? 1 : 0,
144
201
  });
145
202
 
146
203
  const allowClone = await promptSelect(rl, {
147
- title: 'Clone missing component repos?',
204
+ title: `${bold('Cloning')}\n${dim('If repos are missing under components/, should we clone them automatically?')}`,
148
205
  options: [
149
206
  { label: 'yes (default)', value: true },
150
207
  { label: 'no', value: false },
@@ -154,8 +211,8 @@ async function interactiveWizard({ rootDir, defaults }) {
154
211
 
155
212
  const enableAutostart = await promptSelect(rl, {
156
213
  title: isSandboxed()
157
- ? 'Enable macOS autostart (LaunchAgent)? (NOTE: sandbox mode; this is global OS state)'
158
- : 'Enable macOS autostart (LaunchAgent)?',
214
+ ? `${bold('Autostart (macOS)')}\n${dim('Sandbox mode: this is global OS state; normally disabled in sandbox.')}`
215
+ : `${bold('Autostart (macOS)')}\n${dim('Install a LaunchAgent so Happy starts at login?')}`,
159
216
  options: [
160
217
  { label: 'no (default)', value: false },
161
218
  { label: 'yes', value: true },
@@ -164,7 +221,7 @@ async function interactiveWizard({ rootDir, defaults }) {
164
221
  });
165
222
 
166
223
  const buildTauri = await promptSelect(rl, {
167
- title: 'Build Tauri desktop app as part of setup?',
224
+ title: `${bold('Desktop app (optional)')}\n${dim('Build the Tauri desktop app as part of setup? (slow; requires extra toolchain)')}`,
168
225
  options: [
169
226
  { label: 'no (default)', value: false },
170
227
  { label: 'yes', value: true },
@@ -173,7 +230,7 @@ async function interactiveWizard({ rootDir, defaults }) {
173
230
  });
174
231
 
175
232
  const configureGit = await promptSelect(rl, {
176
- title: 'Configure upstream Git remotes and create mirror branches (slopus/main)?',
233
+ title: `${bold('Git remotes')}\n${dim('Configure upstream remotes and create/update mirror branches (e.g. slopus/main)?')}`,
177
234
  options: [
178
235
  { label: 'yes (default)', value: true },
179
236
  { label: 'no', value: false },
@@ -201,7 +258,22 @@ async function main() {
201
258
  if (wantsHelp(argv, { flags })) {
202
259
  printResult({
203
260
  json,
204
- data: { flags: ['--forks', '--upstream', '--clone', '--no-clone', '--autostart', '--no-autostart', '--server=...'], json: true },
261
+ data: {
262
+ flags: [
263
+ '--forks',
264
+ '--upstream',
265
+ '--clone',
266
+ '--no-clone',
267
+ '--autostart',
268
+ '--no-autostart',
269
+ '--server=...',
270
+ '--no-ui-build',
271
+ '--no-ui-deps',
272
+ '--no-cli-deps',
273
+ '--no-cli-build',
274
+ ],
275
+ json: true,
276
+ },
205
277
  text: [
206
278
  '[bootstrap] usage:',
207
279
  ' happys bootstrap [--forks|--upstream] [--server=happy-server|happy-server-light|both] [--json]',
@@ -270,70 +342,125 @@ async function main() {
270
342
  const disableAutostart = flags.has('--no-autostart');
271
343
 
272
344
  const serverComponentName = (wizard?.serverComponentName ?? getServerComponentName({ kv })).trim();
273
- const serverLightDir = getComponentDir(rootDir, 'happy-server-light');
274
- const serverFullDir = getComponentDir(rootDir, 'happy-server');
275
- const cliDir = getComponentDir(rootDir, 'happy-cli');
345
+ // Safety: upstream server-light is not a separate upstream repo/branch today.
346
+ // Upstream slopus/happy-server is Postgres-only, while happy-server-light requires sqlite.
347
+ if (repoSource === 'upstream' && (serverComponentName === 'happy-server-light' || serverComponentName === 'both')) {
348
+ throw new Error(
349
+ `[bootstrap] --upstream is not supported for happy-server-light (sqlite).\n` +
350
+ `Reason: upstream ${DEFAULT_UPSTREAM_REPOS.serverLight} does not provide a happy-server-light branch.\n` +
351
+ `Fix:\n` +
352
+ `- use --forks (recommended), OR\n` +
353
+ `- use --server=happy-server with --upstream`
354
+ );
355
+ }
356
+ // Repo roots (clone locations)
357
+ const uiRepoDir = getComponentRepoDir(rootDir, 'happy');
358
+ const serverLightRepoDir = getComponentRepoDir(rootDir, 'happy-server-light');
359
+
360
+ // Ensure UI exists first (monorepo anchor in slopus/happy).
361
+ await ensureComponentPresent({
362
+ dir: uiRepoDir,
363
+ label: 'UI',
364
+ repoUrl: repos.ui,
365
+ allowClone,
366
+ });
367
+
368
+ // Package dirs (where we run installs/builds). Recompute after cloning UI.
276
369
  const uiDir = getComponentDir(rootDir, 'happy');
370
+ const cliDir = getComponentDir(rootDir, 'happy-cli');
371
+ const serverFullDir = getComponentDir(rootDir, 'happy-server');
372
+
373
+ const cliRepoDir = getComponentRepoDir(rootDir, 'happy-cli');
374
+ const serverFullRepoDir = getComponentRepoDir(rootDir, 'happy-server');
375
+ const hasMonorepo = isHappyMonorepoRoot(uiRepoDir);
277
376
 
278
- // Ensure components exist (embedded layout)
377
+ // Ensure other components exist.
378
+ // - server-light stays separate for now.
379
+ // - full server + cli may be embedded in the monorepo.
279
380
  if (serverComponentName === 'both' || serverComponentName === 'happy-server-light') {
280
- await ensureComponentPresent({
281
- dir: serverLightDir,
282
- label: 'SERVER',
381
+ await ensureComponentPresent({
382
+ dir: serverLightRepoDir,
383
+ label: 'SERVER',
283
384
  repoUrl: repos.serverLight,
284
385
  allowClone,
285
386
  });
286
387
  }
287
- if (serverComponentName === 'both' || serverComponentName === 'happy-server') {
388
+ if (!hasMonorepo) {
389
+ if (serverComponentName === 'both' || serverComponentName === 'happy-server') {
390
+ await ensureComponentPresent({
391
+ dir: serverFullRepoDir,
392
+ label: 'SERVER_FULL',
393
+ repoUrl: repos.serverFull,
394
+ allowClone,
395
+ });
396
+ }
288
397
  await ensureComponentPresent({
289
- dir: serverFullDir,
290
- label: 'SERVER_FULL',
291
- repoUrl: repos.serverFull,
292
- allowClone,
293
- });
398
+ dir: cliRepoDir,
399
+ label: 'CLI',
400
+ repoUrl: repos.cli,
401
+ allowClone,
402
+ });
403
+ } else {
404
+ if ((serverComponentName === 'both' || serverComponentName === 'happy-server') && !(await pathExists(serverFullDir))) {
405
+ throw new Error(`[bootstrap] expected monorepo server package at ${serverFullDir} (missing).`);
406
+ }
407
+ if (!(await pathExists(cliDir))) {
408
+ throw new Error(`[bootstrap] expected monorepo cli package at ${cliDir} (missing).`);
409
+ }
410
+ }
411
+
412
+ // Ensure expected branches are checked out for server flavors (avoids "server-light directory contains full server" mistakes).
413
+ if (serverComponentName === 'both' || serverComponentName === 'happy-server-light') {
414
+ await ensureGitBranchCheckedOut({ repoDir: serverLightRepoDir, branch: 'happy-server-light', label: 'SERVER' });
415
+ }
416
+ if (serverComponentName === 'both' || serverComponentName === 'happy-server') {
417
+ // In fork mode (split repos), full server is a branch in the fork server repo.
418
+ // In upstream mode and in monorepo mode, use main.
419
+ const serverFullBranch = isHappyMonorepoRoot(serverFullRepoDir) ? 'main' : repoSource === 'upstream' ? 'main' : 'happy-server';
420
+ await ensureGitBranchCheckedOut({ repoDir: serverFullRepoDir, branch: serverFullBranch, label: 'SERVER_FULL' });
294
421
  }
295
- await ensureComponentPresent({
296
- dir: cliDir,
297
- label: 'CLI',
298
- repoUrl: repos.cli,
299
- allowClone,
300
- });
301
- await ensureComponentPresent({
302
- dir: uiDir,
303
- label: 'UI',
304
- repoUrl: repos.ui,
305
- allowClone,
306
- });
307
422
 
308
423
  const cliDirFinal = cliDir;
309
424
  const uiDirFinal = uiDir;
310
425
 
311
426
  // Install deps
427
+ const skipUiDeps = flags.has('--no-ui-deps') || (process.env.HAPPY_STACKS_INSTALL_NO_UI_DEPS ?? '').trim() === '1';
428
+ const skipCliDeps = flags.has('--no-cli-deps') || (process.env.HAPPY_STACKS_INSTALL_NO_CLI_DEPS ?? '').trim() === '1';
312
429
  if (serverComponentName === 'both' || serverComponentName === 'happy-server-light') {
313
- await ensureDepsInstalled(serverLightDir, 'happy-server-light');
430
+ await ensureDepsInstalled(getComponentDir(rootDir, 'happy-server-light'), 'happy-server-light');
314
431
  }
315
432
  if (serverComponentName === 'both' || serverComponentName === 'happy-server') {
316
433
  await ensureDepsInstalled(serverFullDir, 'happy-server');
317
434
  }
318
- await ensureDepsInstalled(uiDirFinal, 'happy');
319
- await ensureDepsInstalled(cliDirFinal, 'happy-cli');
435
+ if (!skipUiDeps) {
436
+ await ensureDepsInstalled(uiDirFinal, 'happy');
437
+ }
438
+ if (!skipCliDeps) {
439
+ await ensureDepsInstalled(cliDirFinal, 'happy-cli');
440
+ }
320
441
 
321
442
  // CLI build + link
322
- const buildCli = (process.env.HAPPY_LOCAL_CLI_BUILD ?? '1') !== '0';
323
- const npmLinkCli = (process.env.HAPPY_LOCAL_NPM_LINK ?? '1') !== '0';
324
- await ensureCliBuilt(cliDirFinal, { buildCli });
325
- await ensureHappyCliLocalNpmLinked(rootDir, { npmLinkCli });
443
+ const skipCliBuild = flags.has('--no-cli-build') || (process.env.HAPPY_STACKS_INSTALL_NO_CLI_BUILD ?? '').trim() === '1';
444
+ if (!skipCliBuild) {
445
+ const buildCli = (process.env.HAPPY_LOCAL_CLI_BUILD ?? '1') !== '0';
446
+ const npmLinkCli = (process.env.HAPPY_LOCAL_NPM_LINK ?? '1') !== '0';
447
+ await ensureCliBuilt(cliDirFinal, { buildCli });
448
+ await ensureHappyCliLocalNpmLinked(rootDir, { npmLinkCli });
449
+ }
326
450
 
327
451
  // Build UI (so run works without expo dev server)
452
+ const skipUiBuild = flags.has('--no-ui-build') || (process.env.HAPPY_STACKS_INSTALL_NO_UI_BUILD ?? '').trim() === '1';
328
453
  const buildArgs = [join(rootDir, 'scripts', 'build.mjs')];
329
454
  // Tauri builds are opt-in (slow + requires additional toolchain).
330
455
  const buildTauri = wizard?.buildTauri ?? (flags.has('--tauri') && !flags.has('--no-tauri'));
331
- if (buildTauri) {
332
- buildArgs.push('--tauri');
333
- } else if (flags.has('--no-tauri')) {
334
- buildArgs.push('--no-tauri');
456
+ if (!skipUiBuild) {
457
+ if (buildTauri) {
458
+ buildArgs.push('--tauri');
459
+ } else if (flags.has('--no-tauri')) {
460
+ buildArgs.push('--no-tauri');
461
+ }
462
+ await run(process.execPath, buildArgs, { cwd: rootDir });
335
463
  }
336
- await run(process.execPath, buildArgs, { cwd: rootDir });
337
464
 
338
465
  // Optional autostart (macOS)
339
466
  if (disableAutostart) {
@@ -346,14 +473,16 @@ async function main() {
346
473
  if (wizard?.configureGit) {
347
474
  // Ensure upstream remotes exist so `happys wt sync-all` works consistently.
348
475
  const upstreamRepos = getRepoUrls({ repoSource: 'upstream' });
349
- await ensureUpstreamRemote({ repoDir: uiDir, upstreamUrl: upstreamRepos.ui });
350
- await ensureUpstreamRemote({ repoDir: cliDir, upstreamUrl: upstreamRepos.cli });
476
+ await ensureUpstreamRemote({ repoDir: uiRepoDir, upstreamUrl: upstreamRepos.ui });
477
+ if (cliRepoDir !== uiRepoDir) {
478
+ await ensureUpstreamRemote({ repoDir: cliRepoDir, upstreamUrl: upstreamRepos.cli });
479
+ }
351
480
  // server-light and server-full both track upstream happy-server
352
- if (await pathExists(serverLightDir)) {
353
- await ensureUpstreamRemote({ repoDir: serverLightDir, upstreamUrl: upstreamRepos.serverLight });
481
+ if (await pathExists(serverLightRepoDir)) {
482
+ await ensureUpstreamRemote({ repoDir: serverLightRepoDir, upstreamUrl: upstreamRepos.serverLight });
354
483
  }
355
- if (await pathExists(serverFullDir)) {
356
- await ensureUpstreamRemote({ repoDir: serverFullDir, upstreamUrl: upstreamRepos.serverFull });
484
+ if (serverFullRepoDir !== uiRepoDir && (await pathExists(serverFullRepoDir))) {
485
+ await ensureUpstreamRemote({ repoDir: serverFullRepoDir, upstreamUrl: upstreamRepos.serverFull });
357
486
  }
358
487
 
359
488
  // Create/update mirror branches like slopus/main for each repo (best-effort).
@@ -370,7 +499,16 @@ async function main() {
370
499
  ok: true,
371
500
  repoSource,
372
501
  serverComponentName,
373
- dirs: { serverLightDir, serverFullDir, cliDir: cliDirFinal, uiDir: uiDirFinal },
502
+ dirs: {
503
+ uiRepoDir,
504
+ uiDir: uiDirFinal,
505
+ cliRepoDir,
506
+ cliDir: cliDirFinal,
507
+ serverLightRepoDir,
508
+ serverLightDir: getComponentDir(rootDir, 'happy-server-light'),
509
+ serverFullRepoDir,
510
+ serverFullDir,
511
+ },
374
512
  cloned: allowClone,
375
513
  autostart: enableAutostart ? 'enabled' : sandboxed && enableAutostartRaw && !allowGlobal ? 'skipped (sandbox)' : disableAutostart ? 'disabled' : 'unchanged',
376
514
  interactive: Boolean(wizard),
package/scripts/lint.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import './utils/env/env.mjs';
2
2
  import { parseArgs } from './utils/cli/args.mjs';
3
3
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
4
- import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
4
+ import { componentDirEnvKey, getComponentDir, getRootDir } from './utils/paths/paths.mjs';
5
5
  import { ensureDepsInstalled } from './utils/proc/pm.mjs';
6
6
  import { pathExists } from './utils/fs/fs.mjs';
7
7
  import { run } from './utils/proc/proc.mjs';
8
8
  import { detectPackageManagerCmd, pickFirstScript, readPackageJsonScripts } from './utils/proc/package_scripts.mjs';
9
+ import { getInvokedCwd, inferComponentFromCwd } from './utils/cli/cwd_scope.mjs';
9
10
 
10
11
  const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
11
12
 
@@ -40,18 +41,37 @@ async function main() {
40
41
  'examples:',
41
42
  ' happys lint',
42
43
  ' happys lint happy happy-cli',
44
+ '',
45
+ 'note:',
46
+ ' If run from inside a component checkout/worktree and no components are provided, defaults to that component.',
43
47
  ].join('\n'),
44
48
  });
45
49
  return;
46
50
  }
47
51
 
52
+ const rootDir = getRootDir(import.meta.url);
53
+
48
54
  const positionals = argv.filter((a) => !a.startsWith('--'));
49
- const requested = positionals.length ? positionals : ['all'];
55
+ const inferred =
56
+ positionals.length === 0
57
+ ? inferComponentFromCwd({
58
+ rootDir,
59
+ invokedCwd: getInvokedCwd(process.env),
60
+ components: DEFAULT_COMPONENTS,
61
+ })
62
+ : null;
63
+ if (inferred) {
64
+ const stacksKey = componentDirEnvKey(inferred.component);
65
+ const legacyKey = stacksKey.replace(/^HAPPY_STACKS_/, 'HAPPY_LOCAL_');
66
+ if (!(process.env[stacksKey] ?? '').toString().trim() && !(process.env[legacyKey] ?? '').toString().trim()) {
67
+ process.env[stacksKey] = inferred.repoDir;
68
+ }
69
+ }
70
+
71
+ const requested = positionals.length ? positionals : inferred ? [inferred.component] : ['all'];
50
72
  const wantAll = requested.includes('all');
51
73
  const components = wantAll ? DEFAULT_COMPONENTS : requested;
52
74
 
53
- const rootDir = getRootDir(import.meta.url);
54
-
55
75
  const results = [];
56
76
  for (const component of components) {
57
77
  if (!DEFAULT_COMPONENTS.includes(component)) {
@@ -1,7 +1,6 @@
1
1
  import './utils/env/env.mjs';
2
2
  import { copyFile, mkdir, readFile } from 'node:fs/promises';
3
3
  import { basename, join } from 'node:path';
4
- import { createRequire } from 'node:module';
5
4
 
6
5
  import { parseArgs } from './utils/cli/args.mjs';
7
6
  import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
@@ -13,6 +12,7 @@ import { ensureHappyServerManagedInfra, applyHappyServerMigrations } from './uti
13
12
  import { runCapture } from './utils/proc/proc.mjs';
14
13
  import { pickNextFreeTcpPort } from './utils/net/ports.mjs';
15
14
  import { getEnvValue } from './utils/env/values.mjs';
15
+ import { importPrismaClientForHappyServerLight, importPrismaClientFromNodeModules } from './utils/server/prisma_import.mjs';
16
16
 
17
17
  function usage() {
18
18
  return [
@@ -54,14 +54,6 @@ async function ensureTargetSecretMatchesSource({ sourceSecretPath, targetSecretP
54
54
  }
55
55
  }
56
56
 
57
- async function importPrismaClientFrom(dir) {
58
- const req = createRequire(import.meta.url);
59
- const resolved = req.resolve('@prisma/client', { paths: [dir] });
60
- // eslint-disable-next-line import/no-dynamic-require
61
- const mod = req(resolved);
62
- return mod.PrismaClient;
63
- }
64
-
65
57
  async function migrateLightToServer({ rootDir, fromStack, toStack, includeFiles, force, json }) {
66
58
  const from = resolveStackEnvPath(fromStack);
67
59
  const to = resolveStackEnvPath(toStack);
@@ -139,8 +131,8 @@ async function migrateLightToServer({ rootDir, fromStack, toStack, includeFiles,
139
131
  await copyFile(fromParsed.path, snapshotPath);
140
132
  const snapshotDbUrl = `file:${snapshotPath}`;
141
133
 
142
- const SourcePrismaClient = await importPrismaClientFrom(lightDir);
143
- const TargetPrismaClient = await importPrismaClientFrom(fullDir);
134
+ const SourcePrismaClient = await importPrismaClientForHappyServerLight({ serverDir: lightDir });
135
+ const TargetPrismaClient = await importPrismaClientFromNodeModules({ dir: fullDir });
144
136
 
145
137
  const sourceDb = new SourcePrismaClient({ datasources: { db: { url: snapshotDbUrl } } });
146
138
  const targetDb = new TargetPrismaClient({ datasources: { db: { url: infra.env.DATABASE_URL } } });
@@ -289,4 +281,3 @@ main().catch((err) => {
289
281
  console.error(err);
290
282
  process.exit(1);
291
283
  });
292
-