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.
- package/README.md +64 -33
- package/bin/happys.mjs +44 -1
- 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 +1 -2
- package/docs/monorepo-migration.md +286 -0
- package/docs/server-flavors.md +19 -3
- package/docs/stacks.md +35 -0
- package/package.json +1 -1
- package/scripts/auth.mjs +21 -3
- package/scripts/build.mjs +1 -1
- package/scripts/dev.mjs +20 -7
- package/scripts/doctor.mjs +0 -4
- package/scripts/edison.mjs +2 -2
- package/scripts/env.mjs +150 -0
- package/scripts/env_cmd.test.mjs +128 -0
- package/scripts/init.mjs +5 -2
- package/scripts/install.mjs +99 -57
- package/scripts/migrate.mjs +3 -12
- package/scripts/monorepo.mjs +1096 -0
- package/scripts/monorepo_port.test.mjs +1470 -0
- package/scripts/review.mjs +715 -24
- package/scripts/review_pr.mjs +5 -20
- package/scripts/run.mjs +21 -15
- package/scripts/setup.mjs +147 -25
- package/scripts/setup_pr.mjs +19 -28
- package/scripts/stack.mjs +493 -157
- 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/tui.mjs +88 -2
- package/scripts/utils/cli/cli_registry.mjs +20 -5
- package/scripts/utils/cli/cwd_scope.mjs +56 -2
- package/scripts/utils/cli/cwd_scope.test.mjs +40 -7
- package/scripts/utils/cli/prereqs.mjs +8 -5
- package/scripts/utils/cli/prereqs.test.mjs +34 -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 +14 -1
- package/scripts/utils/dev/expo_dev.mjs +188 -4
- package/scripts/utils/dev/server.mjs +21 -17
- 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/git/worktrees.mjs +63 -12
- package/scripts/utils/git/worktrees_monorepo.test.mjs +54 -0
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/paths/paths.mjs +118 -3
- 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/pm.mjs +113 -16
- 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 +68 -10
- package/scripts/utils/proc/proc.test.mjs +77 -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 +56 -14
- package/scripts/utils/review/runners/coderabbit.test.mjs +59 -0
- package/scripts/utils/review/runners/codex.mjs +32 -22
- 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/server/flavor_scripts.mjs +98 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +146 -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/validate.mjs +53 -16
- package/scripts/utils/server/validate.test.mjs +89 -0
- package/scripts/utils/stack/editor_workspace.mjs +4 -4
- package/scripts/utils/stack/interactive_stack_config.mjs +185 -0
- package/scripts/utils/stack/startup.mjs +113 -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/tailscale/ip.mjs +116 -0
- package/scripts/utils/ui/ansi.mjs +39 -0
- package/scripts/where.mjs +2 -2
- package/scripts/worktrees.mjs +627 -137
- 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/review_pr.mjs
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
-
|
|
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
|
|
149
|
-
console.log(`[local] UI build directory not found at ${uiBuildDir}; UI
|
|
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
|
|
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:
|
|
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
|
|
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: '
|
|
327
|
+
title: bold(`✨ ${cyan('Happy Stacks')} setup ✨\n\nWhat is your goal?`),
|
|
308
328
|
options: [
|
|
309
|
-
{ label: '
|
|
310
|
-
{ label: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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:
|
|
569
|
+
title: bold('Shell PATH'),
|
|
457
570
|
options: [
|
|
458
|
-
{ label:
|
|
459
|
-
{ label:
|
|
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
|
|
493
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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('
|
|
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('
|
|
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
|
-
|
package/scripts/setup_pr.mjs
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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> [--
|
|
244
|
-
' happys setup pr --happy=<pr-url|number> [--
|
|
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
|
-
' --
|
|
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
|
-
|