happy-stacks 0.1.0 → 0.2.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 +130 -74
- package/bin/happys.mjs +140 -9
- 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/server-flavors.md +61 -2
- package/docs/stacks.md +55 -4
- package/extras/swiftbar/auth-login.sh +10 -7
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +175 -83
- package/extras/swiftbar/happys-term.sh +128 -0
- package/extras/swiftbar/happys.sh +35 -0
- 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 +279 -132
- package/extras/swiftbar/lib/system.sh +64 -10
- package/extras/swiftbar/lib/utils.sh +469 -10
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +4 -14
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +19 -10
- package/extras/swiftbar/wt-pr.sh +10 -3
- package/package.json +2 -1
- package/scripts/auth.mjs +833 -14
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +200 -23
- package/scripts/dev.mjs +230 -57
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +275 -46
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +302 -0
- package/scripts/mobile.mjs +59 -21
- package/scripts/run.mjs +222 -43
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +190 -38
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +2273 -92
- package/scripts/stop.mjs +160 -0
- package/scripts/tailscale.mjs +164 -23
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +21 -13
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +13 -1
- package/scripts/utils/dev_auth_key.mjs +169 -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/env.mjs +94 -23
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +96 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +484 -0
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +132 -22
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +75 -7
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +61 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +255 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +135 -15
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
package/scripts/happy.mjs
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
4
|
import { join } from 'node:path';
|
|
6
|
-
import { parseArgs } from './utils/args.mjs';
|
|
7
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
5
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
6
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
7
|
+
import { expandHome } from './utils/canonical_home.mjs';
|
|
8
8
|
import { getComponentDir, getDefaultAutostartPaths, getRootDir } from './utils/paths.mjs';
|
|
9
9
|
|
|
10
|
-
function expandHome(p) {
|
|
11
|
-
return p.replace(/^~(?=\/)/, homedir());
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
function resolveCliHomeDir() {
|
|
15
11
|
const fromExplicit = (process.env.HAPPY_HOME_DIR ?? '').trim();
|
|
16
12
|
if (fromExplicit) {
|
package/scripts/init.mjs
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { ensureHomeEnvUpdated } from './utils/config.mjs';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
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
|
+
}
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
function getCliRootDir() {
|
|
@@ -22,6 +31,47 @@ function parseArgValue(argv, key) {
|
|
|
22
31
|
return null;
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
function firstNonEmpty(...values) {
|
|
35
|
+
for (const v of values) {
|
|
36
|
+
const s = (v ?? '').trim();
|
|
37
|
+
if (s) return s;
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadEnvFile(path, { override = false, overridePrefix = null } = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const contents = await readFile(path, 'utf-8');
|
|
45
|
+
const parsed = parseDotenv(contents);
|
|
46
|
+
for (const [k, v] of parsed.entries()) {
|
|
47
|
+
const allowOverride = override && (!overridePrefix || k.startsWith(overridePrefix));
|
|
48
|
+
if (allowOverride || process.env[k] == null || process.env[k] === '') {
|
|
49
|
+
process.env[k] = v;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore missing/invalid env file
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isWorkspaceBootstrapped(workspaceDir) {
|
|
58
|
+
// Heuristic: if the expected component repos exist in the workspace, we consider bootstrap "already done"
|
|
59
|
+
// and avoid re-running the interactive bootstrap wizard from `happys init`.
|
|
60
|
+
//
|
|
61
|
+
// Users can always re-run bootstrap explicitly:
|
|
62
|
+
// happys bootstrap --interactive
|
|
63
|
+
try {
|
|
64
|
+
const componentsDir = join(workspaceDir, 'components');
|
|
65
|
+
const ui = join(componentsDir, 'happy', 'package.json');
|
|
66
|
+
const cli = join(componentsDir, 'happy-cli', 'package.json');
|
|
67
|
+
const serverLight = join(componentsDir, 'happy-server-light', 'package.json');
|
|
68
|
+
const serverFull = join(componentsDir, 'happy-server', 'package.json');
|
|
69
|
+
return existsSync(ui) && existsSync(cli) && (existsSync(serverLight) || existsSync(serverFull));
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
25
75
|
async function writeExecutable(path, contents) {
|
|
26
76
|
await writeFile(path, contents, { mode: 0o755 });
|
|
27
77
|
}
|
|
@@ -87,64 +137,175 @@ async function main() {
|
|
|
87
137
|
if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
|
|
88
138
|
console.log([
|
|
89
139
|
'[init] usage:',
|
|
90
|
-
' happys init [--home-dir=/path] [--workspace-dir=/path] [--runtime-dir=/path] [--install-path] [--no-runtime] [--no-bootstrap] [--] [bootstrap args...]',
|
|
140
|
+
' happys init [--canonical-home-dir=/path] [--home-dir=/path] [--workspace-dir=/path] [--runtime-dir=/path] [--storage-dir=/path] [--cli-root-dir=/path] [--tailscale-bin=/path] [--tailscale-cmd-timeout-ms=MS] [--tailscale-enable-timeout-ms=MS] [--tailscale-enable-timeout-ms-auto=MS] [--tailscale-reset-timeout-ms=MS] [--install-path] [--no-runtime] [--force-runtime] [--no-bootstrap] [--] [bootstrap args...]',
|
|
91
141
|
'',
|
|
92
142
|
'notes:',
|
|
93
|
-
' - writes
|
|
94
|
-
' - default workspace:
|
|
95
|
-
' - default runtime:
|
|
96
|
-
' -
|
|
97
|
-
' -
|
|
143
|
+
' - writes <canonicalHomeDir>/.env (stable pointer file; default: ~/.happy-stacks/.env)',
|
|
144
|
+
' - default workspace: <homeDir>/workspace',
|
|
145
|
+
' - default runtime: <homeDir>/runtime (recommended for services/SwiftBar)',
|
|
146
|
+
' - runtime install is skipped if the same version is already installed (use --force-runtime to reinstall)',
|
|
147
|
+
' - set HAPPY_STACKS_INIT_NO_RUNTIME=1 to persist skipping runtime installs on this machine',
|
|
148
|
+
' - optional: --install-path adds <homeDir>/bin to your shell PATH (idempotent)',
|
|
149
|
+
' - by default, runs `happys bootstrap --interactive` at the end (TTY only) IF components are not already present',
|
|
98
150
|
].join('\n'));
|
|
99
151
|
return;
|
|
100
152
|
}
|
|
101
153
|
|
|
102
154
|
const cliRootDir = getCliRootDir();
|
|
103
155
|
|
|
156
|
+
// Important: `happys init` must be idempotent and must not "forget" custom dirs from a prior install.
|
|
157
|
+
//
|
|
158
|
+
// Other scripts load this pointer via `scripts/utils/env.mjs`, but `init.mjs` is often run before
|
|
159
|
+
// anything else (or directly from a repo checkout). So we load it here too.
|
|
160
|
+
const canonicalHomeDirRaw = parseArgValue(argv, 'canonical-home-dir');
|
|
161
|
+
const canonicalHomeDir = expandHome(firstNonEmpty(
|
|
162
|
+
canonicalHomeDirRaw,
|
|
163
|
+
process.env.HAPPY_STACKS_CANONICAL_HOME_DIR,
|
|
164
|
+
process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR,
|
|
165
|
+
join(homedir(), '.happy-stacks'),
|
|
166
|
+
));
|
|
167
|
+
process.env.HAPPY_STACKS_CANONICAL_HOME_DIR = canonicalHomeDir;
|
|
168
|
+
process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR = process.env.HAPPY_LOCAL_CANONICAL_HOME_DIR ?? canonicalHomeDir;
|
|
169
|
+
|
|
170
|
+
const canonicalEnvPath = join(canonicalHomeDir, '.env');
|
|
171
|
+
if (existsSync(canonicalEnvPath)) {
|
|
172
|
+
await loadEnvFile(canonicalEnvPath, { override: false });
|
|
173
|
+
await loadEnvFile(canonicalEnvPath, { override: true, overridePrefix: 'HAPPY_STACKS_' });
|
|
174
|
+
await loadEnvFile(canonicalEnvPath, { override: true, overridePrefix: 'HAPPY_LOCAL_' });
|
|
175
|
+
}
|
|
176
|
+
|
|
104
177
|
const homeDirRaw = parseArgValue(argv, 'home-dir');
|
|
105
|
-
const homeDir = expandHome((
|
|
178
|
+
const homeDir = expandHome(firstNonEmpty(
|
|
179
|
+
homeDirRaw,
|
|
180
|
+
process.env.HAPPY_STACKS_HOME_DIR,
|
|
181
|
+
process.env.HAPPY_LOCAL_HOME_DIR,
|
|
182
|
+
join(homedir(), '.happy-stacks'),
|
|
183
|
+
));
|
|
106
184
|
process.env.HAPPY_STACKS_HOME_DIR = homeDir;
|
|
185
|
+
process.env.HAPPY_LOCAL_HOME_DIR = process.env.HAPPY_LOCAL_HOME_DIR ?? homeDir;
|
|
107
186
|
|
|
108
187
|
const workspaceDirRaw = parseArgValue(argv, 'workspace-dir');
|
|
109
|
-
const workspaceDir = expandHome((
|
|
110
|
-
|
|
188
|
+
const workspaceDir = expandHome(firstNonEmpty(
|
|
189
|
+
workspaceDirRaw,
|
|
190
|
+
process.env.HAPPY_STACKS_WORKSPACE_DIR,
|
|
191
|
+
process.env.HAPPY_LOCAL_WORKSPACE_DIR,
|
|
192
|
+
join(homeDir, 'workspace'),
|
|
193
|
+
));
|
|
194
|
+
process.env.HAPPY_STACKS_WORKSPACE_DIR = workspaceDir;
|
|
195
|
+
process.env.HAPPY_LOCAL_WORKSPACE_DIR = process.env.HAPPY_LOCAL_WORKSPACE_DIR ?? workspaceDir;
|
|
111
196
|
|
|
112
197
|
const runtimeDirRaw = parseArgValue(argv, 'runtime-dir');
|
|
113
|
-
const runtimeDir = expandHome((
|
|
114
|
-
|
|
198
|
+
const runtimeDir = expandHome(firstNonEmpty(
|
|
199
|
+
runtimeDirRaw,
|
|
200
|
+
process.env.HAPPY_STACKS_RUNTIME_DIR,
|
|
201
|
+
process.env.HAPPY_LOCAL_RUNTIME_DIR,
|
|
202
|
+
join(homeDir, 'runtime'),
|
|
203
|
+
));
|
|
204
|
+
process.env.HAPPY_STACKS_RUNTIME_DIR = runtimeDir;
|
|
205
|
+
process.env.HAPPY_LOCAL_RUNTIME_DIR = process.env.HAPPY_LOCAL_RUNTIME_DIR ?? runtimeDir;
|
|
206
|
+
|
|
207
|
+
const storageDirRaw = parseArgValue(argv, 'storage-dir');
|
|
208
|
+
const storageDirOverride = expandHome((storageDirRaw ?? '').trim());
|
|
209
|
+
if (storageDirOverride) {
|
|
210
|
+
process.env.HAPPY_STACKS_STORAGE_DIR = process.env.HAPPY_STACKS_STORAGE_DIR ?? storageDirOverride;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cliRootDirRaw = parseArgValue(argv, 'cli-root-dir');
|
|
214
|
+
const cliRootDirOverride = expandHome((cliRootDirRaw ?? '').trim());
|
|
215
|
+
if (cliRootDirOverride) {
|
|
216
|
+
process.env.HAPPY_STACKS_CLI_ROOT_DIR = process.env.HAPPY_STACKS_CLI_ROOT_DIR ?? cliRootDirOverride;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const tailscaleBinRaw = parseArgValue(argv, 'tailscale-bin');
|
|
220
|
+
const tailscaleBinOverride = expandHome((tailscaleBinRaw ?? '').trim());
|
|
221
|
+
if (tailscaleBinOverride) {
|
|
222
|
+
process.env.HAPPY_STACKS_TAILSCALE_BIN = process.env.HAPPY_STACKS_TAILSCALE_BIN ?? tailscaleBinOverride;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const tailscaleCmdTimeoutMsRaw = parseArgValue(argv, 'tailscale-cmd-timeout-ms');
|
|
226
|
+
const tailscaleCmdTimeoutMsOverride = (tailscaleCmdTimeoutMsRaw ?? '').trim();
|
|
227
|
+
if (tailscaleCmdTimeoutMsOverride) {
|
|
228
|
+
process.env.HAPPY_STACKS_TAILSCALE_CMD_TIMEOUT_MS =
|
|
229
|
+
process.env.HAPPY_STACKS_TAILSCALE_CMD_TIMEOUT_MS ?? tailscaleCmdTimeoutMsOverride;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tailscaleEnableTimeoutMsRaw = parseArgValue(argv, 'tailscale-enable-timeout-ms');
|
|
233
|
+
const tailscaleEnableTimeoutMsOverride = (tailscaleEnableTimeoutMsRaw ?? '').trim();
|
|
234
|
+
if (tailscaleEnableTimeoutMsOverride) {
|
|
235
|
+
process.env.HAPPY_STACKS_TAILSCALE_ENABLE_TIMEOUT_MS =
|
|
236
|
+
process.env.HAPPY_STACKS_TAILSCALE_ENABLE_TIMEOUT_MS ?? tailscaleEnableTimeoutMsOverride;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const tailscaleEnableTimeoutMsAutoRaw = parseArgValue(argv, 'tailscale-enable-timeout-ms-auto');
|
|
240
|
+
const tailscaleEnableTimeoutMsAutoOverride = (tailscaleEnableTimeoutMsAutoRaw ?? '').trim();
|
|
241
|
+
if (tailscaleEnableTimeoutMsAutoOverride) {
|
|
242
|
+
process.env.HAPPY_STACKS_TAILSCALE_ENABLE_TIMEOUT_MS_AUTO =
|
|
243
|
+
process.env.HAPPY_STACKS_TAILSCALE_ENABLE_TIMEOUT_MS_AUTO ?? tailscaleEnableTimeoutMsAutoOverride;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const tailscaleResetTimeoutMsRaw = parseArgValue(argv, 'tailscale-reset-timeout-ms');
|
|
247
|
+
const tailscaleResetTimeoutMsOverride = (tailscaleResetTimeoutMsRaw ?? '').trim();
|
|
248
|
+
if (tailscaleResetTimeoutMsOverride) {
|
|
249
|
+
process.env.HAPPY_STACKS_TAILSCALE_RESET_TIMEOUT_MS =
|
|
250
|
+
process.env.HAPPY_STACKS_TAILSCALE_RESET_TIMEOUT_MS ?? tailscaleResetTimeoutMsOverride;
|
|
251
|
+
}
|
|
115
252
|
|
|
116
253
|
const nodePath = process.execPath;
|
|
117
254
|
|
|
118
255
|
await mkdir(homeDir, { recursive: true });
|
|
256
|
+
await mkdir(canonicalHomeDir, { recursive: true });
|
|
119
257
|
await mkdir(workspaceDir, { recursive: true });
|
|
120
258
|
await mkdir(join(workspaceDir, 'components'), { recursive: true });
|
|
121
259
|
await mkdir(runtimeDir, { recursive: true });
|
|
122
260
|
await mkdir(join(homeDir, 'bin'), { recursive: true });
|
|
123
261
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
262
|
+
const pointerUpdates = [
|
|
263
|
+
{ key: 'HAPPY_STACKS_HOME_DIR', value: homeDir },
|
|
264
|
+
{ key: 'HAPPY_STACKS_WORKSPACE_DIR', value: workspaceDir },
|
|
265
|
+
{ key: 'HAPPY_STACKS_RUNTIME_DIR', value: runtimeDir },
|
|
266
|
+
{ key: 'HAPPY_STACKS_NODE', value: nodePath },
|
|
267
|
+
];
|
|
268
|
+
if (storageDirOverride) {
|
|
269
|
+
pointerUpdates.push({ key: 'HAPPY_STACKS_STORAGE_DIR', value: storageDirOverride });
|
|
270
|
+
}
|
|
271
|
+
if (cliRootDirOverride) {
|
|
272
|
+
pointerUpdates.push({ key: 'HAPPY_STACKS_CLI_ROOT_DIR', value: cliRootDirOverride });
|
|
273
|
+
}
|
|
132
274
|
|
|
133
|
-
|
|
275
|
+
// Write the "real" home env (used by runtime + scripts), AND a stable pointer at ~/.happy-stacks/.env.
|
|
276
|
+
// The pointer file allows launchd/SwiftBar/minimal shells to discover the actual install location
|
|
277
|
+
// even when no env vars are exported.
|
|
278
|
+
await ensureHomeEnvUpdated({ updates: pointerUpdates });
|
|
279
|
+
await ensureCanonicalHomeEnvUpdated({ updates: pointerUpdates });
|
|
280
|
+
|
|
281
|
+
const initNoRuntimeRaw = (process.env.HAPPY_STACKS_INIT_NO_RUNTIME ?? process.env.HAPPY_LOCAL_INIT_NO_RUNTIME ?? '').trim();
|
|
282
|
+
const initNoRuntime = initNoRuntimeRaw === '1' || initNoRuntimeRaw.toLowerCase() === 'true' || initNoRuntimeRaw.toLowerCase() === 'yes';
|
|
283
|
+
const forceRuntime = argv.includes('--force-runtime');
|
|
284
|
+
const skipRuntime = argv.includes('--no-runtime') || (initNoRuntime && !forceRuntime);
|
|
285
|
+
const installRuntime = !skipRuntime;
|
|
134
286
|
if (installRuntime) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const spec =
|
|
287
|
+
const cliPkg = await readJsonIfExists(join(cliRootDir, 'package.json'));
|
|
288
|
+
const cliVersion = String(cliPkg?.version ?? '').trim() || 'latest';
|
|
289
|
+
const spec = cliVersion === '0.0.0' ? 'happy-stacks@latest' : `happy-stacks@${cliVersion}`;
|
|
138
290
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
291
|
+
const runtimePkgPath = join(runtimeDir, 'node_modules', 'happy-stacks', 'package.json');
|
|
292
|
+
const runtimePkg = await readJsonIfExists(runtimePkgPath);
|
|
293
|
+
const runtimeVersion = String(runtimePkg?.version ?? '').trim();
|
|
294
|
+
const sameVersionInstalled = Boolean(cliVersion && cliVersion !== '0.0.0' && runtimeVersion && runtimeVersion === cliVersion);
|
|
295
|
+
|
|
296
|
+
if (!forceRuntime && sameVersionInstalled) {
|
|
297
|
+
console.log(`[init] runtime already installed in ${runtimeDir} (happy-stacks@${runtimeVersion})`);
|
|
298
|
+
} else {
|
|
299
|
+
console.log(`[init] installing runtime into ${runtimeDir} (${spec})...`);
|
|
300
|
+
let res = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--silent', '--prefix', runtimeDir, spec], { stdio: 'inherit' });
|
|
146
301
|
if (res.status !== 0) {
|
|
147
|
-
|
|
302
|
+
// Pre-publish developer experience: if the package isn't on npm yet (E404),
|
|
303
|
+
// fall back to installing the local checkout into the runtime prefix.
|
|
304
|
+
console.log(`[init] runtime install failed; attempting local install from ${cliRootDir}...`);
|
|
305
|
+
res = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--silent', '--prefix', runtimeDir, cliRootDir], { stdio: 'inherit' });
|
|
306
|
+
if (res.status !== 0) {
|
|
307
|
+
process.exit(res.status ?? 1);
|
|
308
|
+
}
|
|
148
309
|
}
|
|
149
310
|
}
|
|
150
311
|
}
|
|
@@ -154,15 +315,58 @@ async function main() {
|
|
|
154
315
|
const shim = [
|
|
155
316
|
'#!/bin/bash',
|
|
156
317
|
'set -euo pipefail',
|
|
157
|
-
|
|
318
|
+
`CANONICAL_ENV="${canonicalEnvPath}"`,
|
|
319
|
+
'',
|
|
320
|
+
'# Best-effort: if env vars are not exported (common under launchd/SwiftBar),',
|
|
321
|
+
'# read the stable pointer file at CANONICAL_ENV to discover the real dirs.',
|
|
322
|
+
'if [[ -f "$CANONICAL_ENV" ]]; then',
|
|
323
|
+
' if [[ -z "${HAPPY_STACKS_HOME_DIR:-}" ]]; then',
|
|
324
|
+
' HAPPY_STACKS_HOME_DIR="$(grep -E \'^HAPPY_STACKS_HOME_DIR=\' "$CANONICAL_ENV" | head -n 1 | sed \'s/^HAPPY_STACKS_HOME_DIR=//\')" || true',
|
|
325
|
+
' export HAPPY_STACKS_HOME_DIR',
|
|
326
|
+
' fi',
|
|
327
|
+
' if [[ -z "${HAPPY_STACKS_WORKSPACE_DIR:-}" ]]; then',
|
|
328
|
+
' HAPPY_STACKS_WORKSPACE_DIR="$(grep -E \'^HAPPY_STACKS_WORKSPACE_DIR=\' "$CANONICAL_ENV" | head -n 1 | sed \'s/^HAPPY_STACKS_WORKSPACE_DIR=//\')" || true',
|
|
329
|
+
' export HAPPY_STACKS_WORKSPACE_DIR',
|
|
330
|
+
' fi',
|
|
331
|
+
' if [[ -z "${HAPPY_STACKS_RUNTIME_DIR:-}" ]]; then',
|
|
332
|
+
' HAPPY_STACKS_RUNTIME_DIR="$(grep -E \'^HAPPY_STACKS_RUNTIME_DIR=\' "$CANONICAL_ENV" | head -n 1 | sed \'s/^HAPPY_STACKS_RUNTIME_DIR=//\')" || true',
|
|
333
|
+
' export HAPPY_STACKS_RUNTIME_DIR',
|
|
334
|
+
' fi',
|
|
335
|
+
' if [[ -z "${HAPPY_STACKS_NODE:-}" ]]; then',
|
|
336
|
+
' HAPPY_STACKS_NODE="$(grep -E \'^HAPPY_STACKS_NODE=\' "$CANONICAL_ENV" | head -n 1 | sed \'s/^HAPPY_STACKS_NODE=//\')" || true',
|
|
337
|
+
' export HAPPY_STACKS_NODE',
|
|
338
|
+
' fi',
|
|
339
|
+
' if [[ -z "${HAPPY_STACKS_CLI_ROOT_DIR:-}" ]]; then',
|
|
340
|
+
' HAPPY_STACKS_CLI_ROOT_DIR="$(grep -E \'^HAPPY_STACKS_CLI_ROOT_DIR=\' "$CANONICAL_ENV" | head -n 1 | sed \'s/^HAPPY_STACKS_CLI_ROOT_DIR=//\')" || true',
|
|
341
|
+
' export HAPPY_STACKS_CLI_ROOT_DIR',
|
|
342
|
+
' fi',
|
|
343
|
+
'fi',
|
|
344
|
+
'',
|
|
345
|
+
`HOME_DIR="\${HAPPY_STACKS_HOME_DIR:-${canonicalHomeDir}}"`,
|
|
158
346
|
'ENV_FILE="$HOME_DIR/.env"',
|
|
159
|
-
'
|
|
160
|
-
'if [[ -
|
|
347
|
+
'WORKDIR="${HAPPY_STACKS_WORKSPACE_DIR:-$HOME_DIR/workspace}"',
|
|
348
|
+
'if [[ -d "$WORKDIR" ]]; then',
|
|
349
|
+
' cd "$WORKDIR"',
|
|
350
|
+
'else',
|
|
351
|
+
' cd "$HOME"',
|
|
352
|
+
'fi',
|
|
353
|
+
'NODE_BIN="${HAPPY_STACKS_NODE:-}"',
|
|
354
|
+
'if [[ -z "$NODE_BIN" && -f "$ENV_FILE" ]]; then',
|
|
161
355
|
' NODE_BIN="$(grep -E \'^HAPPY_STACKS_NODE=\' "$ENV_FILE" | head -n 1 | sed \'s/^HAPPY_STACKS_NODE=//\')"',
|
|
162
356
|
'fi',
|
|
163
357
|
'if [[ -z "$NODE_BIN" ]]; then',
|
|
164
358
|
' NODE_BIN="$(command -v node 2>/dev/null || true)"',
|
|
165
359
|
'fi',
|
|
360
|
+
'CLI_ROOT_DIR="${HAPPY_STACKS_CLI_ROOT_DIR:-}"',
|
|
361
|
+
'if [[ -z "$CLI_ROOT_DIR" && -f "$ENV_FILE" ]]; then',
|
|
362
|
+
' CLI_ROOT_DIR="$(grep -E \'^HAPPY_STACKS_CLI_ROOT_DIR=\' "$ENV_FILE" | head -n 1 | sed \'s/^HAPPY_STACKS_CLI_ROOT_DIR=//\')" || true',
|
|
363
|
+
'fi',
|
|
364
|
+
'if [[ -n "$CLI_ROOT_DIR" ]]; then',
|
|
365
|
+
' CLI_ENTRY="$CLI_ROOT_DIR/bin/happys.mjs"',
|
|
366
|
+
' if [[ -f "$CLI_ENTRY" ]]; then',
|
|
367
|
+
' exec "$NODE_BIN" "$CLI_ENTRY" "$@"',
|
|
368
|
+
' fi',
|
|
369
|
+
'fi',
|
|
166
370
|
'RUNTIME_DIR="${HAPPY_STACKS_RUNTIME_DIR:-$HOME_DIR/runtime}"',
|
|
167
371
|
'ENTRY="$RUNTIME_DIR/node_modules/happy-stacks/bin/happys.mjs"',
|
|
168
372
|
'if [[ -f "$ENTRY" ]]; then',
|
|
@@ -175,22 +379,31 @@ async function main() {
|
|
|
175
379
|
await writeExecutable(happysShimPath, shim);
|
|
176
380
|
await writeExecutable(happyShimPath, `#!/bin/bash\nset -euo pipefail\nexec \"${happysShimPath}\" happy \"$@\"\n`);
|
|
177
381
|
|
|
382
|
+
let didInstallPath = false;
|
|
178
383
|
if (argv.includes('--install-path')) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
console.log(
|
|
384
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
385
|
+
console.log('[init] sandbox mode: skipping --install-path (would modify your shell config)');
|
|
386
|
+
console.log('[init] tip: set HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1 if you really want to test PATH modifications');
|
|
182
387
|
} else {
|
|
183
|
-
|
|
388
|
+
const res = await ensurePathInstalled({ homeDir });
|
|
389
|
+
didInstallPath = true;
|
|
390
|
+
if (res.updated) {
|
|
391
|
+
console.log(`[init] added ${homeDir}/bin to PATH via ${res.path}`);
|
|
392
|
+
} else {
|
|
393
|
+
console.log(`[init] PATH already configured in ${res.path}`);
|
|
394
|
+
}
|
|
184
395
|
}
|
|
185
396
|
}
|
|
186
397
|
|
|
398
|
+
const invokedBySetup = (process.env.HAPPY_STACKS_SETUP_CHILD ?? '').trim() === '1';
|
|
399
|
+
|
|
187
400
|
console.log('[init] complete');
|
|
188
401
|
console.log(`[init] home: ${homeDir}`);
|
|
189
402
|
console.log(`[init] workspace: ${workspaceDir}`);
|
|
190
403
|
console.log(`[init] shims: ${homeDir}/bin`);
|
|
191
404
|
console.log('');
|
|
192
405
|
|
|
193
|
-
if (!argv.includes('--install-path')) {
|
|
406
|
+
if (!argv.includes('--install-path') || !didInstallPath) {
|
|
194
407
|
console.log('[init] note: to use `happys` / `happy` from any terminal, add shims to PATH:');
|
|
195
408
|
console.log(` export PATH="${homeDir}/bin:$PATH"`);
|
|
196
409
|
console.log(' (or re-run: happys init --install-path)');
|
|
@@ -202,11 +415,15 @@ async function main() {
|
|
|
202
415
|
|
|
203
416
|
const wantBootstrap = !argv.includes('--no-bootstrap');
|
|
204
417
|
const isTty = process.stdout.isTTY && process.stdin.isTTY;
|
|
205
|
-
const
|
|
418
|
+
const alreadyBootstrapped = isWorkspaceBootstrapped(workspaceDir);
|
|
419
|
+
const bootstrapExplicit = bootstrapArgs.length > 0;
|
|
420
|
+
const shouldBootstrap = wantBootstrap && (bootstrapExplicit || !alreadyBootstrapped);
|
|
206
421
|
|
|
207
422
|
if (shouldBootstrap) {
|
|
208
423
|
const nextArgs = [...bootstrapArgs];
|
|
209
|
-
|
|
424
|
+
// Only auto-enable the interactive wizard when init is driving bootstrap with no explicit args.
|
|
425
|
+
// If users pass args after `--`, we assume they know what they want and avoid injecting prompts.
|
|
426
|
+
if (!bootstrapExplicit && isTty && !nextArgs.includes('--interactive') && !nextArgs.includes('-i')) {
|
|
210
427
|
nextArgs.unshift('--interactive');
|
|
211
428
|
}
|
|
212
429
|
console.log('[init] running bootstrap...');
|
|
@@ -221,12 +438,24 @@ async function main() {
|
|
|
221
438
|
return;
|
|
222
439
|
}
|
|
223
440
|
|
|
441
|
+
if (wantBootstrap && alreadyBootstrapped && !bootstrapExplicit) {
|
|
442
|
+
console.log('[init] bootstrap: already set up; skipping');
|
|
443
|
+
console.log('[init] tip: for guided onboarding: happys setup');
|
|
444
|
+
console.log('');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// When `happys setup` drives init, avoid printing confusing “next steps”.
|
|
448
|
+
if (invokedBySetup) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
224
452
|
console.log('[init] next steps:');
|
|
225
453
|
console.log(` export PATH=\"${homeDir}/bin:$PATH\"`);
|
|
226
|
-
console.log(' happys
|
|
454
|
+
console.log(' happys setup');
|
|
227
455
|
}
|
|
228
456
|
|
|
229
457
|
main().catch((err) => {
|
|
230
458
|
console.error('[init] failed:', err);
|
|
231
459
|
process.exit(1);
|
|
232
460
|
});
|
|
461
|
+
|
package/scripts/install.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import './utils/env.mjs';
|
|
2
|
-
import { parseArgs } from './utils/args.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
3
|
import { pathExists } from './utils/fs.mjs';
|
|
4
4
|
import { run } from './utils/proc.mjs';
|
|
5
5
|
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
@@ -8,15 +8,16 @@ import { ensureCliBuilt, ensureDepsInstalled, ensureHappyCliLocalNpmLinked } fro
|
|
|
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
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
11
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
12
12
|
import { ensureEnvLocalUpdated } from './utils/env_local.mjs';
|
|
13
|
-
import { isTty, prompt, promptSelect, withRl } from './utils/wizard.mjs';
|
|
13
|
+
import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
14
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Install/setup the local stack:
|
|
17
18
|
* - ensure components exist (optionally clone if missing)
|
|
18
19
|
* - install dependencies where needed
|
|
19
|
-
* - build happy-cli (optional) and install `happy`/`happys` shims under
|
|
20
|
+
* - build happy-cli (optional) and install `happy`/`happys` shims under `<homeDir>/bin`
|
|
20
21
|
* - build the web UI bundle (so `run` can serve it)
|
|
21
22
|
* - optional macOS autostart (LaunchAgent)
|
|
22
23
|
*/
|
|
@@ -152,7 +153,9 @@ async function interactiveWizard({ rootDir, defaults }) {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
const enableAutostart = await promptSelect(rl, {
|
|
155
|
-
title:
|
|
156
|
+
title: isSandboxed()
|
|
157
|
+
? 'Enable macOS autostart (LaunchAgent)? (NOTE: sandbox mode; this is global OS state)'
|
|
158
|
+
: 'Enable macOS autostart (LaunchAgent)?',
|
|
156
159
|
options: [
|
|
157
160
|
{ label: 'no (default)', value: false },
|
|
158
161
|
{ label: 'yes', value: true },
|
|
@@ -211,6 +214,8 @@ async function main() {
|
|
|
211
214
|
const rootDir = getRootDir(import.meta.url);
|
|
212
215
|
|
|
213
216
|
const interactive = flags.has('--interactive') && isTty();
|
|
217
|
+
const allowGlobal = sandboxAllowsGlobalSideEffects();
|
|
218
|
+
const sandboxed = isSandboxed();
|
|
214
219
|
|
|
215
220
|
// Defaults for wizard.
|
|
216
221
|
const defaultRepoSource = resolveRepoSource({ flags });
|
|
@@ -220,7 +225,7 @@ async function main() {
|
|
|
220
225
|
upstreamOwner: 'slopus',
|
|
221
226
|
serverComponentName: getServerComponentName({ kv }),
|
|
222
227
|
allowClone: !flags.has('--no-clone') && ((process.env.HAPPY_LOCAL_CLONE_MISSING ?? '1') !== '0' || flags.has('--clone')),
|
|
223
|
-
enableAutostart: flags.has('--autostart') || (process.env.HAPPY_LOCAL_AUTOSTART ?? '0') === '1',
|
|
228
|
+
enableAutostart: (!sandboxed || allowGlobal) && (flags.has('--autostart') || (process.env.HAPPY_LOCAL_AUTOSTART ?? '0') === '1'),
|
|
224
229
|
buildTauri: flags.has('--tauri') && !flags.has('--no-tauri'),
|
|
225
230
|
};
|
|
226
231
|
|
|
@@ -260,7 +265,8 @@ async function main() {
|
|
|
260
265
|
const cloneMissingDefault = (process.env.HAPPY_LOCAL_CLONE_MISSING ?? '1') !== '0';
|
|
261
266
|
const allowClone =
|
|
262
267
|
wizard?.allowClone ?? (!flags.has('--no-clone') && (flags.has('--clone') || cloneMissingDefault));
|
|
263
|
-
const
|
|
268
|
+
const enableAutostartRaw = wizard?.enableAutostart ?? (flags.has('--autostart') || (process.env.HAPPY_LOCAL_AUTOSTART ?? '0') === '1');
|
|
269
|
+
const enableAutostart = sandboxed && !allowGlobal ? false : enableAutostartRaw;
|
|
264
270
|
const disableAutostart = flags.has('--no-autostart');
|
|
265
271
|
|
|
266
272
|
const serverComponentName = (wizard?.serverComponentName ?? getServerComponentName({ kv })).trim();
|
|
@@ -366,7 +372,7 @@ async function main() {
|
|
|
366
372
|
serverComponentName,
|
|
367
373
|
dirs: { serverLightDir, serverFullDir, cliDir: cliDirFinal, uiDir: uiDirFinal },
|
|
368
374
|
cloned: allowClone,
|
|
369
|
-
autostart: enableAutostart ? 'enabled' : disableAutostart ? 'disabled' : 'unchanged',
|
|
375
|
+
autostart: enableAutostart ? 'enabled' : sandboxed && enableAutostartRaw && !allowGlobal ? 'skipped (sandbox)' : disableAutostart ? 'disabled' : 'unchanged',
|
|
370
376
|
interactive: Boolean(wizard),
|
|
371
377
|
},
|
|
372
378
|
text: '[local] setup complete',
|
package/scripts/lint.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import './utils/env.mjs';
|
|
2
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
3
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
4
|
+
import { getComponentDir, getRootDir } from './utils/paths.mjs';
|
|
5
|
+
import { ensureDepsInstalled, requirePnpm } from './utils/pm.mjs';
|
|
6
|
+
import { pathExists } from './utils/fs.mjs';
|
|
7
|
+
import { run } from './utils/proc.mjs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
|
|
12
|
+
|
|
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
|
+
function pickLintScript(scripts) {
|
|
33
|
+
if (!scripts) return null;
|
|
34
|
+
const candidates = [
|
|
35
|
+
'lint',
|
|
36
|
+
'lint:ci',
|
|
37
|
+
'check',
|
|
38
|
+
'check:lint',
|
|
39
|
+
'eslint',
|
|
40
|
+
'eslint:check',
|
|
41
|
+
];
|
|
42
|
+
return candidates.find((k) => typeof scripts[k] === 'string' && scripts[k].trim()) ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main() {
|
|
46
|
+
const argv = process.argv.slice(2);
|
|
47
|
+
const { flags } = parseArgs(argv);
|
|
48
|
+
const json = wantsJson(argv, { flags });
|
|
49
|
+
|
|
50
|
+
if (wantsHelp(argv, { flags })) {
|
|
51
|
+
printResult({
|
|
52
|
+
json,
|
|
53
|
+
data: { components: DEFAULT_COMPONENTS, flags: ['--json'] },
|
|
54
|
+
text: [
|
|
55
|
+
'[lint] usage:',
|
|
56
|
+
' happys lint [component...] [--json]',
|
|
57
|
+
'',
|
|
58
|
+
'components:',
|
|
59
|
+
` ${DEFAULT_COMPONENTS.join(' | ')}`,
|
|
60
|
+
'',
|
|
61
|
+
'examples:',
|
|
62
|
+
' happys lint',
|
|
63
|
+
' happys lint happy happy-cli',
|
|
64
|
+
].join('\n'),
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const positionals = argv.filter((a) => !a.startsWith('--'));
|
|
70
|
+
const requested = positionals.length ? positionals : ['all'];
|
|
71
|
+
const wantAll = requested.includes('all');
|
|
72
|
+
const components = wantAll ? DEFAULT_COMPONENTS : requested;
|
|
73
|
+
|
|
74
|
+
const rootDir = getRootDir(import.meta.url);
|
|
75
|
+
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const component of components) {
|
|
78
|
+
if (!DEFAULT_COMPONENTS.includes(component)) {
|
|
79
|
+
results.push({ component, ok: false, skipped: false, error: `unknown component (expected one of: ${DEFAULT_COMPONENTS.join(', ')})` });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dir = getComponentDir(rootDir, component);
|
|
84
|
+
if (!(await pathExists(dir))) {
|
|
85
|
+
results.push({ component, ok: false, skipped: false, dir, error: `missing component dir: ${dir}` });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const scripts = await readScripts(dir);
|
|
90
|
+
if (!scripts) {
|
|
91
|
+
results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const script = pickLintScript(scripts);
|
|
96
|
+
if (!script) {
|
|
97
|
+
results.push({ component, ok: true, skipped: true, dir, reason: 'no lint script found in package.json' });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await ensureDepsInstalled(dir, component);
|
|
102
|
+
const pm = await detectPackageManagerCmd(dir);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.log(`[lint] ${component}: running ${pm.name} ${script}`);
|
|
107
|
+
await run(pm.cmd, pm.argsForScript(script), { cwd: dir, env: process.env });
|
|
108
|
+
results.push({ component, ok: true, skipped: false, dir, pm: pm.name, script });
|
|
109
|
+
} catch (e) {
|
|
110
|
+
results.push({ component, ok: false, skipped: false, dir, pm: pm.name, script, error: String(e?.message ?? e) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ok = results.every((r) => r.ok);
|
|
115
|
+
if (json) {
|
|
116
|
+
printResult({ json, data: { ok, results } });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines = ['[lint] results:'];
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
if (r.ok && r.skipped) {
|
|
123
|
+
lines.push(`- ↪ ${r.component}: skipped (${r.reason})`);
|
|
124
|
+
} else if (r.ok) {
|
|
125
|
+
lines.push(`- ✅ ${r.component}: ok (${r.pm} ${r.script})`);
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(`- ❌ ${r.component}: failed (${r.pm ?? 'unknown'} ${r.script ?? ''})`);
|
|
128
|
+
if (r.error) lines.push(` - ${r.error}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!ok) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('[lint] failed');
|
|
134
|
+
}
|
|
135
|
+
printResult({ json: false, text: lines.join('\n') });
|
|
136
|
+
if (!ok) {
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main().catch((err) => {
|
|
142
|
+
console.error('[lint] failed:', err);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
|
145
|
+
|