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.
- package/README.md +93 -40
- package/bin/happys.mjs +158 -16
- package/docs/codex-mcp-resume.md +130 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +17640 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +3845 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +102 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1452 -0
- package/docs/commit-audits/happy/leeroy-wip.manual-review-queue.md +116 -0
- package/docs/happy-development.md +3 -4
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/docs/monorepo-migration.md +286 -0
- package/docs/server-flavors.md +19 -3
- package/docs/stacks.md +35 -0
- package/package.json +5 -1
- package/scripts/auth.mjs +32 -10
- package/scripts/build.mjs +55 -8
- package/scripts/daemon.mjs +166 -10
- package/scripts/dev.mjs +198 -50
- package/scripts/doctor.mjs +0 -4
- package/scripts/edison.mjs +6 -4
- package/scripts/env.mjs +150 -0
- package/scripts/env_cmd.test.mjs +128 -0
- package/scripts/init.mjs +8 -3
- package/scripts/install.mjs +207 -69
- package/scripts/lint.mjs +24 -4
- package/scripts/migrate.mjs +3 -12
- package/scripts/mobile.mjs +88 -104
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/monorepo.mjs +1096 -0
- package/scripts/monorepo_port.test.mjs +1470 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +908 -0
- package/scripts/review_pr.mjs +353 -0
- package/scripts/run.mjs +101 -21
- package/scripts/service.mjs +2 -2
- package/scripts/setup.mjs +189 -68
- package/scripts/setup_pr.mjs +586 -38
- package/scripts/stack.mjs +990 -196
- package/scripts/stack_archive_cmd.test.mjs +91 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +65 -0
- package/scripts/stack_env_cmd.test.mjs +87 -0
- package/scripts/stack_happy_cmd.test.mjs +126 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +71 -0
- package/scripts/stack_monorepo_defaults.test.mjs +62 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +66 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +55 -0
- package/scripts/stack_shorthand_cmd.test.mjs +55 -0
- package/scripts/stack_wt_list.test.mjs +128 -0
- package/scripts/tailscale.mjs +37 -1
- package/scripts/test.mjs +45 -8
- package/scripts/tui.mjs +395 -39
- package/scripts/typecheck.mjs +24 -4
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/guided_pr_auth.mjs +79 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +75 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/auth/login_ux.mjs +32 -13
- package/scripts/utils/auth/sources.mjs +26 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +43 -4
- package/scripts/utils/cli/cwd_scope.mjs +136 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +110 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/prereqs.mjs +75 -0
- package/scripts/utils/cli/prereqs.test.mjs +34 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +17 -9
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +60 -0
- package/scripts/utils/dev/daemon.mjs +61 -4
- package/scripts/utils/dev/expo_dev.mjs +430 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/dev/server.mjs +36 -42
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/edison/git_roots.mjs +29 -0
- package/scripts/utils/edison/git_roots.test.mjs +36 -0
- package/scripts/utils/env/env.mjs +7 -3
- package/scripts/utils/env/env_file.mjs +4 -2
- package/scripts/utils/env/env_file.test.mjs +44 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/expo/expo.mjs +20 -1
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/worktrees.mjs +80 -25
- package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/mobile/config.mjs +31 -0
- package/scripts/utils/mobile/dev_client_links.mjs +60 -0
- package/scripts/utils/mobile/identifiers.mjs +47 -0
- package/scripts/utils/mobile/identifiers.test.mjs +42 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +98 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/net/ports.mjs +9 -1
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +50 -3
- package/scripts/utils/paths/paths.mjs +159 -40
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +45 -0
- package/scripts/utils/proc/commands.mjs +2 -3
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pm.mjs +176 -22
- package/scripts/utils/proc/pm_spawn.test.mjs +76 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +142 -0
- package/scripts/utils/proc/proc.mjs +136 -4
- package/scripts/utils/proc/proc.test.mjs +77 -0
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +51 -0
- package/scripts/utils/review/findings.mjs +165 -0
- package/scripts/utils/review/findings.test.mjs +85 -0
- package/scripts/utils/review/head_slice.mjs +153 -0
- package/scripts/utils/review/head_slice.test.mjs +91 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/runners/coderabbit.mjs +61 -0
- package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
- package/scripts/utils/review/runners/codex.mjs +61 -0
- package/scripts/utils/review/runners/codex.test.mjs +35 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +32 -0
- package/scripts/utils/review/targets.mjs +24 -0
- package/scripts/utils/review/targets.test.mjs +36 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +106 -0
- package/scripts/utils/server/flavor_scripts.mjs +98 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +146 -0
- package/scripts/utils/server/mobile_api_url.mjs +61 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +41 -0
- package/scripts/utils/server/prisma_import.mjs +37 -0
- package/scripts/utils/server/prisma_import.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +14 -0
- package/scripts/utils/server/ui_env.test.mjs +46 -0
- package/scripts/utils/server/urls.mjs +14 -4
- package/scripts/utils/server/validate.mjs +53 -16
- package/scripts/utils/server/validate.test.mjs +89 -0
- package/scripts/utils/service/autostart_darwin.mjs +42 -2
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +2 -2
- package/scripts/utils/stack/editor_workspace.mjs +6 -6
- package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +2 -1
- package/scripts/utils/stack/startup.mjs +120 -13
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +64 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +70 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +88 -0
- package/scripts/utils/stack/stop.mjs +15 -4
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/ui/ansi.mjs +39 -0
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/validate.mjs +88 -0
- package/scripts/where.mjs +2 -2
- package/scripts/worktrees.mjs +755 -179
- package/scripts/worktrees_archive_cmd.test.mjs +245 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +63 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +33 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +67 -0
- package/scripts/utils/dev/expo_web.mjs +0 -112
package/scripts/install.mjs
CHANGED
|
@@ -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
|
-
//
|
|
28
|
-
|
|
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
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
cli: up('happy
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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: '
|
|
178
|
+
title: `${bold('Repo source')}\n${dim('Where should Happy Stacks clone the component repos from?')}`,
|
|
124
179
|
options: [
|
|
125
|
-
{ label:
|
|
126
|
-
{ label:
|
|
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: '
|
|
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)
|
|
140
|
-
{ label: 'happy-server only (full server)
|
|
141
|
-
{ label:
|
|
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: '
|
|
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
|
-
? '
|
|
158
|
-
: '
|
|
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
|
|
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: {
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
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
|
-
|
|
281
|
-
dir:
|
|
282
|
-
|
|
381
|
+
await ensureComponentPresent({
|
|
382
|
+
dir: serverLightRepoDir,
|
|
383
|
+
label: 'SERVER',
|
|
283
384
|
repoUrl: repos.serverLight,
|
|
284
385
|
allowClone,
|
|
285
386
|
});
|
|
286
387
|
}
|
|
287
|
-
if (
|
|
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:
|
|
290
|
-
label: '
|
|
291
|
-
repoUrl: repos.
|
|
292
|
-
|
|
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(
|
|
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
|
-
|
|
319
|
-
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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 (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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:
|
|
350
|
-
|
|
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(
|
|
353
|
-
await ensureUpstreamRemote({ repoDir:
|
|
481
|
+
if (await pathExists(serverLightRepoDir)) {
|
|
482
|
+
await ensureUpstreamRemote({ repoDir: serverLightRepoDir, upstreamUrl: upstreamRepos.serverLight });
|
|
354
483
|
}
|
|
355
|
-
if (await pathExists(
|
|
356
|
-
await ensureUpstreamRemote({ repoDir:
|
|
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: {
|
|
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
|
|
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)) {
|
package/scripts/migrate.mjs
CHANGED
|
@@ -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
|
|
143
|
-
const TargetPrismaClient = await
|
|
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
|
-
|