happy-stacks 0.1.2 → 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 +164 -89
- package/bin/happys.mjs +70 -10
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/stacks.md +39 -0
- package/extras/swiftbar/auth-login.sh +5 -2
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +131 -81
- package/extras/swiftbar/happys-term.sh +15 -38
- package/extras/swiftbar/happys.sh +15 -32
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +209 -80
- package/extras/swiftbar/lib/system.sh +27 -4
- package/extras/swiftbar/lib/utils.sh +311 -28
- package/extras/swiftbar/pnpm.sh +2 -1
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +11 -2
- package/extras/swiftbar/wt-pr.sh +9 -2
- package/package.json +2 -1
- package/scripts/auth.mjs +521 -226
- package/scripts/build.mjs +29 -10
- package/scripts/cli-link.mjs +6 -6
- package/scripts/completion.mjs +18 -11
- package/scripts/daemon.mjs +133 -31
- package/scripts/dev.mjs +196 -137
- package/scripts/doctor.mjs +44 -55
- package/scripts/edison.mjs +1853 -0
- package/scripts/happy.mjs +10 -25
- package/scripts/init.mjs +46 -31
- package/scripts/install.mjs +21 -15
- package/scripts/lint.mjs +124 -0
- package/scripts/menubar.mjs +76 -10
- package/scripts/migrate.mjs +35 -35
- package/scripts/mobile.mjs +24 -17
- package/scripts/run.mjs +122 -35
- package/scripts/self.mjs +13 -35
- package/scripts/server_flavor.mjs +7 -7
- package/scripts/service.mjs +31 -28
- package/scripts/setup.mjs +694 -0
- package/scripts/setup_pr.mjs +165 -0
- package/scripts/stack.mjs +1851 -363
- package/scripts/stop.mjs +9 -6
- package/scripts/tailscale.mjs +23 -11
- package/scripts/test.mjs +123 -0
- package/scripts/tui.mjs +526 -0
- package/scripts/typecheck.mjs +10 -31
- package/scripts/ui_gateway.mjs +3 -3
- package/scripts/uninstall.mjs +21 -13
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/auth/files.mjs +56 -0
- package/scripts/utils/auth/handy_master_secret.mjs +68 -0
- package/scripts/utils/auth/login_ux.mjs +76 -0
- package/scripts/utils/auth/sources.mjs +12 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +2 -2
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/dev/daemon.mjs +104 -0
- package/scripts/utils/dev/expo_web.mjs +112 -0
- package/scripts/utils/dev/server.mjs +183 -0
- package/scripts/utils/{config.mjs → env/config.mjs} +8 -3
- package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
- package/scripts/utils/{env.mjs → env/env.mjs} +64 -13
- package/scripts/utils/{env_file.mjs → env/env_file.mjs} +38 -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/sandbox.mjs +14 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/{expo.mjs → expo/expo.mjs} +7 -11
- 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} +60 -4
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
- package/scripts/utils/paths/canonical_home.mjs +20 -0
- package/scripts/utils/paths/localhost_host.mjs +9 -0
- package/scripts/utils/{paths.mjs → paths/paths.mjs} +14 -8
- package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +4 -4
- package/scripts/utils/proc/commands.mjs +34 -0
- package/scripts/utils/proc/ownership.mjs +135 -0
- package/scripts/utils/proc/package_scripts.mjs +31 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/proc/pm.mjs +317 -0
- package/scripts/utils/{proc.mjs → proc/proc.mjs} +30 -2
- package/scripts/utils/proc/watch.mjs +63 -0
- package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +109 -94
- package/scripts/utils/server/port.mjs +68 -0
- package/scripts/utils/{server.mjs → server/server.mjs} +36 -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 +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/runtime_state.mjs +87 -0
- package/scripts/utils/stack/stacks.mjs +45 -0
- package/scripts/utils/stack/startup.mjs +208 -0
- package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +85 -42
- package/scripts/utils/ui/browser.mjs +22 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/where.mjs +17 -10
- package/scripts/worktrees.mjs +110 -64
- package/scripts/utils/pm.mjs +0 -303
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
import { pathExists } from '../fs/fs.mjs';
|
|
8
|
+
import { readJsonIfExists, writeJsonAtomic } from '../fs/json.mjs';
|
|
9
|
+
import { run, runCapture, spawnProc } from './proc.mjs';
|
|
10
|
+
import { commandExists } from './commands.mjs';
|
|
11
|
+
import { getDefaultAutostartPaths, getHappyStacksHomeDir } from '../paths/paths.mjs';
|
|
12
|
+
import { resolveInstalledPath, resolveInstalledCliRoot } from '../paths/runtime.mjs';
|
|
13
|
+
|
|
14
|
+
function sha256Hex(s) {
|
|
15
|
+
return createHash('sha256').update(String(s ?? ''), 'utf-8').digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveBuildStatePath({ label, dir }) {
|
|
19
|
+
const homeDir = getHappyStacksHomeDir();
|
|
20
|
+
const key = sha256Hex(resolve(dir));
|
|
21
|
+
return join(homeDir, 'cache', 'build', label, `${key}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function computeGitWorktreeSignature(dir) {
|
|
25
|
+
try {
|
|
26
|
+
// Fast path: only if this is a git worktree.
|
|
27
|
+
const inside = (await runCapture('git', ['-C', dir, 'rev-parse', '--is-inside-work-tree'])).trim();
|
|
28
|
+
if (inside !== 'true') return null;
|
|
29
|
+
const head = (await runCapture('git', ['-C', dir, 'rev-parse', 'HEAD'])).trim();
|
|
30
|
+
// Includes staged + unstaged + untracked changes; captures “dirty” vs “clean”.
|
|
31
|
+
const status = await runCapture('git', ['-C', dir, 'status', '--porcelain=v1']);
|
|
32
|
+
return {
|
|
33
|
+
kind: 'git',
|
|
34
|
+
head,
|
|
35
|
+
statusHash: sha256Hex(status),
|
|
36
|
+
signature: sha256Hex(`${head}\n${status}`),
|
|
37
|
+
};
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function requirePnpm() {
|
|
44
|
+
if (await commandExists('pnpm')) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(
|
|
48
|
+
'[local] pnpm is required to install dependencies for Happy Stacks.\n' +
|
|
49
|
+
'Install it via Corepack: `corepack enable && corepack prepare pnpm@latest --activate`'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getComponentPm(dir) {
|
|
54
|
+
const yarnLock = join(dir, 'yarn.lock');
|
|
55
|
+
if (await pathExists(yarnLock)) {
|
|
56
|
+
// IMPORTANT: when happy-stacks itself is pinned to pnpm via Corepack, running `yarn`
|
|
57
|
+
// from the happy-stacks cwd can be blocked. Always probe yarn with cwd=componentDir.
|
|
58
|
+
if (!(await commandExists('yarn', { cwd: dir }))) {
|
|
59
|
+
throw new Error(`[local] yarn is required for component at ${dir} (yarn.lock present). Install it via Corepack: \`corepack enable\``);
|
|
60
|
+
}
|
|
61
|
+
return { name: 'yarn', cmd: 'yarn' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Default fallback if no yarn.lock: use pnpm.
|
|
65
|
+
await requirePnpm();
|
|
66
|
+
return { name: 'pnpm', cmd: 'pnpm' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function requireDir(label, dir) {
|
|
70
|
+
if (await pathExists(dir)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(
|
|
74
|
+
`[local] missing ${label} at ${dir}\n` +
|
|
75
|
+
`Run: happys bootstrap (auto-clones missing components), or place the repo under components/`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function ensureDepsInstalled(dir, label, { quiet = false } = {}) {
|
|
80
|
+
const pkgJson = join(dir, 'package.json');
|
|
81
|
+
if (!(await pathExists(pkgJson))) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const nodeModules = join(dir, 'node_modules');
|
|
86
|
+
const pnpmModulesMeta = join(dir, 'node_modules', '.modules.yaml');
|
|
87
|
+
const pm = await getComponentPm(dir);
|
|
88
|
+
const stdio = quiet ? 'ignore' : 'inherit';
|
|
89
|
+
|
|
90
|
+
if (await pathExists(nodeModules)) {
|
|
91
|
+
const yarnLock = join(dir, 'yarn.lock');
|
|
92
|
+
const yarnIntegrity = join(nodeModules, '.yarn-integrity');
|
|
93
|
+
const pnpmLock = join(dir, 'pnpm-lock.yaml');
|
|
94
|
+
|
|
95
|
+
// If this repo is Yarn-managed (yarn.lock present) but node_modules was created by pnpm,
|
|
96
|
+
// reinstall with Yarn to restore upstream-locked dependency versions.
|
|
97
|
+
if (pm.name === 'yarn' && (await pathExists(pnpmModulesMeta))) {
|
|
98
|
+
if (!quiet) {
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log(`[local] converting ${label} dependencies back to yarn (reinstalling node_modules)...`);
|
|
101
|
+
}
|
|
102
|
+
await rm(nodeModules, { recursive: true, force: true });
|
|
103
|
+
await run(pm.cmd, ['install'], { cwd: dir, stdio });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If dependencies changed since the last install, re-run install even if node_modules exists.
|
|
107
|
+
const mtimeMs = async (p) => {
|
|
108
|
+
try {
|
|
109
|
+
const s = await stat(p);
|
|
110
|
+
return s.mtimeMs ?? 0;
|
|
111
|
+
} catch {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (pm.name === 'yarn' && (await pathExists(yarnLock))) {
|
|
117
|
+
const lockM = await mtimeMs(yarnLock);
|
|
118
|
+
const pkgM = await mtimeMs(pkgJson);
|
|
119
|
+
const intM = await mtimeMs(yarnIntegrity);
|
|
120
|
+
if (!intM || lockM > intM || pkgM > intM) {
|
|
121
|
+
if (!quiet) {
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.log(`[local] refreshing ${label} dependencies (yarn.lock/package.json changed)...`);
|
|
124
|
+
}
|
|
125
|
+
await run(pm.cmd, ['install'], { cwd: dir, stdio });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (pm.name === 'pnpm' && (await pathExists(pnpmLock))) {
|
|
130
|
+
const lockM = await mtimeMs(pnpmLock);
|
|
131
|
+
const metaM = await mtimeMs(pnpmModulesMeta);
|
|
132
|
+
if (!metaM || lockM > metaM) {
|
|
133
|
+
if (!quiet) {
|
|
134
|
+
// eslint-disable-next-line no-console
|
|
135
|
+
console.log(`[local] refreshing ${label} dependencies (pnpm-lock changed)...`);
|
|
136
|
+
}
|
|
137
|
+
await run(pm.cmd, ['install'], { cwd: dir, stdio });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!quiet) {
|
|
145
|
+
// eslint-disable-next-line no-console
|
|
146
|
+
console.log(`[local] installing ${label} dependencies (first run)...`);
|
|
147
|
+
}
|
|
148
|
+
await run(pm.cmd, ['install'], { cwd: dir, stdio });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function ensureCliBuilt(cliDir, { buildCli }) {
|
|
152
|
+
await ensureDepsInstalled(cliDir, 'happy-cli');
|
|
153
|
+
if (!buildCli) {
|
|
154
|
+
return { built: false, reason: 'disabled' };
|
|
155
|
+
}
|
|
156
|
+
// Default: build only when needed (fast + reliable for worktrees that haven't been built yet).
|
|
157
|
+
//
|
|
158
|
+
// You can force always-build by setting:
|
|
159
|
+
// - HAPPY_STACKS_CLI_BUILD_MODE=always (legacy: HAPPY_LOCAL_CLI_BUILD_MODE=always)
|
|
160
|
+
// Or disable via:
|
|
161
|
+
// - HAPPY_STACKS_CLI_BUILD=0 (legacy: HAPPY_LOCAL_CLI_BUILD=0)
|
|
162
|
+
const modeRaw = (process.env.HAPPY_STACKS_CLI_BUILD_MODE ?? process.env.HAPPY_LOCAL_CLI_BUILD_MODE ?? 'auto').trim().toLowerCase();
|
|
163
|
+
const mode = modeRaw === 'always' || modeRaw === 'auto' || modeRaw === 'never' ? modeRaw : 'auto';
|
|
164
|
+
if (mode === 'never') {
|
|
165
|
+
return { built: false, reason: 'mode_never' };
|
|
166
|
+
}
|
|
167
|
+
const distEntrypoint = join(cliDir, 'dist', 'index.mjs');
|
|
168
|
+
const buildStatePath = resolveBuildStatePath({ label: 'happy-cli', dir: cliDir });
|
|
169
|
+
const gitSig = await computeGitWorktreeSignature(cliDir);
|
|
170
|
+
const prev = await readJsonIfExists(buildStatePath);
|
|
171
|
+
|
|
172
|
+
if (mode === 'auto') {
|
|
173
|
+
// If dist doesn't exist, we must build.
|
|
174
|
+
if (!(await pathExists(distEntrypoint))) {
|
|
175
|
+
// fallthrough to build
|
|
176
|
+
} else if (gitSig && prev?.signature && prev.signature === gitSig.signature) {
|
|
177
|
+
return { built: false, reason: 'up_to_date' };
|
|
178
|
+
} else if (!gitSig) {
|
|
179
|
+
// No git info: best-effort skip if dist exists (keeps this fast outside git worktrees).
|
|
180
|
+
return { built: false, reason: 'no_git_info' };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// eslint-disable-next-line no-console
|
|
185
|
+
console.log('[local] building happy-cli...');
|
|
186
|
+
const pm = await getComponentPm(cliDir);
|
|
187
|
+
await run(pm.cmd, ['build'], { cwd: cliDir });
|
|
188
|
+
|
|
189
|
+
// Persist new build state (best-effort).
|
|
190
|
+
const nowSig = gitSig ?? (await computeGitWorktreeSignature(cliDir));
|
|
191
|
+
if (nowSig) {
|
|
192
|
+
await writeJsonAtomic(buildStatePath, {
|
|
193
|
+
label: 'happy-cli',
|
|
194
|
+
dir: resolve(cliDir),
|
|
195
|
+
signature: nowSig.signature,
|
|
196
|
+
head: nowSig.head,
|
|
197
|
+
statusHash: nowSig.statusHash,
|
|
198
|
+
builtAt: new Date().toISOString(),
|
|
199
|
+
}).catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
return { built: true, reason: mode === 'always' ? 'mode_always' : 'changed' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getPathEntries() {
|
|
205
|
+
const raw = process.env.PATH ?? '';
|
|
206
|
+
const delimiter = process.platform === 'win32' ? ';' : ':';
|
|
207
|
+
return raw.split(delimiter).filter(Boolean);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isPathInside(path, dir) {
|
|
211
|
+
const p = resolve(path);
|
|
212
|
+
const d = resolve(dir);
|
|
213
|
+
return p === d || p.startsWith(d.endsWith(sep) ? d : d + sep);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function ensureHappyCliLocalNpmLinked(rootDir, { npmLinkCli }) {
|
|
217
|
+
if (!npmLinkCli) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const homeDir = getHappyStacksHomeDir();
|
|
222
|
+
const binDir = join(homeDir, 'bin');
|
|
223
|
+
await mkdir(binDir, { recursive: true });
|
|
224
|
+
|
|
225
|
+
const happysShim = join(binDir, 'happys');
|
|
226
|
+
const happyShim = join(binDir, 'happy');
|
|
227
|
+
|
|
228
|
+
const shim = `#!/bin/bash
|
|
229
|
+
set -euo pipefail
|
|
230
|
+
# Prefer the sibling happys shim (works for sandbox installs too).
|
|
231
|
+
BIN_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
232
|
+
HAPPYS="$BIN_DIR/happys"
|
|
233
|
+
if [[ -x "$HAPPYS" ]]; then
|
|
234
|
+
exec "$HAPPYS" happy "$@"
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# Fallback: run happy-stacks from runtime install if present.
|
|
238
|
+
HOME_DIR="\${HAPPY_STACKS_HOME_DIR:-\${HAPPY_LOCAL_HOME_DIR:-$HOME/.happy-stacks}}"
|
|
239
|
+
RUNTIME="$HOME_DIR/runtime/node_modules/happy-stacks/bin/happys.mjs"
|
|
240
|
+
if [[ -f "$RUNTIME" ]]; then
|
|
241
|
+
exec node "$RUNTIME" happy "$@"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
echo "error: cannot find happys shim or runtime install" >&2
|
|
245
|
+
exit 1
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
const writeIfChanged = async (path, text) => {
|
|
249
|
+
let existing = '';
|
|
250
|
+
try {
|
|
251
|
+
existing = await readFile(path, 'utf-8');
|
|
252
|
+
} catch {
|
|
253
|
+
existing = '';
|
|
254
|
+
}
|
|
255
|
+
if (existing === text) return false;
|
|
256
|
+
await writeFile(path, text, 'utf-8');
|
|
257
|
+
return true;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
await writeIfChanged(happyShim, shim);
|
|
261
|
+
await chmod(happyShim, 0o755).catch(() => {});
|
|
262
|
+
|
|
263
|
+
// happys shim: use node + CLI root; if runtime install exists, prefer it.
|
|
264
|
+
const cliRoot = resolveInstalledCliRoot(rootDir);
|
|
265
|
+
const happysShimText = `#!/bin/bash
|
|
266
|
+
set -euo pipefail
|
|
267
|
+
exec node "${resolveInstalledPath(rootDir, 'bin/happys.mjs')}" "$@"
|
|
268
|
+
`;
|
|
269
|
+
await writeIfChanged(happysShim, happysShimText);
|
|
270
|
+
await chmod(happysShim, 0o755).catch(() => {});
|
|
271
|
+
|
|
272
|
+
// If user’s PATH points at a legacy install path, try to make it sane (best-effort).
|
|
273
|
+
const entries = getPathEntries();
|
|
274
|
+
const legacyBin = join(homedir(), '.happy-stacks', 'bin');
|
|
275
|
+
const newBin = join(getDefaultAutostartPaths().baseDir, 'bin');
|
|
276
|
+
if (entries.some((p) => isPathInside(p, legacyBin)) && !entries.some((p) => isPathInside(p, newBin))) {
|
|
277
|
+
// eslint-disable-next-line no-console
|
|
278
|
+
console.log(`[local] note: your PATH includes ${legacyBin}; recommended path is ${newBin}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { ok: true, cliRoot, binDir, happyShim, happysShim };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function pmExecBin(dirOrOpts, binArg, argsArg, optsArg) {
|
|
285
|
+
const usesObjectStyle = typeof dirOrOpts === 'object' && dirOrOpts !== null;
|
|
286
|
+
|
|
287
|
+
const dir = usesObjectStyle ? dirOrOpts.dir : dirOrOpts;
|
|
288
|
+
const bin = usesObjectStyle ? dirOrOpts.bin : binArg;
|
|
289
|
+
const args = usesObjectStyle ? (dirOrOpts.args ?? []) : (argsArg ?? []);
|
|
290
|
+
|
|
291
|
+
const env = usesObjectStyle ? (dirOrOpts.env ?? process.env) : (optsArg?.env ?? process.env);
|
|
292
|
+
const quiet = usesObjectStyle ? Boolean(dirOrOpts.quiet) : Boolean(optsArg?.quiet);
|
|
293
|
+
const stdio = quiet ? 'ignore' : 'inherit';
|
|
294
|
+
|
|
295
|
+
const pm = await getComponentPm(dir);
|
|
296
|
+
if (pm.name === 'yarn') {
|
|
297
|
+
await run(pm.cmd, ['run', bin, ...args], { cwd: dir, env, stdio });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
await run(pm.cmd, ['exec', bin, ...args], { cwd: dir, env, stdio });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function pmSpawnBin(dir, label, bin, args, { env = process.env } = {}) {
|
|
304
|
+
const pm = await getComponentPm(dir);
|
|
305
|
+
if (pm.name === 'yarn') {
|
|
306
|
+
return spawnProc(label, pm.cmd, ['run', bin, ...args], env, { cwd: dir });
|
|
307
|
+
}
|
|
308
|
+
return spawnProc(label, pm.cmd, ['exec', bin, ...args], env, { cwd: dir });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function pmSpawnScript(dir, label, script, args, { env = process.env } = {}) {
|
|
312
|
+
const pm = await getComponentPm(dir);
|
|
313
|
+
if (pm.name === 'yarn') {
|
|
314
|
+
return spawnProc(label, pm.cmd, ['run', script, ...args], env, { cwd: dir });
|
|
315
|
+
}
|
|
316
|
+
return spawnProc(label, pm.cmd, ['run', script, ...args], env, { cwd: dir });
|
|
317
|
+
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
|
|
3
|
+
function writeWithPrefix(stream, prefix, bufState, chunk) {
|
|
4
|
+
const s = chunk.toString();
|
|
5
|
+
bufState.buf += s;
|
|
6
|
+
while (true) {
|
|
7
|
+
const idx = bufState.buf.indexOf('\n');
|
|
8
|
+
if (idx < 0) break;
|
|
9
|
+
const line = bufState.buf.slice(0, idx);
|
|
10
|
+
bufState.buf = bufState.buf.slice(idx + 1);
|
|
11
|
+
stream.write(`${prefix}${line}\n`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function flushPrefixed(stream, prefix, bufState) {
|
|
16
|
+
if (!bufState.buf) return;
|
|
17
|
+
stream.write(`${prefix}${bufState.buf}\n`);
|
|
18
|
+
bufState.buf = '';
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export function spawnProc(label, cmd, args, env, options = {}) {
|
|
4
22
|
const child = spawn(cmd, args, {
|
|
5
23
|
env,
|
|
@@ -10,8 +28,17 @@ export function spawnProc(label, cmd, args, env, options = {}) {
|
|
|
10
28
|
...options,
|
|
11
29
|
});
|
|
12
30
|
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
const outState = { buf: '' };
|
|
32
|
+
const errState = { buf: '' };
|
|
33
|
+
const outPrefix = `[${label}] `;
|
|
34
|
+
const errPrefix = `[${label}] `;
|
|
35
|
+
|
|
36
|
+
child.stdout?.on('data', (d) => writeWithPrefix(process.stdout, outPrefix, outState, d));
|
|
37
|
+
child.stderr?.on('data', (d) => writeWithPrefix(process.stderr, errPrefix, errState, d));
|
|
38
|
+
child.on('close', () => {
|
|
39
|
+
flushPrefixed(process.stdout, outPrefix, outState);
|
|
40
|
+
flushPrefixed(process.stderr, errPrefix, errState);
|
|
41
|
+
});
|
|
15
42
|
child.on('exit', (code, sig) => {
|
|
16
43
|
if (code !== 0) {
|
|
17
44
|
process.stderr.write(`[${label}] exited (code=${code}, sig=${sig})\n`);
|
|
@@ -105,3 +132,4 @@ export async function runCapture(cmd, args, options = {}) {
|
|
|
105
132
|
});
|
|
106
133
|
});
|
|
107
134
|
}
|
|
135
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
function safeWatch(path, handler) {
|
|
4
|
+
try {
|
|
5
|
+
// Node supports recursive watching on macOS and Windows. On Linux this may throw; we fail closed by returning null.
|
|
6
|
+
return watch(path, { recursive: true }, handler);
|
|
7
|
+
} catch {
|
|
8
|
+
try {
|
|
9
|
+
return watch(path, {}, handler);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Very small, dependency-free debounced watcher.
|
|
18
|
+
* Intended for dev ergonomics (rebuild/restart), not for correctness-critical logic.
|
|
19
|
+
*/
|
|
20
|
+
export function watchDebounced({ paths, debounceMs = 500, onChange } = {}) {
|
|
21
|
+
const list = Array.isArray(paths) ? paths.filter(Boolean) : [];
|
|
22
|
+
if (!list.length) return null;
|
|
23
|
+
if (typeof onChange !== 'function') return null;
|
|
24
|
+
|
|
25
|
+
let closed = false;
|
|
26
|
+
let t = null;
|
|
27
|
+
const watchers = [];
|
|
28
|
+
|
|
29
|
+
const trigger = (eventType, filename) => {
|
|
30
|
+
if (closed) return;
|
|
31
|
+
if (t) clearTimeout(t);
|
|
32
|
+
t = setTimeout(() => {
|
|
33
|
+
t = null;
|
|
34
|
+
try {
|
|
35
|
+
onChange({ eventType, filename });
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
}, debounceMs);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const p of list) {
|
|
43
|
+
const w = safeWatch(p, trigger);
|
|
44
|
+
if (w) watchers.push(w);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!watchers.length) return null;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
close() {
|
|
51
|
+
closed = true;
|
|
52
|
+
if (t) clearTimeout(t);
|
|
53
|
+
for (const w of watchers) {
|
|
54
|
+
try {
|
|
55
|
+
w.close();
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|