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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createReadStream, existsSync } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
|
|
5
|
+
function splitLines(s) {
|
|
6
|
+
return String(s ?? '').split(/\r?\n/);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function supportsAnsi() {
|
|
10
|
+
if (!process.stdout.isTTY) return false;
|
|
11
|
+
if (process.env.NO_COLOR) return false;
|
|
12
|
+
if ((process.env.TERM ?? '').toLowerCase() === 'dumb') return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function dim(s) {
|
|
17
|
+
return supportsAnsi() ? `\x1b[2m${s}\x1b[0m` : String(s);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Lightweight file log forwarder (tail-like) with pause/resume.
|
|
22
|
+
*
|
|
23
|
+
* - Always advances the file offset (prevents backpressure issues).
|
|
24
|
+
* - While paused, it buffers the last N lines and prints them once resumed.
|
|
25
|
+
*/
|
|
26
|
+
export function createFileLogForwarder({
|
|
27
|
+
path,
|
|
28
|
+
enabled = true,
|
|
29
|
+
pollMs = 200,
|
|
30
|
+
maxBytesPerTick = 256 * 1024,
|
|
31
|
+
bufferedLinesWhilePaused = 120,
|
|
32
|
+
startFromEnd = true,
|
|
33
|
+
label = 'logs',
|
|
34
|
+
} = {}) {
|
|
35
|
+
const p = String(path ?? '').trim();
|
|
36
|
+
if (!enabled || !p) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
start: async () => {},
|
|
40
|
+
stop: async () => {},
|
|
41
|
+
pause: () => {},
|
|
42
|
+
resume: () => {},
|
|
43
|
+
isPaused: () => false,
|
|
44
|
+
path: p,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let running = false;
|
|
49
|
+
let paused = false;
|
|
50
|
+
let offset = 0;
|
|
51
|
+
let partial = '';
|
|
52
|
+
let buffered = [];
|
|
53
|
+
|
|
54
|
+
const pushBufferedLine = (line) => {
|
|
55
|
+
if (!line) return;
|
|
56
|
+
buffered.push(line);
|
|
57
|
+
if (buffered.length > bufferedLinesWhilePaused) {
|
|
58
|
+
buffered = buffered.slice(buffered.length - bufferedLinesWhilePaused);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const flushBuffered = () => {
|
|
63
|
+
if (!buffered.length) return;
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log(dim(`[${label}] (showing last ${buffered.length} lines while paused)`));
|
|
66
|
+
for (const l of buffered) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log(l);
|
|
69
|
+
}
|
|
70
|
+
buffered = [];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const readNewBytes = async () => {
|
|
74
|
+
if (!existsSync(p)) return;
|
|
75
|
+
let st = null;
|
|
76
|
+
try {
|
|
77
|
+
st = await stat(p);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const size = Number(st?.size ?? 0);
|
|
82
|
+
if (!Number.isFinite(size) || size <= 0) return;
|
|
83
|
+
if (size < offset) {
|
|
84
|
+
// truncated/rotated
|
|
85
|
+
offset = 0;
|
|
86
|
+
}
|
|
87
|
+
if (size === offset) return;
|
|
88
|
+
|
|
89
|
+
const end = Math.min(size, offset + maxBytesPerTick);
|
|
90
|
+
const start = offset;
|
|
91
|
+
offset = end;
|
|
92
|
+
|
|
93
|
+
await new Promise((resolvePromise) => {
|
|
94
|
+
const chunks = [];
|
|
95
|
+
const stream = createReadStream(p, { start, end: end - 1 });
|
|
96
|
+
stream.on('data', (d) => chunks.push(Buffer.from(d)));
|
|
97
|
+
stream.on('error', () => resolvePromise());
|
|
98
|
+
stream.on('close', () => {
|
|
99
|
+
const text = partial + Buffer.concat(chunks).toString('utf-8');
|
|
100
|
+
const lines = splitLines(text);
|
|
101
|
+
partial = lines.pop() ?? '';
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (paused) {
|
|
104
|
+
pushBufferedLine(line);
|
|
105
|
+
} else {
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.log(line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
resolvePromise();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const loop = async () => {
|
|
116
|
+
while (running) {
|
|
117
|
+
// eslint-disable-next-line no-await-in-loop
|
|
118
|
+
await readNewBytes();
|
|
119
|
+
// eslint-disable-next-line no-await-in-loop
|
|
120
|
+
await delay(pollMs);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
path: p,
|
|
127
|
+
start: async () => {
|
|
128
|
+
if (running) return;
|
|
129
|
+
running = true;
|
|
130
|
+
// By default, start at end (don't replay historical logs).
|
|
131
|
+
if (startFromEnd) {
|
|
132
|
+
try {
|
|
133
|
+
const st = await stat(p);
|
|
134
|
+
offset = Number(st?.size ?? 0) || 0;
|
|
135
|
+
} catch {
|
|
136
|
+
offset = 0;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
offset = 0;
|
|
140
|
+
}
|
|
141
|
+
void loop();
|
|
142
|
+
},
|
|
143
|
+
stop: async () => {
|
|
144
|
+
running = false;
|
|
145
|
+
},
|
|
146
|
+
pause: () => {
|
|
147
|
+
paused = true;
|
|
148
|
+
buffered = [];
|
|
149
|
+
},
|
|
150
|
+
resume: () => {
|
|
151
|
+
paused = false;
|
|
152
|
+
flushBuffered();
|
|
153
|
+
},
|
|
154
|
+
isPaused: () => paused,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function normalizeProfile(raw) {
|
|
2
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
3
|
+
if (!v) return '';
|
|
4
|
+
if (v === 'selfhost' || v === 'self-host' || v === 'self_host' || v === 'host') return 'selfhost';
|
|
5
|
+
if (v === 'dev' || v === 'developer' || v === 'develop' || v === 'development') return 'dev';
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeServerComponent(raw) {
|
|
10
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
11
|
+
if (!v) return '';
|
|
12
|
+
if (v === 'light' || v === 'server-light' || v === 'happy-server-light') return 'happy-server-light';
|
|
13
|
+
if (v === 'server' || v === 'full' || v === 'happy-server') return 'happy-server';
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { commandExists } from '../proc/commands.mjs';
|
|
2
|
+
|
|
3
|
+
function formatMissingTool({ name, why, install }) {
|
|
4
|
+
return [`- ${name}: ${why}`, ...(install?.length ? install.map((l) => ` ${l}`) : [])].join('\n');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function assertCliPrereqs({ git = false, pnpm = false, codex = false, coderabbit = false } = {}) {
|
|
8
|
+
const missing = [];
|
|
9
|
+
|
|
10
|
+
if (git) {
|
|
11
|
+
const hasGit = await commandExists('git');
|
|
12
|
+
if (!hasGit) {
|
|
13
|
+
const install =
|
|
14
|
+
process.platform === 'darwin'
|
|
15
|
+
? ['Install Xcode Command Line Tools: `xcode-select --install`', 'Or install Git via Homebrew: `brew install git`']
|
|
16
|
+
: ['Install Git using your package manager (e.g. `apt install git`, `dnf install git`)'];
|
|
17
|
+
missing.push({
|
|
18
|
+
name: 'git',
|
|
19
|
+
why: 'required for cloning + updating PR worktrees',
|
|
20
|
+
install,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (pnpm) {
|
|
26
|
+
const hasPnpm = await commandExists('pnpm');
|
|
27
|
+
if (!hasPnpm) {
|
|
28
|
+
missing.push({
|
|
29
|
+
name: 'pnpm',
|
|
30
|
+
why: 'required to install dependencies for Happy Stacks components',
|
|
31
|
+
install: ['Install it via Corepack: `corepack enable && corepack prepare pnpm@latest --activate`'],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (codex) {
|
|
37
|
+
const hasCodex = await commandExists('codex');
|
|
38
|
+
if (!hasCodex) {
|
|
39
|
+
missing.push({
|
|
40
|
+
name: 'codex',
|
|
41
|
+
why: 'required to run Codex review',
|
|
42
|
+
install: [
|
|
43
|
+
'Install Codex CLI and ensure `codex` is on PATH',
|
|
44
|
+
'If using a managed install, ensure your PATH includes the Codex binary',
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (coderabbit) {
|
|
51
|
+
const hasCodeRabbit = await commandExists('coderabbit');
|
|
52
|
+
if (!hasCodeRabbit) {
|
|
53
|
+
missing.push({
|
|
54
|
+
name: 'coderabbit',
|
|
55
|
+
why: 'required to run CodeRabbit CLI review',
|
|
56
|
+
install: [
|
|
57
|
+
'Install CodeRabbit CLI: `curl -fsSL https://cli.coderabbit.ai/install.sh | sh`',
|
|
58
|
+
'Then authenticate: `coderabbit auth login`',
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!missing.length) return;
|
|
65
|
+
|
|
66
|
+
throw new Error(
|
|
67
|
+
`[prereqs] missing required tools:\n` +
|
|
68
|
+
`${missing.map(formatMissingTool).join('\n')}\n\n` +
|
|
69
|
+
`[prereqs] After installing, re-run the command.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
|
|
6
|
+
function isTty() {
|
|
7
|
+
return Boolean(process.stdout.isTTY && process.stderr.isTTY);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function spinnerFrames() {
|
|
11
|
+
return ['|', '/', '-', '\\'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createStepPrinter({ enabled = true } = {}) {
|
|
15
|
+
const tty = enabled && isTty();
|
|
16
|
+
const frames = spinnerFrames();
|
|
17
|
+
let timer = null;
|
|
18
|
+
let idx = 0;
|
|
19
|
+
let currentLine = '';
|
|
20
|
+
|
|
21
|
+
const write = (s) => process.stdout.write(s);
|
|
22
|
+
|
|
23
|
+
const start = (label) => {
|
|
24
|
+
if (!tty) {
|
|
25
|
+
write(`- [..] ${label}\n`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
currentLine = `- [${frames[idx % frames.length]}] ${label}`;
|
|
29
|
+
write(currentLine);
|
|
30
|
+
timer = setInterval(() => {
|
|
31
|
+
idx++;
|
|
32
|
+
const next = `- [${frames[idx % frames.length]}] ${label}`;
|
|
33
|
+
const pad = currentLine.length > next.length ? ' '.repeat(currentLine.length - next.length) : '';
|
|
34
|
+
currentLine = next;
|
|
35
|
+
write(`\r${next}${pad}`);
|
|
36
|
+
}, 120);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const stop = (result, label) => {
|
|
40
|
+
if (timer) clearInterval(timer);
|
|
41
|
+
timer = null;
|
|
42
|
+
if (!tty) {
|
|
43
|
+
write(`- [${result}] ${label}\n`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const out = `- [${result}] ${label}`;
|
|
47
|
+
const pad = currentLine.length > out.length ? ' '.repeat(currentLine.length - out.length) : '';
|
|
48
|
+
currentLine = '';
|
|
49
|
+
write(`\r${out}${pad}\n`);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const info = (line) => {
|
|
53
|
+
write(`${line}\n`);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return { start, stop, info };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runCommandLogged({
|
|
60
|
+
label,
|
|
61
|
+
cmd,
|
|
62
|
+
args,
|
|
63
|
+
cwd,
|
|
64
|
+
env,
|
|
65
|
+
logPath,
|
|
66
|
+
showSteps = true,
|
|
67
|
+
quiet = true,
|
|
68
|
+
}) {
|
|
69
|
+
const steps = createStepPrinter({ enabled: showSteps });
|
|
70
|
+
if (quiet) {
|
|
71
|
+
await mkdir(dirname(logPath), { recursive: true }).catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
steps.start(label);
|
|
75
|
+
|
|
76
|
+
const child = spawn(cmd, args, {
|
|
77
|
+
cwd,
|
|
78
|
+
env,
|
|
79
|
+
stdio: quiet ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
80
|
+
shell: false,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let stdout = '';
|
|
84
|
+
let stderr = '';
|
|
85
|
+
let logStream = null;
|
|
86
|
+
if (quiet) {
|
|
87
|
+
logStream = createWriteStream(logPath, { flags: 'a' });
|
|
88
|
+
child.stdout?.on('data', (d) => {
|
|
89
|
+
const s = d.toString();
|
|
90
|
+
stdout += s;
|
|
91
|
+
logStream?.write(s);
|
|
92
|
+
});
|
|
93
|
+
child.stderr?.on('data', (d) => {
|
|
94
|
+
const s = d.toString();
|
|
95
|
+
stderr += s;
|
|
96
|
+
logStream?.write(s);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const res = await new Promise((resolvePromise, rejectPromise) => {
|
|
101
|
+
child.on('error', rejectPromise);
|
|
102
|
+
child.on('close', (code, signal) => resolvePromise({ code: code ?? 1, signal: signal ?? null }));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
logStream?.end();
|
|
107
|
+
} catch {
|
|
108
|
+
// ignore
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (res.code === 0) {
|
|
112
|
+
steps.stop('✓', label);
|
|
113
|
+
return { ok: true, code: 0, stdout, stderr, logPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
steps.stop('x', label);
|
|
117
|
+
const err = new Error(`${cmd} failed (code=${res.code}${res.signal ? `, sig=${res.signal}` : ''})`);
|
|
118
|
+
err.code = 'EEXIT';
|
|
119
|
+
err.exitCode = res.code;
|
|
120
|
+
err.signal = res.signal;
|
|
121
|
+
err.stdout = stdout;
|
|
122
|
+
err.stderr = stderr;
|
|
123
|
+
err.logPath = logPath;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
|
|
@@ -6,8 +6,8 @@ import { dirname } from 'node:path';
|
|
|
6
6
|
import { getHappysRegistry } from './cli_registry.mjs';
|
|
7
7
|
|
|
8
8
|
function cliRootDir() {
|
|
9
|
-
// scripts/utils/* -> scripts -> repo root
|
|
10
|
-
return dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
9
|
+
// scripts/utils/cli/* -> scripts/utils -> scripts -> repo root
|
|
10
|
+
return dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function runOrThrow(label, args) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function getVerbosityLevel(env = process.env) {
|
|
2
|
+
const raw = (env.HAPPY_STACKS_VERBOSE ?? '').toString().trim();
|
|
3
|
+
if (!raw) return 0;
|
|
4
|
+
const n = Number(raw);
|
|
5
|
+
if (!Number.isFinite(n)) return 1;
|
|
6
|
+
return Math.max(0, Math.min(3, Math.floor(n)));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isVerbose(env = process.env) {
|
|
10
|
+
return getVerbosityLevel(env) > 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function base64Url(buf) {
|
|
4
|
+
return Buffer.from(buf)
|
|
5
|
+
.toString('base64')
|
|
6
|
+
.replaceAll('+', '-')
|
|
7
|
+
.replaceAll('/', '_')
|
|
8
|
+
.replaceAll('=', '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function randomToken(lenBytes = 24) {
|
|
12
|
+
return base64Url(randomBytes(lenBytes));
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -1,13 +1,33 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
|
|
3
|
-
import { ensureCliBuilt, ensureDepsInstalled } from '
|
|
4
|
-
import { watchDebounced } from '
|
|
5
|
-
import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded } from '
|
|
6
|
-
import { startLocalDaemonWithAuth } from '
|
|
4
|
+
import { ensureCliBuilt, ensureDepsInstalled } from '../proc/pm.mjs';
|
|
5
|
+
import { watchDebounced } from '../proc/watch.mjs';
|
|
6
|
+
import { getAccountCountForServerComponent, prepareDaemonAuthSeedIfNeeded } from '../stack/startup.mjs';
|
|
7
|
+
import { startLocalDaemonWithAuth } from '../../daemon.mjs';
|
|
7
8
|
|
|
8
9
|
export async function ensureDevCliReady({ cliDir, buildCli }) {
|
|
9
10
|
await ensureDepsInstalled(cliDir, 'happy-cli');
|
|
10
|
-
|
|
11
|
+
const res = await ensureCliBuilt(cliDir, { buildCli });
|
|
12
|
+
|
|
13
|
+
// Fail closed: dev mode must never start the daemon without a usable happy-cli build output.
|
|
14
|
+
// Even if the user disabled CLI builds globally (or build mode is "never"), missing dist will
|
|
15
|
+
// cause an immediate MODULE_NOT_FOUND crash when spawning the daemon.
|
|
16
|
+
const distEntrypoint = join(cliDir, 'dist', 'index.mjs');
|
|
17
|
+
if (!existsSync(distEntrypoint)) {
|
|
18
|
+
// Last-chance recovery: force a build once.
|
|
19
|
+
await ensureCliBuilt(cliDir, { buildCli: true });
|
|
20
|
+
if (!existsSync(distEntrypoint)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`[local] happy-cli build output is missing.\n` +
|
|
23
|
+
`Expected: ${distEntrypoint}\n` +
|
|
24
|
+
`Fix: run the component build directly and inspect its output:\n` +
|
|
25
|
+
` cd "${cliDir}" && yarn build`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return res;
|
|
11
31
|
}
|
|
12
32
|
|
|
13
33
|
export async function prepareDaemonAuthSeed({
|
|
@@ -77,8 +97,25 @@ export function watchHappyCliAndRestartDaemon({
|
|
|
77
97
|
if (!enabled || !startDaemon) return null;
|
|
78
98
|
|
|
79
99
|
let inFlight = false;
|
|
100
|
+
|
|
101
|
+
// IMPORTANT:
|
|
102
|
+
// Watch only source/config paths, not build outputs. Watching the whole repo can
|
|
103
|
+
// trigger rebuild loops because `yarn build` writes to `dist/` (and may touch other
|
|
104
|
+
// generated files), which then retriggers the watcher.
|
|
105
|
+
const watchPaths = [
|
|
106
|
+
join(cliDir, 'src'),
|
|
107
|
+
join(cliDir, 'bin'),
|
|
108
|
+
join(cliDir, 'codex'),
|
|
109
|
+
join(cliDir, 'package.json'),
|
|
110
|
+
join(cliDir, 'tsconfig.json'),
|
|
111
|
+
join(cliDir, 'tsconfig.build.json'),
|
|
112
|
+
join(cliDir, 'pkgroll.config.mjs'),
|
|
113
|
+
join(cliDir, 'yarn.lock'),
|
|
114
|
+
join(cliDir, 'pnpm-lock.yaml'),
|
|
115
|
+
].filter((p) => existsSync(p));
|
|
116
|
+
|
|
80
117
|
return watchDebounced({
|
|
81
|
-
paths: [resolve(
|
|
118
|
+
paths: (watchPaths.length ? watchPaths : [cliDir]).map((p) => resolve(p)),
|
|
82
119
|
debounceMs: 500,
|
|
83
120
|
onChange: async () => {
|
|
84
121
|
if (isShuttingDown?.()) return;
|
|
@@ -88,6 +125,13 @@ export function watchHappyCliAndRestartDaemon({
|
|
|
88
125
|
// eslint-disable-next-line no-console
|
|
89
126
|
console.log('[local] watch: happy-cli changed → rebuilding + restarting daemon...');
|
|
90
127
|
await ensureCliBuilt(cliDir, { buildCli });
|
|
128
|
+
const distEntrypoint = join(cliDir, 'dist', 'index.mjs');
|
|
129
|
+
if (!existsSync(distEntrypoint)) {
|
|
130
|
+
console.warn(
|
|
131
|
+
`[local] watch: happy-cli build did not produce ${distEntrypoint}; refusing to restart daemon to avoid downtime.`
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
91
135
|
await startLocalDaemonWithAuth({
|
|
92
136
|
cliBin,
|
|
93
137
|
cliHomeDir,
|