happy-stacks 0.2.0 → 0.4.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 +84 -25
- package/bin/happys.mjs +116 -17
- package/docs/happy-development.md +2 -2
- package/docs/isolated-linux-vm.md +82 -0
- package/docs/mobile-ios.md +112 -54
- package/package.json +5 -1
- package/scripts/auth.mjs +59 -208
- package/scripts/build.mjs +58 -12
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +168 -20
- package/scripts/dev.mjs +196 -70
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +105 -78
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +8 -14
- package/scripts/install.mjs +119 -23
- package/scripts/lint.mjs +31 -32
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +93 -108
- package/scripts/mobile_dev_client.mjs +83 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +51 -0
- package/scripts/review.mjs +217 -0
- package/scripts/review_pr.mjs +368 -0
- package/scripts/run.mjs +95 -21
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +19 -29
- package/scripts/setup.mjs +63 -160
- package/scripts/setup_pr.mjs +592 -52
- package/scripts/stack.mjs +608 -200
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +44 -11
- package/scripts/test.mjs +52 -36
- package/scripts/tui.mjs +314 -74
- package/scripts/typecheck.mjs +31 -32
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/auth/daemon_gate.mjs +55 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +37 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- 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/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +72 -0
- package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +32 -13
- package/scripts/utils/auth/sources.mjs +38 -0
- package/scripts/utils/auth/stack_guided_login.mjs +353 -0
- package/scripts/utils/cli/cli_registry.mjs +24 -0
- package/scripts/utils/cli/cwd_scope.mjs +82 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +77 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +72 -0
- package/scripts/utils/cli/progress.mjs +126 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- package/scripts/utils/cli/verbosity.mjs +12 -0
- package/scripts/utils/cli/wizard.mjs +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/{dev_daemon.mjs → dev/daemon.mjs} +51 -7
- package/scripts/utils/dev/expo_dev.mjs +246 -0
- package/scripts/utils/dev/expo_dev.test.mjs +76 -0
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +22 -32
- package/scripts/utils/dev_auth_key.mjs +1 -1
- package/scripts/utils/{config.mjs → env/config.mjs} +3 -2
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +5 -3
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +2 -1
- package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +52 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +23 -10
- package/scripts/utils/expo/metro_ports.mjs +114 -0
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +27 -23
- package/scripts/utils/handy_master_secret.mjs +2 -2
- 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/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +12 -6
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +56 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +52 -45
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +3 -1
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/{ownership.mjs → proc/ownership.mjs} +1 -1
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +128 -158
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +77 -2
- package/scripts/utils/review/base_ref.mjs +74 -0
- package/scripts/utils/review/base_ref.test.mjs +54 -0
- package/scripts/utils/review/runners/coderabbit.mjs +19 -0
- package/scripts/utils/review/runners/codex.mjs +51 -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/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- 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/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +101 -0
- package/scripts/utils/server/validate.mjs +88 -0
- package/scripts/utils/service/autostart_darwin.mjs +182 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +50 -0
- package/scripts/utils/stack/context.mjs +23 -0
- package/scripts/utils/stack/dirs.mjs +27 -0
- package/scripts/utils/stack/editor_workspace.mjs +152 -0
- package/scripts/utils/stack/names.mjs +12 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +9 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +24 -19
- package/scripts/utils/stack_context.mjs +3 -3
- package/scripts/utils/stack_runtime_state.mjs +1 -1
- package/scripts/utils/stacks.mjs +2 -2
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/validate.mjs +1 -1
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +171 -113
- package/scripts/utils/auth_sources.mjs +0 -12
- package/scripts/utils/dev_expo_web.mjs +0 -112
- package/scripts/utils/localhost_host.mjs +0 -17
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{sandbox.mjs → env/sandbox.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
- /package/scripts/utils/{canonical_home.mjs → paths/canonical_home.mjs} +0 -0
- /package/scripts/utils/{watch.mjs → proc/watch.mjs} +0 -0
package/scripts/edison.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
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 { resolveStackEnvPath, getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import {
|
|
6
|
-
import { pathExists } from './utils/fs.mjs';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { resolveStackEnvPath, getComponentDir, getRootDir } from './utils/paths/paths.mjs';
|
|
5
|
+
import { parseEnvToObject } from './utils/env/dotenv.mjs';
|
|
6
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
7
|
+
import { readTextOrEmpty } from './utils/fs/ops.mjs';
|
|
8
|
+
import { readJsonIfExists } from './utils/fs/json.mjs';
|
|
9
|
+
import { isPidAlive } from './utils/proc/pids.mjs';
|
|
10
|
+
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
11
|
+
import { preferStackLocalhostHost, resolveLocalhostHost } from './utils/paths/localhost_host.mjs';
|
|
12
|
+
import { sanitizeStackName } from './utils/stack/names.mjs';
|
|
9
13
|
import { join } from 'node:path';
|
|
10
14
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import { readFile } from 'node:fs/promises';
|
|
12
15
|
import { mkdir, lstat, rename, symlink, writeFile, readdir, chmod } from 'node:fs/promises';
|
|
13
16
|
import os from 'node:os';
|
|
14
17
|
|
|
@@ -26,25 +29,7 @@ function cleanHappyStacksEnv(baseEnv) {
|
|
|
26
29
|
return cleaned;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const raw = await readFile(path, 'utf-8');
|
|
32
|
-
return raw;
|
|
33
|
-
} catch {
|
|
34
|
-
return '';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function readJsonIfExists(path) {
|
|
39
|
-
try {
|
|
40
|
-
if (!path || !(await pathExists(path))) return null;
|
|
41
|
-
const raw = await readFile(path, 'utf-8');
|
|
42
|
-
const parsed = JSON.parse(raw);
|
|
43
|
-
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
32
|
+
const readExistingEnv = readTextOrEmpty;
|
|
48
33
|
|
|
49
34
|
function inferServerPortFromRuntimeState(runtimeState) {
|
|
50
35
|
try {
|
|
@@ -56,17 +41,6 @@ function inferServerPortFromRuntimeState(runtimeState) {
|
|
|
56
41
|
}
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
function isPidAlive(pid) {
|
|
60
|
-
const n = Number(pid);
|
|
61
|
-
if (!Number.isFinite(n) || n <= 1) return false;
|
|
62
|
-
try {
|
|
63
|
-
process.kill(n, 0);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
44
|
function isRuntimeStateAlive(runtimeState) {
|
|
71
45
|
try {
|
|
72
46
|
const ownerPid = runtimeState?.ownerPid;
|
|
@@ -376,11 +350,6 @@ async function inferTaskIdFromArgs({ rootDir, edisonArgs }) {
|
|
|
376
350
|
return '';
|
|
377
351
|
}
|
|
378
352
|
|
|
379
|
-
function parseEnvToObject(raw) {
|
|
380
|
-
const parsed = parseDotenv(raw);
|
|
381
|
-
return Object.fromEntries(parsed.entries());
|
|
382
|
-
}
|
|
383
|
-
|
|
384
353
|
function resolveComponentDirsFromStackEnv({ rootDir, stackEnv }) {
|
|
385
354
|
const out = [];
|
|
386
355
|
|
|
@@ -452,17 +421,6 @@ function resolveComponentsFromFrontmatter(fm) {
|
|
|
452
421
|
return [];
|
|
453
422
|
}
|
|
454
423
|
|
|
455
|
-
function sanitizeStackName(raw) {
|
|
456
|
-
return String(raw ?? '')
|
|
457
|
-
.trim()
|
|
458
|
-
.toLowerCase()
|
|
459
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
460
|
-
.replace(/-+/g, '-')
|
|
461
|
-
.replace(/^-+/, '')
|
|
462
|
-
.replace(/-+$/, '')
|
|
463
|
-
.slice(0, 64);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
424
|
function yamlQuote(v) {
|
|
467
425
|
const s = String(v ?? '');
|
|
468
426
|
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
@@ -608,12 +566,13 @@ async function cmdTaskScaffold({ rootDir, argv, json }) {
|
|
|
608
566
|
const taskId = positionals[1]?.trim?.() ? positionals[1].trim() : '';
|
|
609
567
|
if (!taskId) {
|
|
610
568
|
throw new Error(
|
|
611
|
-
'[edison] usage: happys edison task:scaffold <task-id> [--mode=upstream|fork|both] [--tracks=upstream,fork] [--yes] [--json]'
|
|
569
|
+
'[edison] usage: happys edison task:scaffold <task-id> [--mode=upstream|fork|both] [--tracks=upstream,fork] [--yes] [--reuse-only] [--json]'
|
|
612
570
|
);
|
|
613
571
|
}
|
|
614
572
|
|
|
615
573
|
const mode = (kv.get('--mode') ?? '').trim().toLowerCase() || 'upstream';
|
|
616
574
|
const yes = flags.has('--yes');
|
|
575
|
+
const reuseOnly = flags.has('--reuse-only') || (kv.get('--reuse-only') ?? '').trim() === '1';
|
|
617
576
|
|
|
618
577
|
const taskPath = join(rootDir, '.project', 'tasks');
|
|
619
578
|
const taskGlobRoots = ['todo', 'wip', 'done', 'validated', 'blocked'];
|
|
@@ -722,12 +681,32 @@ async function cmdTaskScaffold({ rootDir, argv, json }) {
|
|
|
722
681
|
createdTasks.push({ id: trackTaskId, kind: 'track', stack, path: trackRes.path, created: trackRes.created });
|
|
723
682
|
|
|
724
683
|
const stackExists = Array.isArray(stacks) && stacks.some((s) => String(s?.name ?? '') === stack);
|
|
684
|
+
const expectedStackRemote = track === 'fork' || track === 'integration' ? 'origin' : 'upstream';
|
|
725
685
|
if (!stackExists) {
|
|
726
|
-
await run('node', ['./bin/happys.mjs', 'stack', 'new', stack, '--json'], { cwd: rootDir });
|
|
686
|
+
await run('node', ['./bin/happys.mjs', 'stack', 'new', stack, `--remote=${expectedStackRemote}`, '--json'], { cwd: rootDir });
|
|
727
687
|
createdStacks.push({ stack });
|
|
728
688
|
stacks.push({ name: stack });
|
|
729
689
|
}
|
|
730
690
|
|
|
691
|
+
// If the stack already exists, reuse pinned worktrees when possible.
|
|
692
|
+
let stackInfo = null;
|
|
693
|
+
if (stackExists) {
|
|
694
|
+
const raw = await runCapture('node', ['./bin/happys.mjs', 'stack', 'info', stack, '--json'], { cwd: rootDir });
|
|
695
|
+
stackInfo = JSON.parse(raw);
|
|
696
|
+
const actualRemote = String(stackInfo?.stackRemote ?? '').trim() || 'upstream';
|
|
697
|
+
if (actualRemote !== expectedStackRemote) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`[edison] stack remote mismatch for track "${track}".\n` +
|
|
700
|
+
`- stack: ${stack}\n` +
|
|
701
|
+
`- expected: ${expectedStackRemote}\n` +
|
|
702
|
+
`- actual: ${actualRemote}\n\n` +
|
|
703
|
+
`Fix:\n` +
|
|
704
|
+
`- run: happys stack edit ${stack} --interactive\n` +
|
|
705
|
+
`- set: Git remote for creating new worktrees = ${expectedStackRemote}\n`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
731
710
|
const qaRes = await ensureQaFile({
|
|
732
711
|
rootDir,
|
|
733
712
|
taskId: trackTaskId,
|
|
@@ -750,7 +729,11 @@ async function cmdTaskScaffold({ rootDir, argv, json }) {
|
|
|
750
729
|
const compTaskId = existingComp?.id || nextChildId(trackTaskId, existingIds);
|
|
751
730
|
existingIds.add(compTaskId);
|
|
752
731
|
const compTitle = `Component: ${c} (${track})`;
|
|
753
|
-
const
|
|
732
|
+
const pinnedSpec =
|
|
733
|
+
stackInfo && Array.isArray(stackInfo.components)
|
|
734
|
+
? String(stackInfo.components.find((x) => x?.component === c)?.worktreeSpec ?? '').trim()
|
|
735
|
+
: '';
|
|
736
|
+
const baseWorktree = String(existingComp?.fm?.base_worktree ?? '').trim() || pinnedSpec || `edison/${compTaskId}`;
|
|
754
737
|
const compFm = {
|
|
755
738
|
id: compTaskId,
|
|
756
739
|
title: compTitle,
|
|
@@ -785,16 +768,32 @@ async function cmdTaskScaffold({ rootDir, argv, json }) {
|
|
|
785
768
|
});
|
|
786
769
|
createdQas.push({ id: `${compTaskId}-qa`, path: qa2.path, created: qa2.created });
|
|
787
770
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
'
|
|
791
|
-
|
|
792
|
-
{
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
771
|
+
if (pinnedSpec) {
|
|
772
|
+
// Stack already pins an existing worktree; reuse it rather than creating a new one.
|
|
773
|
+
const pinnedDir = String(stackInfo.components.find((x) => x?.component === c)?.dir ?? '').trim();
|
|
774
|
+
createdWorktrees.push({ component: c, variant: 'reuse', taskId: compTaskId, path: pinnedDir, branch: null, worktreeSpec: pinnedSpec });
|
|
775
|
+
pinned.push({ stack, component: c, taskId: compTaskId, path: pinnedDir });
|
|
776
|
+
} else if (reuseOnly && stackExists) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`[edison] --reuse-only: stack "${stack}" is not pinned to a worktree for component "${c}".\n` +
|
|
779
|
+
`Fix:\n` +
|
|
780
|
+
`- pin an existing worktree to the stack:\n` +
|
|
781
|
+
` happys stack wt ${stack} -- use ${c} <owner/branch|/abs/path>\n` +
|
|
782
|
+
`- then re-run:\n` +
|
|
783
|
+
` happys edison task:scaffold ${taskId} --yes --reuse-only\n`
|
|
784
|
+
);
|
|
785
|
+
} else {
|
|
786
|
+
const from = track === 'fork' ? 'origin' : 'upstream';
|
|
787
|
+
const stdout = await runCapture(
|
|
788
|
+
'node',
|
|
789
|
+
['./bin/happys.mjs', 'wt', 'new', c, baseWorktree, `--from=${from}`, '--json'],
|
|
790
|
+
{ cwd: rootDir }
|
|
791
|
+
);
|
|
792
|
+
const res = JSON.parse(stdout);
|
|
793
|
+
createdWorktrees.push({ component: c, variant: from, taskId: compTaskId, path: res.path, branch: res.branch });
|
|
794
|
+
await run('node', ['./bin/happys.mjs', 'stack', 'wt', stack, '--', 'use', c, res.path, '--json'], { cwd: rootDir });
|
|
795
|
+
pinned.push({ stack, component: c, taskId: compTaskId, path: res.path });
|
|
796
|
+
}
|
|
798
797
|
}
|
|
799
798
|
}
|
|
800
799
|
} else {
|
|
@@ -817,22 +816,48 @@ async function cmdTaskScaffold({ rootDir, argv, json }) {
|
|
|
817
816
|
` happys edison task:scaffold ${taskId} --yes\n`
|
|
818
817
|
);
|
|
819
818
|
}
|
|
820
|
-
|
|
819
|
+
const stackRemote = mode === 'fork' ? 'origin' : 'upstream';
|
|
820
|
+
await run('node', ['./bin/happys.mjs', 'stack', 'new', stack, `--remote=${stackRemote}`, '--json'], { cwd: rootDir });
|
|
821
821
|
createdStacks.push({ stack });
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
+
let stackInfo = null;
|
|
825
|
+
if (stackExists) {
|
|
826
|
+
const raw = await runCapture('node', ['./bin/happys.mjs', 'stack', 'info', stack, '--json'], { cwd: rootDir });
|
|
827
|
+
stackInfo = JSON.parse(raw);
|
|
828
|
+
}
|
|
829
|
+
|
|
824
830
|
for (const c of components) {
|
|
825
|
-
const
|
|
831
|
+
const pinnedSpec =
|
|
832
|
+
stackInfo && Array.isArray(stackInfo.components)
|
|
833
|
+
? String(stackInfo.components.find((x) => x?.component === c)?.worktreeSpec ?? '').trim()
|
|
834
|
+
: '';
|
|
835
|
+
const baseWorktree = pinnedSpec || `edison/${taskId}`;
|
|
826
836
|
const from = mode === 'fork' ? 'origin' : 'upstream';
|
|
827
|
-
|
|
828
|
-
'
|
|
829
|
-
|
|
830
|
-
{
|
|
831
|
-
)
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
837
|
+
if (pinnedSpec) {
|
|
838
|
+
const pinnedDir = String(stackInfo.components.find((x) => x?.component === c)?.dir ?? '').trim();
|
|
839
|
+
createdWorktrees.push({ component: c, variant: 'reuse', taskId, path: pinnedDir, branch: null, worktreeSpec: pinnedSpec });
|
|
840
|
+
pinned.push({ stack, component: c, taskId, path: pinnedDir });
|
|
841
|
+
} else if (reuseOnly && stackExists) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`[edison] --reuse-only: stack "${stack}" is not pinned to a worktree for component "${c}".\n` +
|
|
844
|
+
`Fix:\n` +
|
|
845
|
+
`- pin an existing worktree to the stack:\n` +
|
|
846
|
+
` happys stack wt ${stack} -- use ${c} <owner/branch|/abs/path>\n` +
|
|
847
|
+
`- then re-run:\n` +
|
|
848
|
+
` happys edison task:scaffold ${taskId} --yes --reuse-only\n`
|
|
849
|
+
);
|
|
850
|
+
} else {
|
|
851
|
+
const stdout = await runCapture(
|
|
852
|
+
'node',
|
|
853
|
+
['./bin/happys.mjs', 'wt', 'new', c, baseWorktree, `--from=${from}`, '--json'],
|
|
854
|
+
{ cwd: rootDir }
|
|
855
|
+
);
|
|
856
|
+
const res = JSON.parse(stdout);
|
|
857
|
+
createdWorktrees.push({ component: c, variant: from, taskId, path: res.path, branch: res.branch });
|
|
858
|
+
await run('node', ['./bin/happys.mjs', 'stack', 'wt', stack, '--', 'use', c, res.path, '--json'], { cwd: rootDir });
|
|
859
|
+
pinned.push({ stack, component: c, taskId, path: res.path });
|
|
860
|
+
}
|
|
836
861
|
}
|
|
837
862
|
}
|
|
838
863
|
|
|
@@ -1756,7 +1781,9 @@ async function main() {
|
|
|
1756
1781
|
env.HAPPY_STACKS_EDISON_WRAPPER = '1';
|
|
1757
1782
|
// Provide a stack-scoped localhost hostname for validators and browser flows.
|
|
1758
1783
|
// This ensures origin isolation even if ports are reused later (common with ephemeral ports).
|
|
1759
|
-
const localhostHost =
|
|
1784
|
+
const localhostHost = Boolean(stackName)
|
|
1785
|
+
? await preferStackLocalhostHost({ stackName })
|
|
1786
|
+
: resolveLocalhostHost({ stackMode: false, stackName: 'main' });
|
|
1760
1787
|
env.HAPPY_STACKS_LOCALHOST_HOST = localhostHost;
|
|
1761
1788
|
env.HAPPY_LOCAL_LOCALHOST_HOST = localhostHost;
|
|
1762
1789
|
|
package/scripts/happy.mjs
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
function resolveCliHomeDir() {
|
|
11
|
-
const fromExplicit = (process.env.HAPPY_HOME_DIR ?? '').trim();
|
|
12
|
-
if (fromExplicit) {
|
|
13
|
-
return expandHome(fromExplicit);
|
|
14
|
-
}
|
|
15
|
-
const fromStacks = (process.env.HAPPY_STACKS_CLI_HOME_DIR ?? process.env.HAPPY_LOCAL_CLI_HOME_DIR ?? '').trim();
|
|
16
|
-
if (fromStacks) {
|
|
17
|
-
return expandHome(fromStacks);
|
|
18
|
-
}
|
|
19
|
-
return join(getDefaultAutostartPaths().baseDir, 'cli');
|
|
20
|
-
}
|
|
7
|
+
import { getComponentDir, getRootDir, getStackName } from './utils/paths/paths.mjs';
|
|
8
|
+
import { resolveCliHomeDir } from './utils/stack/dirs.mjs';
|
|
9
|
+
import { getPublicServerUrlEnvOverride, resolveServerPortFromEnv } from './utils/server/urls.mjs';
|
|
21
10
|
|
|
22
11
|
async function main() {
|
|
23
12
|
const argv = process.argv.slice(2);
|
|
@@ -42,12 +31,12 @@ async function main() {
|
|
|
42
31
|
|
|
43
32
|
const rootDir = getRootDir(import.meta.url);
|
|
44
33
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const serverPort =
|
|
34
|
+
const stackName =
|
|
35
|
+
(process.env.HAPPY_STACKS_STACK ?? process.env.HAPPY_LOCAL_STACK ?? '').toString().trim() || getStackName();
|
|
36
|
+
const serverPort = resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
|
|
48
37
|
|
|
49
38
|
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
50
|
-
const publicServerUrl = (
|
|
39
|
+
const { publicServerUrl } = getPublicServerUrlEnvOverride({ env: process.env, serverPort, stackName });
|
|
51
40
|
|
|
52
41
|
const cliHomeDir = resolveCliHomeDir();
|
|
53
42
|
|
package/scripts/init.mjs
CHANGED
|
@@ -4,19 +4,11 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { ensureCanonicalHomeEnvUpdated, ensureHomeEnvUpdated } from './utils/config.mjs';
|
|
8
|
-
import { parseDotenv } from './utils/dotenv.mjs';
|
|
9
|
-
import { expandHome } from './utils/canonical_home.mjs';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
async function readJsonIfExists(path) {
|
|
13
|
-
try {
|
|
14
|
-
const raw = await readFile(path, 'utf-8');
|
|
15
|
-
return JSON.parse(raw);
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
7
|
+
import { ensureCanonicalHomeEnvUpdated, ensureHomeEnvUpdated } from './utils/env/config.mjs';
|
|
8
|
+
import { parseDotenv } from './utils/env/dotenv.mjs';
|
|
9
|
+
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
10
|
+
import { readJsonIfExists } from './utils/fs/json.mjs';
|
|
11
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
20
12
|
|
|
21
13
|
function getCliRootDir() {
|
|
22
14
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -207,7 +199,9 @@ async function main() {
|
|
|
207
199
|
const storageDirRaw = parseArgValue(argv, 'storage-dir');
|
|
208
200
|
const storageDirOverride = expandHome((storageDirRaw ?? '').trim());
|
|
209
201
|
if (storageDirOverride) {
|
|
210
|
-
|
|
202
|
+
// In sandbox mode, storage dir MUST be isolated and must override any pre-existing env.
|
|
203
|
+
process.env.HAPPY_STACKS_STORAGE_DIR = isSandboxed() ? storageDirOverride : (process.env.HAPPY_STACKS_STORAGE_DIR ?? storageDirOverride);
|
|
204
|
+
process.env.HAPPY_LOCAL_STORAGE_DIR = process.env.HAPPY_LOCAL_STORAGE_DIR ?? process.env.HAPPY_STACKS_STORAGE_DIR;
|
|
211
205
|
}
|
|
212
206
|
|
|
213
207
|
const cliRootDirRaw = parseArgValue(argv, 'cli-root-dir');
|
package/scripts/install.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
-
import { pathExists } from './utils/fs.mjs';
|
|
4
|
-
import { run } from './utils/proc.mjs';
|
|
5
|
-
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
6
|
-
import { getServerComponentName } from './utils/server.mjs';
|
|
7
|
-
import { ensureCliBuilt, ensureDepsInstalled, ensureHappyCliLocalNpmLinked } from './utils/pm.mjs';
|
|
3
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
4
|
+
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
5
|
+
import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
|
|
6
|
+
import { getServerComponentName } from './utils/server/server.mjs';
|
|
7
|
+
import { ensureCliBuilt, ensureDepsInstalled, ensureHappyCliLocalNpmLinked } from './utils/proc/pm.mjs';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
import { mkdir } from 'node:fs/promises';
|
|
10
10
|
import { installService, uninstallService } from './service.mjs';
|
|
11
11
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
12
|
-
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
12
|
+
import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
|
|
13
13
|
import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
14
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Install/setup the local stack:
|
|
@@ -24,8 +24,10 @@ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs
|
|
|
24
24
|
|
|
25
25
|
const DEFAULT_FORK_REPOS = {
|
|
26
26
|
serverLight: 'https://github.com/leeroybrun/happy-server-light.git',
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// Both server flavors live as branches in the same fork repo:
|
|
28
|
+
// - happy-server-light (sqlite)
|
|
29
|
+
// - happy-server (full)
|
|
30
|
+
serverFull: 'https://github.com/leeroybrun/happy-server-light.git',
|
|
29
31
|
cli: 'https://github.com/leeroybrun/happy-cli.git',
|
|
30
32
|
ui: 'https://github.com/leeroybrun/happy.git',
|
|
31
33
|
};
|
|
@@ -44,7 +46,8 @@ function repoUrlsFromOwners({ forkOwner, upstreamOwner }) {
|
|
|
44
46
|
return {
|
|
45
47
|
forks: {
|
|
46
48
|
serverLight: fork('happy-server-light'),
|
|
47
|
-
|
|
49
|
+
// Fork convention: server full is a branch in happy-server-light repo (not a separate repo).
|
|
50
|
+
serverFull: fork('happy-server-light'),
|
|
48
51
|
cli: fork('happy-cli'),
|
|
49
52
|
ui: fork('happy'),
|
|
50
53
|
},
|
|
@@ -86,6 +89,51 @@ function getRepoUrls({ repoSource }) {
|
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
async function ensureGitBranchCheckedOut({ repoDir, branch, label }) {
|
|
93
|
+
if (!(await pathExists(join(repoDir, '.git')))) return;
|
|
94
|
+
const b = String(branch ?? '').trim();
|
|
95
|
+
if (!b) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const head = (await runCapture('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoDir })).trim();
|
|
99
|
+
if (head && head === b) return;
|
|
100
|
+
} catch {
|
|
101
|
+
// ignore
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Ensure branch exists locally, otherwise fetch it from origin.
|
|
105
|
+
let hasLocal = true;
|
|
106
|
+
try {
|
|
107
|
+
await run('git', ['show-ref', '--verify', '--quiet', `refs/heads/${b}`], { cwd: repoDir });
|
|
108
|
+
} catch {
|
|
109
|
+
hasLocal = false;
|
|
110
|
+
}
|
|
111
|
+
if (!hasLocal) {
|
|
112
|
+
try {
|
|
113
|
+
await run('git', ['fetch', '--quiet', 'origin', b], { cwd: repoDir });
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[local] ${label}: expected branch "${b}" to exist in ${repoDir}.\n` +
|
|
117
|
+
`[local] Fix: use --forks for happy-server-light (sqlite), or use --server=happy-server with --upstream.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await run('git', ['checkout', '-q', b], { cwd: repoDir });
|
|
124
|
+
} catch {
|
|
125
|
+
// If remote-tracking branch exists but local doesn't, create it.
|
|
126
|
+
try {
|
|
127
|
+
await run('git', ['checkout', '-q', '-B', b, `origin/${b}`], { cwd: repoDir });
|
|
128
|
+
} catch {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`[local] ${label}: failed to checkout branch "${b}" in ${repoDir}.\n` +
|
|
131
|
+
`[local] Fix: re-run with --force in worktree flows, or delete the checkout and re-run install/bootstrap.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
89
137
|
async function ensureComponentPresent({ dir, label, repoUrl, allowClone }) {
|
|
90
138
|
if (await pathExists(dir)) {
|
|
91
139
|
return;
|
|
@@ -201,7 +249,22 @@ async function main() {
|
|
|
201
249
|
if (wantsHelp(argv, { flags })) {
|
|
202
250
|
printResult({
|
|
203
251
|
json,
|
|
204
|
-
data: {
|
|
252
|
+
data: {
|
|
253
|
+
flags: [
|
|
254
|
+
'--forks',
|
|
255
|
+
'--upstream',
|
|
256
|
+
'--clone',
|
|
257
|
+
'--no-clone',
|
|
258
|
+
'--autostart',
|
|
259
|
+
'--no-autostart',
|
|
260
|
+
'--server=...',
|
|
261
|
+
'--no-ui-build',
|
|
262
|
+
'--no-ui-deps',
|
|
263
|
+
'--no-cli-deps',
|
|
264
|
+
'--no-cli-build',
|
|
265
|
+
],
|
|
266
|
+
json: true,
|
|
267
|
+
},
|
|
205
268
|
text: [
|
|
206
269
|
'[bootstrap] usage:',
|
|
207
270
|
' happys bootstrap [--forks|--upstream] [--server=happy-server|happy-server-light|both] [--json]',
|
|
@@ -270,6 +333,17 @@ async function main() {
|
|
|
270
333
|
const disableAutostart = flags.has('--no-autostart');
|
|
271
334
|
|
|
272
335
|
const serverComponentName = (wizard?.serverComponentName ?? getServerComponentName({ kv })).trim();
|
|
336
|
+
// Safety: upstream server-light is not a separate upstream repo/branch today.
|
|
337
|
+
// Upstream slopus/happy-server is Postgres-only, while happy-server-light requires sqlite.
|
|
338
|
+
if (repoSource === 'upstream' && (serverComponentName === 'happy-server-light' || serverComponentName === 'both')) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`[bootstrap] --upstream is not supported for happy-server-light (sqlite).\n` +
|
|
341
|
+
`Reason: upstream ${DEFAULT_UPSTREAM_REPOS.serverLight} does not provide a happy-server-light branch.\n` +
|
|
342
|
+
`Fix:\n` +
|
|
343
|
+
`- use --forks (recommended), OR\n` +
|
|
344
|
+
`- use --server=happy-server with --upstream`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
273
347
|
const serverLightDir = getComponentDir(rootDir, 'happy-server-light');
|
|
274
348
|
const serverFullDir = getComponentDir(rootDir, 'happy-server');
|
|
275
349
|
const cliDir = getComponentDir(rootDir, 'happy-cli');
|
|
@@ -305,35 +379,57 @@ async function main() {
|
|
|
305
379
|
allowClone,
|
|
306
380
|
});
|
|
307
381
|
|
|
382
|
+
// Ensure expected branches are checked out for server flavors (avoids "server-light directory contains full server" mistakes).
|
|
383
|
+
if (serverComponentName === 'both' || serverComponentName === 'happy-server-light') {
|
|
384
|
+
await ensureGitBranchCheckedOut({ repoDir: serverLightDir, branch: 'happy-server-light', label: 'SERVER' });
|
|
385
|
+
}
|
|
386
|
+
if (serverComponentName === 'both' || serverComponentName === 'happy-server') {
|
|
387
|
+
// In fork mode, full server is a branch in the fork server repo. In upstream mode, use upstream main.
|
|
388
|
+
const serverFullBranch = repoSource === 'upstream' ? 'main' : 'happy-server';
|
|
389
|
+
await ensureGitBranchCheckedOut({ repoDir: serverFullDir, branch: serverFullBranch, label: 'SERVER_FULL' });
|
|
390
|
+
}
|
|
391
|
+
|
|
308
392
|
const cliDirFinal = cliDir;
|
|
309
393
|
const uiDirFinal = uiDir;
|
|
310
394
|
|
|
311
395
|
// Install deps
|
|
396
|
+
const skipUiDeps = flags.has('--no-ui-deps') || (process.env.HAPPY_STACKS_INSTALL_NO_UI_DEPS ?? '').trim() === '1';
|
|
397
|
+
const skipCliDeps = flags.has('--no-cli-deps') || (process.env.HAPPY_STACKS_INSTALL_NO_CLI_DEPS ?? '').trim() === '1';
|
|
312
398
|
if (serverComponentName === 'both' || serverComponentName === 'happy-server-light') {
|
|
313
399
|
await ensureDepsInstalled(serverLightDir, 'happy-server-light');
|
|
314
400
|
}
|
|
315
401
|
if (serverComponentName === 'both' || serverComponentName === 'happy-server') {
|
|
316
402
|
await ensureDepsInstalled(serverFullDir, 'happy-server');
|
|
317
403
|
}
|
|
318
|
-
|
|
319
|
-
|
|
404
|
+
if (!skipUiDeps) {
|
|
405
|
+
await ensureDepsInstalled(uiDirFinal, 'happy');
|
|
406
|
+
}
|
|
407
|
+
if (!skipCliDeps) {
|
|
408
|
+
await ensureDepsInstalled(cliDirFinal, 'happy-cli');
|
|
409
|
+
}
|
|
320
410
|
|
|
321
411
|
// CLI build + link
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
412
|
+
const skipCliBuild = flags.has('--no-cli-build') || (process.env.HAPPY_STACKS_INSTALL_NO_CLI_BUILD ?? '').trim() === '1';
|
|
413
|
+
if (!skipCliBuild) {
|
|
414
|
+
const buildCli = (process.env.HAPPY_LOCAL_CLI_BUILD ?? '1') !== '0';
|
|
415
|
+
const npmLinkCli = (process.env.HAPPY_LOCAL_NPM_LINK ?? '1') !== '0';
|
|
416
|
+
await ensureCliBuilt(cliDirFinal, { buildCli });
|
|
417
|
+
await ensureHappyCliLocalNpmLinked(rootDir, { npmLinkCli });
|
|
418
|
+
}
|
|
326
419
|
|
|
327
420
|
// Build UI (so run works without expo dev server)
|
|
421
|
+
const skipUiBuild = flags.has('--no-ui-build') || (process.env.HAPPY_STACKS_INSTALL_NO_UI_BUILD ?? '').trim() === '1';
|
|
328
422
|
const buildArgs = [join(rootDir, 'scripts', 'build.mjs')];
|
|
329
423
|
// Tauri builds are opt-in (slow + requires additional toolchain).
|
|
330
424
|
const buildTauri = wizard?.buildTauri ?? (flags.has('--tauri') && !flags.has('--no-tauri'));
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
425
|
+
if (!skipUiBuild) {
|
|
426
|
+
if (buildTauri) {
|
|
427
|
+
buildArgs.push('--tauri');
|
|
428
|
+
} else if (flags.has('--no-tauri')) {
|
|
429
|
+
buildArgs.push('--no-tauri');
|
|
430
|
+
}
|
|
431
|
+
await run(process.execPath, buildArgs, { cwd: rootDir });
|
|
335
432
|
}
|
|
336
|
-
await run(process.execPath, buildArgs, { cwd: rootDir });
|
|
337
433
|
|
|
338
434
|
// Optional autostart (macOS)
|
|
339
435
|
if (disableAutostart) {
|