happy-stacks 0.2.0 → 0.3.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 +59 -22
- package/bin/happys.mjs +2 -2
- package/package.json +1 -1
- package/scripts/auth.mjs +49 -202
- package/scripts/build.mjs +5 -6
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +5 -5
- package/scripts/daemon.mjs +9 -17
- package/scripts/dev.mjs +18 -27
- package/scripts/doctor.mjs +20 -36
- package/scripts/edison.mjs +102 -77
- package/scripts/happy.mjs +8 -19
- package/scripts/init.mjs +5 -13
- package/scripts/install.mjs +8 -8
- package/scripts/lint.mjs +8 -29
- package/scripts/menubar.mjs +6 -13
- package/scripts/migrate.mjs +11 -21
- package/scripts/mobile.mjs +13 -12
- package/scripts/run.mjs +15 -15
- package/scripts/self.mjs +11 -29
- package/scripts/server_flavor.mjs +4 -4
- package/scripts/service.mjs +18 -28
- package/scripts/setup.mjs +26 -122
- package/scripts/setup_pr.mjs +11 -28
- package/scripts/stack.mjs +111 -161
- package/scripts/stop.mjs +3 -3
- package/scripts/tailscale.mjs +7 -10
- package/scripts/test.mjs +8 -29
- package/scripts/tui.mjs +8 -38
- package/scripts/typecheck.mjs +8 -29
- package/scripts/ui_gateway.mjs +1 -1
- package/scripts/uninstall.mjs +6 -6
- package/scripts/utils/{dev_auth_key.mjs → auth/dev_key.mjs} +2 -8
- package/scripts/utils/{auth_files.mjs → auth/files.mjs} +2 -4
- package/scripts/utils/{handy_master_secret.mjs → auth/handy_master_secret.mjs} +6 -32
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/smoke_help.mjs +2 -2
- 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} +4 -4
- package/scripts/utils/{dev_expo_web.mjs → dev/expo_web.mjs} +5 -5
- package/scripts/utils/{dev_server.mjs → dev/server.mjs} +7 -7
- 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.mjs → expo/expo.mjs} +3 -9
- 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/refs.mjs +26 -0
- package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +3 -3
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
- package/scripts/utils/{localhost_host.mjs → paths/localhost_host.mjs} +2 -10
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +10 -7
- 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/pids.mjs +11 -0
- package/scripts/utils/{pm.mjs → proc/pm.mjs} +65 -152
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +1 -0
- package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +10 -49
- package/scripts/utils/server/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +12 -0
- package/scripts/utils/server/urls.mjs +91 -0
- package/scripts/utils/{validate.mjs → server/validate.mjs} +1 -1
- package/scripts/utils/service/autostart_darwin.mjs +142 -0
- package/scripts/utils/{stack_context.mjs → stack/context.mjs} +2 -2
- 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_runtime_state.mjs → stack/runtime_state.mjs} +10 -27
- package/scripts/utils/{stacks.mjs → stack/stacks.mjs} +9 -2
- package/scripts/utils/{stack_startup.mjs → stack/startup.mjs} +2 -2
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +9 -15
- package/scripts/utils/{browser.mjs → ui/browser.mjs} +1 -1
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/where.mjs +6 -6
- package/scripts/worktrees.mjs +30 -58
- package/scripts/utils/server_port.mjs +0 -9
- package/scripts/utils/server_urls.mjs +0 -54
- /package/scripts/utils/{auth_login_ux.mjs → auth/login_ux.mjs} +0 -0
- /package/scripts/utils/{auth_sources.mjs → auth/sources.mjs} +0 -0
- /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/doctor.mjs
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
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 { runCapture } from './utils/proc.mjs';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
4
|
+
import { runCapture } from './utils/proc/proc.mjs';
|
|
5
|
+
import { resolveCommandPath } from './utils/proc/commands.mjs';
|
|
6
|
+
import { getComponentDir, getDefaultAutostartPaths, getHappyStacksHomeDir, getRootDir, getWorkspaceDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
7
|
+
import { killPortListeners } from './utils/net/ports.mjs';
|
|
8
|
+
import { getServerComponentName } from './utils/server/server.mjs';
|
|
9
|
+
import { fetchHappyHealth } from './utils/server/server.mjs';
|
|
8
10
|
import { daemonStatusSummary } from './daemon.mjs';
|
|
9
11
|
import { tailscaleServeStatus } from './tailscale.mjs';
|
|
10
12
|
import { homedir } from 'node:os';
|
|
11
13
|
import { join } from 'node:path';
|
|
12
14
|
import { existsSync } from 'node:fs';
|
|
13
|
-
import { readFile } from 'node:fs/promises';
|
|
14
15
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
15
|
-
import { getRuntimeDir } from './utils/runtime.mjs';
|
|
16
|
-
import { assertServerComponentDirMatches } from './utils/validate.mjs';
|
|
17
|
-
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/
|
|
18
|
-
import { resolveStackContext } from './utils/
|
|
16
|
+
import { getRuntimeDir } from './utils/paths/runtime.mjs';
|
|
17
|
+
import { assertServerComponentDirMatches } from './utils/server/validate.mjs';
|
|
18
|
+
import { resolveServerPortFromEnv, resolveServerUrls } from './utils/server/urls.mjs';
|
|
19
|
+
import { resolveStackContext } from './utils/stack/context.mjs';
|
|
20
|
+
import { readJsonIfExists } from './utils/fs/json.mjs';
|
|
21
|
+
import { readPackageJsonVersion } from './utils/fs/package_json.mjs';
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Doctor script for common happy-stacks failure modes.
|
|
@@ -43,7 +46,8 @@ async function fetchHealth(url) {
|
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
// Prefer /health when available, but fall back to / (matches waitForServerReady).
|
|
46
|
-
const
|
|
49
|
+
const healthRaw = await fetchHappyHealth(url);
|
|
50
|
+
const health = { ok: healthRaw.ok, status: healthRaw.status, body: healthRaw.text ? healthRaw.text.trim() : null };
|
|
47
51
|
if (health.ok) {
|
|
48
52
|
return health;
|
|
49
53
|
}
|
|
@@ -54,26 +58,6 @@ async function fetchHealth(url) {
|
|
|
54
58
|
return health.ok ? health : root;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
async function readJsonSafe(path) {
|
|
58
|
-
try {
|
|
59
|
-
const raw = await readFile(path, 'utf-8');
|
|
60
|
-
return JSON.parse(raw);
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function readPkgVersion(path) {
|
|
67
|
-
try {
|
|
68
|
-
const raw = await readFile(path, 'utf-8');
|
|
69
|
-
const pkg = JSON.parse(raw);
|
|
70
|
-
const v = String(pkg.version ?? '').trim();
|
|
71
|
-
return v || null;
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
61
|
async function resolveSwiftbarPluginsDir() {
|
|
78
62
|
if (process.platform !== 'darwin') {
|
|
79
63
|
return null;
|
|
@@ -114,8 +98,8 @@ async function main() {
|
|
|
114
98
|
const workspaceDir = getWorkspaceDir(rootDir);
|
|
115
99
|
const updateCachePath = join(homeDir, 'cache', 'update.json');
|
|
116
100
|
const runtimePkgJson = join(runtimeDir, 'node_modules', 'happy-stacks', 'package.json');
|
|
117
|
-
const runtimeVersion = await
|
|
118
|
-
const updateCache =
|
|
101
|
+
const runtimeVersion = await readPackageJsonVersion(runtimePkgJson);
|
|
102
|
+
const updateCache = await readJsonIfExists(updateCachePath, { defaultValue: null });
|
|
119
103
|
|
|
120
104
|
const autostart = getDefaultAutostartPaths();
|
|
121
105
|
const stackCtx = resolveStackContext({ env: process.env, autostart });
|
|
@@ -302,7 +286,7 @@ async function main() {
|
|
|
302
286
|
|
|
303
287
|
// happy wrapper
|
|
304
288
|
try {
|
|
305
|
-
const happyPath =
|
|
289
|
+
const happyPath = await resolveCommandPath('happy');
|
|
306
290
|
if (happyPath) {
|
|
307
291
|
report.checks.happyOnPath = { ok: true, path: happyPath };
|
|
308
292
|
if (!json) console.log(`✅ happy on PATH: ${happyPath}`);
|
|
@@ -314,7 +298,7 @@ async function main() {
|
|
|
314
298
|
|
|
315
299
|
// happys on PATH
|
|
316
300
|
try {
|
|
317
|
-
const happysPath =
|
|
301
|
+
const happysPath = await resolveCommandPath('happys');
|
|
318
302
|
if (happysPath) {
|
|
319
303
|
report.checks.happysOnPath = { ok: true, path: happysPath };
|
|
320
304
|
if (!json) console.log(`✅ happys on PATH: ${happysPath}`);
|
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 { 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
|
|
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)));
|
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 } 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:
|
package/scripts/lint.mjs
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
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 { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
5
|
-
import { ensureDepsInstalled
|
|
6
|
-
import { pathExists } from './utils/fs.mjs';
|
|
7
|
-
import { run } from './utils/proc.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
|
|
5
|
+
import { ensureDepsInstalled } from './utils/proc/pm.mjs';
|
|
6
|
+
import { pathExists } from './utils/fs/fs.mjs';
|
|
7
|
+
import { run } from './utils/proc/proc.mjs';
|
|
8
|
+
import { detectPackageManagerCmd, pickFirstScript, readPackageJsonScripts } from './utils/proc/package_scripts.mjs';
|
|
10
9
|
|
|
11
10
|
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
12
11
|
|
|
13
|
-
async function detectPackageManagerCmd(dir) {
|
|
14
|
-
if (await pathExists(join(dir, 'yarn.lock'))) {
|
|
15
|
-
return { name: 'yarn', cmd: 'yarn', argsForScript: (script) => ['-s', script] };
|
|
16
|
-
}
|
|
17
|
-
await requirePnpm();
|
|
18
|
-
return { name: 'pnpm', cmd: 'pnpm', argsForScript: (script) => ['--silent', script] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function readScripts(dir) {
|
|
22
|
-
try {
|
|
23
|
-
const raw = await readFile(join(dir, 'package.json'), 'utf-8');
|
|
24
|
-
const pkg = JSON.parse(raw);
|
|
25
|
-
const scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
26
|
-
return scripts;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
12
|
function pickLintScript(scripts) {
|
|
33
|
-
if (!scripts) return null;
|
|
34
13
|
const candidates = [
|
|
35
14
|
'lint',
|
|
36
15
|
'lint:ci',
|
|
@@ -39,7 +18,7 @@ function pickLintScript(scripts) {
|
|
|
39
18
|
'eslint',
|
|
40
19
|
'eslint:check',
|
|
41
20
|
];
|
|
42
|
-
return
|
|
21
|
+
return pickFirstScript(scripts, candidates);
|
|
43
22
|
}
|
|
44
23
|
|
|
45
24
|
async function main() {
|
|
@@ -86,7 +65,7 @@ async function main() {
|
|
|
86
65
|
continue;
|
|
87
66
|
}
|
|
88
67
|
|
|
89
|
-
const scripts = await
|
|
68
|
+
const scripts = await readPackageJsonScripts(dir);
|
|
90
69
|
if (!scripts) {
|
|
91
70
|
results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
|
|
92
71
|
continue;
|
package/scripts/menubar.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
2
|
import { cp, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
|
|
7
|
+
import { getHappyStacksHomeDir, getRootDir } from './utils/paths/paths.mjs';
|
|
8
8
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
9
9
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
10
|
-
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
11
|
-
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
10
|
+
import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
|
|
11
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
12
|
+
import { normalizeProfile } from './utils/cli/normalize.mjs';
|
|
12
13
|
|
|
13
14
|
async function ensureSwiftbarAssets({ cliRootDir }) {
|
|
14
15
|
const homeDir = getHappyStacksHomeDir();
|
|
@@ -59,14 +60,6 @@ function removeSwiftbarPlugins({ patterns }) {
|
|
|
59
60
|
return out || null;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function normalizeMenubarMode(raw) {
|
|
63
|
-
const v = String(raw ?? '').trim().toLowerCase();
|
|
64
|
-
if (!v) return '';
|
|
65
|
-
if (v === 'selfhost' || v === 'self-host' || v === 'self_host' || v === 'host') return 'selfhost';
|
|
66
|
-
if (v === 'dev' || v === 'developer') return 'dev';
|
|
67
|
-
return '';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
63
|
async function main() {
|
|
71
64
|
const rawArgv = process.argv.slice(2);
|
|
72
65
|
const argv = rawArgv[0] === 'menubar' ? rawArgv.slice(1) : rawArgv;
|
|
@@ -128,7 +121,7 @@ async function main() {
|
|
|
128
121
|
if (cmd === 'mode') {
|
|
129
122
|
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
130
123
|
const raw = positionals[1] ?? '';
|
|
131
|
-
const mode =
|
|
124
|
+
const mode = normalizeProfile(raw);
|
|
132
125
|
if (!mode) {
|
|
133
126
|
throw new Error('[menubar] usage: happys menubar mode <selfhost|dev> [--json]');
|
|
134
127
|
}
|
package/scripts/migrate.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import './utils/env.mjs';
|
|
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
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
|
|
6
6
|
import { parseArgs } from './utils/cli/args.mjs';
|
|
7
7
|
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { resolveStackEnvPath } from './utils/paths.mjs';
|
|
11
|
-
import { ensureDepsInstalled } from './utils/pm.mjs';
|
|
12
|
-
import { ensureHappyServerManagedInfra, applyHappyServerMigrations } from './utils/happy_server_infra.mjs';
|
|
13
|
-
import { runCapture } from './utils/proc.mjs';
|
|
14
|
-
import { pickNextFreeTcpPort } from './utils/ports.mjs';
|
|
8
|
+
import { ensureEnvFileUpdated } from './utils/env/env_file.mjs';
|
|
9
|
+
import { readEnvObjectFromFile } from './utils/env/read.mjs';
|
|
10
|
+
import { resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
11
|
+
import { ensureDepsInstalled } from './utils/proc/pm.mjs';
|
|
12
|
+
import { ensureHappyServerManagedInfra, applyHappyServerMigrations } from './utils/server/infra/happy_server_infra.mjs';
|
|
13
|
+
import { runCapture } from './utils/proc/proc.mjs';
|
|
14
|
+
import { pickNextFreeTcpPort } from './utils/net/ports.mjs';
|
|
15
|
+
import { getEnvValue } from './utils/env/values.mjs';
|
|
15
16
|
|
|
16
17
|
function usage() {
|
|
17
18
|
return [
|
|
@@ -25,18 +26,7 @@ function usage() {
|
|
|
25
26
|
].join('\n');
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const raw = await readFile(envPath, 'utf-8');
|
|
31
|
-
return Object.fromEntries(parseDotenv(raw).entries());
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getEnvValue(env, key) {
|
|
38
|
-
return (env?.[key] ?? '').toString().trim();
|
|
39
|
-
}
|
|
29
|
+
const readEnvObject = readEnvObjectFromFile;
|
|
40
30
|
|
|
41
31
|
function parseFileDatabaseUrl(url) {
|
|
42
32
|
const raw = String(url ?? '').trim();
|
|
@@ -290,7 +280,7 @@ async function main() {
|
|
|
290
280
|
throw new Error('[migrate] --to-stack is required');
|
|
291
281
|
}
|
|
292
282
|
|
|
293
|
-
const rootDir = (await import('./utils/paths.mjs')).getRootDir(import.meta.url);
|
|
283
|
+
const rootDir = (await import('./utils/paths/paths.mjs')).getRootDir(import.meta.url);
|
|
294
284
|
await migrateLightToServer({ rootDir, fromStack, toStack, includeFiles, force, json });
|
|
295
285
|
}
|
|
296
286
|
|