@gjsify/cli 0.25.1 → 0.26.1
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/dist/cli.gjs.mjs +171 -171
- package/lib/cli-app.js +7 -10
- package/lib/commands/affected.js +160 -17
- package/lib/commands/check.js +25 -8
- package/lib/commands/create.js +3 -1
- package/lib/commands/debug.js +3 -1
- package/lib/commands/flatpak/check.js +4 -3
- package/lib/commands/flatpak/diff.js +3 -1
- package/lib/commands/flatpak/sources.js +4 -7
- package/lib/commands/foreach.js +25 -7
- package/lib/commands/generate-installer.js +2 -4
- package/lib/commands/install.js +15 -7
- package/lib/commands/login.js +3 -1
- package/lib/commands/onboard.js +3 -6
- package/lib/commands/publish.js +8 -11
- package/lib/commands/self-update.js +4 -8
- package/lib/commands/showcase.d.ts +16 -0
- package/lib/commands/showcase.js +44 -24
- package/lib/commands/storybook.js +16 -2
- package/lib/commands/system-check.js +1 -2
- package/lib/commands/test.js +6 -2
- package/lib/commands/trust.js +12 -7
- package/lib/commands/tsc.js +3 -1
- package/lib/commands/uninstall.js +1 -2
- package/lib/commands/whoami.js +6 -3
- package/lib/utils/build-cache.js +7 -2
- package/lib/utils/dir-link.d.ts +58 -0
- package/lib/utils/dir-link.js +126 -0
- package/lib/utils/dlx-cache.d.ts +30 -3
- package/lib/utils/dlx-cache.js +63 -10
- package/lib/utils/install-backend-native.js +1 -1
- package/lib/utils/install-lock.js +5 -7
- package/lib/utils/install-packument-cache.d.ts +23 -0
- package/lib/utils/install-packument-cache.js +45 -10
- package/lib/utils/install-tarball-cache.js +5 -8
- package/lib/utils/oxc-resolve.js +1 -1
- package/lib/utils/prompt.js +6 -1
- package/lib/utils/runtimes.d.ts +18 -0
- package/lib/utils/runtimes.js +20 -0
- package/lib/utils/stdin.js +6 -7
- package/package.json +17 -17
package/lib/cli-app.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// build`, and `gjsify run build:types → gjsify tsc → gjs tsc.gjs.mjs`), so on
|
|
11
11
|
// CI's few cores the multiplier oversubscribes and the build thrashes. Letting
|
|
12
12
|
// `gjsify run` dispatch the inner `gjsify <cmd>` in-process collapses two gjs
|
|
13
|
-
// into one. See
|
|
13
|
+
// into one. See status/open-todos.md.
|
|
14
14
|
//
|
|
15
15
|
// This module MUST NOT execute anything at load time (no top-level
|
|
16
16
|
// `parseAsync`) — `run.ts` imports it, and a top-level run would re-dispatch
|
|
@@ -28,15 +28,12 @@ import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
|
28
28
|
// GJS's @gjsify/process shim — fake `process.versions.node` for npm
|
|
29
29
|
// compatibility, so a plain Node probe is a false positive on either.
|
|
30
30
|
function runtimeLabel() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
/* not GJS */
|
|
31
|
+
// Off GJS `gjsSystemVersion()` RETURNS undefined (an optional-chained
|
|
32
|
+
// `globalThis.imports` read, same probe as `isGjs()`) — it never throws.
|
|
33
|
+
const sysVersion = gjsSystemVersion();
|
|
34
|
+
if (sysVersion !== undefined) {
|
|
35
|
+
const v = Number(sysVersion);
|
|
36
|
+
return `GJS ${Math.floor(v / 10000)}.${Math.floor((v % 10000) / 100)}.${v % 100} (SpiderMonkey)`;
|
|
40
37
|
}
|
|
41
38
|
if (isBun()) {
|
|
42
39
|
const version = globalThis.Bun?.version ?? process.versions.bun;
|
package/lib/commands/affected.js
CHANGED
|
@@ -7,15 +7,30 @@
|
|
|
7
7
|
// up running the touched workspace + its downstream consumers instead of
|
|
8
8
|
// the whole monorepo.
|
|
9
9
|
//
|
|
10
|
+
// EVERY VERDICT THIS FILE EMITS IS SCOPED TO ONE WORKFLOW: `main.yml`. That
|
|
11
|
+
// scoping is the single most misread thing here, so it is stated before the
|
|
12
|
+
// tables rather than after. The classifier is booted only by `main.yml`'s
|
|
13
|
+
// `changes` job, and `main.yml` is the only workflow whose jobs consume its
|
|
14
|
+
// outputs. So "ignored" NEVER means "irrelevant to the repository" — it means
|
|
15
|
+
// "cannot change what `main.yml` builds or tests". Sibling workflows
|
|
16
|
+
// (`prebuilds.yml`, `audit-runtimes.yml`, `node-gi.yml`, `napi.yml`,
|
|
17
|
+
// `deploy-docs.yml`, …) gate themselves with their OWN `paths:` filters and are
|
|
18
|
+
// unaffected by anything decided here. `IGNORE` has always worked this way —
|
|
19
|
+
// `packages/node-gi/**` and `packages/napi/**` are load-bearing build inputs to
|
|
20
|
+
// their own workflows and ignored here — but the table read as a global claim,
|
|
21
|
+
// which is how a directory that is genuinely irrelevant to `main.yml` still
|
|
22
|
+
// looks like something that must not be ignored.
|
|
23
|
+
//
|
|
10
24
|
// Classifier table (first-match wins) handles the cases that aren't a
|
|
11
25
|
// straightforward "file lives in workspace X":
|
|
12
26
|
//
|
|
13
|
-
// 1. IGNORE —
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
27
|
+
// 1. IGNORE — cannot change what `main.yml` builds or tests: pure-docs /
|
|
28
|
+
// *.md / website / refs/ submodule / other workflows' files and their
|
|
29
|
+
// inputs / LICENSE / .gitignore. Discarded FIRST, before every other
|
|
30
|
+
// rule: an ignored file is never build-relevant to this workflow, not
|
|
31
|
+
// even inside a GLOBAL_TRIGGERS directory (e.g.
|
|
32
|
+
// `packages/infra/cli/README.md` must not force a full run just because
|
|
33
|
+
// `packages/infra/cli/` is a global trigger). Ignore wins over global.
|
|
19
34
|
//
|
|
20
35
|
// 2. GLOBAL_TRIGGERS — a non-ignored change touching infra the classifier
|
|
21
36
|
// itself depends on (`@gjsify/workspace`, `@gjsify/cli`, the bundler
|
|
@@ -24,12 +39,17 @@
|
|
|
24
39
|
// can't trust the closure when the algorithm that computes it just
|
|
25
40
|
// changed.
|
|
26
41
|
//
|
|
27
|
-
// 3.
|
|
42
|
+
// 3. SCRIPT_COUPLINGS — a build input that reaches its consumer through a
|
|
43
|
+
// BUILD SCRIPT instead of a dependency, so the closure walk has no edge
|
|
44
|
+
// to follow. Contributes EXTRA seeds (and may force a test tier). See
|
|
45
|
+
// the table for why this cannot be inferred and must be declared.
|
|
46
|
+
//
|
|
47
|
+
// 4. TEST_ONLY — every changed file under ONE workspace is a spec
|
|
28
48
|
// file, e2e fixture, or integration test. Seed = that workspace
|
|
29
49
|
// but SKIP the closure expansion (downstream consumers don't care
|
|
30
50
|
// about test code changes).
|
|
31
51
|
//
|
|
32
|
-
//
|
|
52
|
+
// 5. CODE (default) — `workspacesForChangedFiles` maps file → ws,
|
|
33
53
|
// then `affectedClosure` walks reverse-dep edges.
|
|
34
54
|
//
|
|
35
55
|
// Integration tests are gated separately: they run when any workspace
|
|
@@ -162,13 +182,55 @@ const GLOBAL_TRIGGERS = [
|
|
|
162
182
|
/^\.github\/workflows\/main\.yml$/,
|
|
163
183
|
/^scripts\/audit-runtimes\.mjs$/,
|
|
164
184
|
];
|
|
185
|
+
/**
|
|
186
|
+
* Inputs OWNED BY ANOTHER WORKFLOW — build-relevant, just not to `main.yml`.
|
|
187
|
+
*
|
|
188
|
+
* This is the sub-table where the workflow scoping stated at the top of the
|
|
189
|
+
* file is load-bearing, so each entry NAMES the workflow that does gate it.
|
|
190
|
+
* The test for membership is not "is this file unimportant" (none of these
|
|
191
|
+
* are) but "can `main.yml` build or test differently because of it" — and for
|
|
192
|
+
* every entry the answer is no, while the owning workflow has the path in its
|
|
193
|
+
* own `paths:` filter and would go red on a break.
|
|
194
|
+
*
|
|
195
|
+
* Adding a regex here is therefore a claim with two halves, and BOTH must
|
|
196
|
+
* hold: `main.yml` must not read the path, and some other workflow must.
|
|
197
|
+
*/
|
|
198
|
+
const OTHER_WORKFLOW_INPUTS = [
|
|
199
|
+
// Each sibling workflow's own definition. `main.yml` is deliberately
|
|
200
|
+
// absent — it is a GLOBAL_TRIGGER, since a job-shape change there is
|
|
201
|
+
// invisible until the workflow re-runs.
|
|
202
|
+
/^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image)\.yml$/,
|
|
203
|
+
// `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and
|
|
204
|
+
// the `changed-packages` classifier that decides which native packages a
|
|
205
|
+
// prebuild run rebuilds. `prebuilds.yml` lists `.github/prebuild-toolchain/**`
|
|
206
|
+
// in BOTH its `push` and `pull_request` `paths:` filters, so a break here
|
|
207
|
+
// reds that workflow on the same PR. `main.yml` never executes any of it —
|
|
208
|
+
// it builds no prebuild and runs no emulated leg.
|
|
209
|
+
/^\.github\/prebuild-toolchain\//,
|
|
210
|
+
// The REPO-SCOPED manifest-conformance rules (#847) — `tier`, `refs-pin`,
|
|
211
|
+
// `platforms-ci` plus the `unchecked-fields` ledger. Their gate is
|
|
212
|
+
// `audit-runtimes.yml`, which runs `node scripts/audit-runtimes.mjs
|
|
213
|
+
// --check --strict` on EVERY pull_request with no `paths:` filter at all,
|
|
214
|
+
// so this content is verified on every PR regardless of what is ignored
|
|
215
|
+
// here; `prebuilds.yml` additionally declares it a shared input (#843) and
|
|
216
|
+
// rebuilds every native package when it changes.
|
|
217
|
+
//
|
|
218
|
+
// The distinction that makes this safe is a real one, not a technicality:
|
|
219
|
+
// `main.yml` DOES run manifest-conformance code, but it reaches it through
|
|
220
|
+
// `scripts/verify-package-outputs.mjs` importing
|
|
221
|
+
// `packages/infra/manifest-conformance/lib/**` — the PACKAGE, a normal
|
|
222
|
+
// workspace under the `packages/infra/*` glob that maps and seeds like any
|
|
223
|
+
// other. Only the `scripts/`-side repo-scoped half is ignored, and that
|
|
224
|
+
// half is precisely the half `main.yml` does not load.
|
|
225
|
+
/^scripts\/manifest-conformance\//,
|
|
226
|
+
];
|
|
165
227
|
/** Patterns that contribute no seed and don't force a full run. */
|
|
166
228
|
const IGNORE = [
|
|
167
229
|
/\.md$/i,
|
|
168
230
|
/^refs\//,
|
|
169
231
|
/^website\//,
|
|
170
232
|
/^docs\//,
|
|
171
|
-
|
|
233
|
+
...OTHER_WORKFLOW_INPUTS,
|
|
172
234
|
/^\.githooks\//,
|
|
173
235
|
// @gjsify/node-gi (the Node-native GObject-Introspection engine, Axis 5) is
|
|
174
236
|
// NOT a gjsify workspace member — the GJS-first install/foreach tooling can't
|
|
@@ -200,6 +262,31 @@ const IGNORE = [
|
|
|
200
262
|
];
|
|
201
263
|
/** Patterns that suggest a test-only change. */
|
|
202
264
|
const TEST_PATHS = [/\.spec\.[mc]?[tj]sx?$/, /^tests\/(e2e|integration)\//];
|
|
265
|
+
const SCRIPT_COUPLINGS = [
|
|
266
|
+
{
|
|
267
|
+
// `templates/*` ARE workspaces (root `package.json#workspaces` carries
|
|
268
|
+
// a `templates/*` glob), so a template change already seeds its own
|
|
269
|
+
// `@gjsify/template-<name>`. That is not the consumer that matters.
|
|
270
|
+
//
|
|
271
|
+
// The consumer is `@gjsify/create-app`, and it is unreachable twice
|
|
272
|
+
// over: it lives at `packages/infra/create-gjsify/`, so no
|
|
273
|
+
// directory-name search finds it, and it takes the templates in
|
|
274
|
+
// through `node scripts/process-template.mjs` in its `build` script
|
|
275
|
+
// (which resolves each template's `workspace:^` specifiers and copies
|
|
276
|
+
// the result into `dist-templates/`), so there is no dependency edge
|
|
277
|
+
// either. `dist-templates/` is a build-output cache candidate, so once
|
|
278
|
+
// it is stale it stays stale.
|
|
279
|
+
match: /^templates\//,
|
|
280
|
+
seeds: ['@gjsify/create-app'],
|
|
281
|
+
// …and `tests/e2e/create-app` is the ONLY thing that would notice: it
|
|
282
|
+
// scaffolds a project out of `dist-templates/` and builds it. Without
|
|
283
|
+
// this a templates-only PR got neither the regenerated templates nor
|
|
284
|
+
// the suite that exercises them. PR #853 passed only by accident of
|
|
285
|
+
// also touching root `package.json` and tripping a global trigger.
|
|
286
|
+
runE2E: true,
|
|
287
|
+
why: '@gjsify/create-app build → node scripts/process-template.mjs reads templates/',
|
|
288
|
+
},
|
|
289
|
+
];
|
|
203
290
|
function classifyAndExpand(workspaces, changedFiles) {
|
|
204
291
|
const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
|
|
205
292
|
if (files.length === 0) {
|
|
@@ -250,23 +337,64 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
250
337
|
}
|
|
251
338
|
// Map files → workspaces. Files outside any workspace stay in `unmatched`.
|
|
252
339
|
const { matched, unmatched } = workspacesForChangedFiles(workspaces, remaining);
|
|
340
|
+
// SCRIPT_COUPLINGS: add the seeds no graph edge can reach. Applied BEFORE
|
|
341
|
+
// the `unmatched` bail-out so the table works for a coupled directory that
|
|
342
|
+
// is NOT itself a workspace — such a file would otherwise force a full run
|
|
343
|
+
// and the declared seeds would never be consulted.
|
|
344
|
+
const knownNames = new Set(workspaces.map((w) => w.name));
|
|
345
|
+
const couplingSeeds = new Set();
|
|
346
|
+
const couplingAccounted = new Set();
|
|
347
|
+
let couplingRunE2E = false;
|
|
348
|
+
for (const c of SCRIPT_COUPLINGS) {
|
|
349
|
+
const hits = remaining.filter((f) => c.match.test(f));
|
|
350
|
+
if (hits.length === 0)
|
|
351
|
+
continue;
|
|
352
|
+
for (const h of hits)
|
|
353
|
+
couplingAccounted.add(h);
|
|
354
|
+
for (const s of c.seeds) {
|
|
355
|
+
// A declared seed that is not a workspace means the table has
|
|
356
|
+
// drifted from the tree (package renamed, moved, removed). Fail
|
|
357
|
+
// towards the full run and SAY SO — the whole point of this table
|
|
358
|
+
// is that a missing rebuild here is otherwise invisible, so it must
|
|
359
|
+
// never degrade quietly back into the bug it exists to prevent.
|
|
360
|
+
if (!knownNames.has(s)) {
|
|
361
|
+
return {
|
|
362
|
+
global: true,
|
|
363
|
+
reason: `script-coupling seed ${s} is not a workspace (${c.match.source} → ${c.why}); ` +
|
|
364
|
+
`SCRIPT_COUPLINGS in commands/affected.ts is stale`,
|
|
365
|
+
workspaces: workspaces.map((w) => w.name),
|
|
366
|
+
runE2E: true,
|
|
367
|
+
runIntegration: true,
|
|
368
|
+
skipAll: false,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
couplingSeeds.add(s);
|
|
372
|
+
}
|
|
373
|
+
if (c.runE2E)
|
|
374
|
+
couplingRunE2E = true;
|
|
375
|
+
}
|
|
253
376
|
// Unmatched-but-not-ignored files are suspicious enough to fall back to
|
|
254
377
|
// the conservative "full run" path. Examples: a new top-level dotfile,
|
|
255
378
|
// a script in `scripts/` we haven't carved out, a refs/-adjacent file.
|
|
256
|
-
|
|
379
|
+
// A file a coupling claimed is accounted for and does not count here.
|
|
380
|
+
const stillUnmatched = unmatched.filter((f) => !couplingAccounted.has(f));
|
|
381
|
+
if (stillUnmatched.length > 0) {
|
|
257
382
|
return {
|
|
258
383
|
global: true,
|
|
259
|
-
reason: `unmatched files (${
|
|
384
|
+
reason: `unmatched files (${stillUnmatched.length}): ${stillUnmatched.slice(0, 3).join(', ')}${stillUnmatched.length > 3 ? '…' : ''}`,
|
|
260
385
|
workspaces: workspaces.map((w) => w.name),
|
|
261
386
|
runE2E: true,
|
|
262
387
|
runIntegration: true,
|
|
263
388
|
skipAll: false,
|
|
264
389
|
};
|
|
265
390
|
}
|
|
391
|
+
for (const s of couplingSeeds)
|
|
392
|
+
matched.add(s);
|
|
266
393
|
// TEST_ONLY shortcut: every remaining file is a spec / e2e / integration
|
|
267
394
|
// path, all under ONE workspace. Skip the closure expansion — test code
|
|
268
|
-
// has no downstream consumers.
|
|
269
|
-
|
|
395
|
+
// has no downstream consumers. A coupling deliberately disables it: the
|
|
396
|
+
// extra seeds exist precisely because a downstream consumer DOES care.
|
|
397
|
+
const testOnly = couplingSeeds.size === 0 && remaining.every((f) => TEST_PATHS.some((re) => re.test(f)));
|
|
270
398
|
if (testOnly && matched.size === 1) {
|
|
271
399
|
const only = [...matched][0];
|
|
272
400
|
// E2E or integration test-only changes still need their own job.
|
|
@@ -317,10 +445,13 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
317
445
|
}
|
|
318
446
|
}
|
|
319
447
|
}
|
|
320
|
-
|
|
448
|
+
// A `tests/e2e/**` touch turns the tier on, and so does any coupling that
|
|
449
|
+
// declares e2e as its only real coverage (see SCRIPT_COUPLINGS).
|
|
450
|
+
const runE2E = remaining.some((f) => f.startsWith('tests/e2e/')) || couplingRunE2E;
|
|
321
451
|
return {
|
|
322
452
|
global: false,
|
|
323
|
-
reason: `closure (${closure.size} ws from ${matched.size} seed(s))
|
|
453
|
+
reason: `closure (${closure.size} ws from ${matched.size} seed(s))` +
|
|
454
|
+
(couplingSeeds.size > 0 ? `, ${couplingSeeds.size} via script-coupling` : ''),
|
|
324
455
|
workspaces: [...closure].sort(),
|
|
325
456
|
runE2E,
|
|
326
457
|
runIntegration,
|
|
@@ -343,7 +474,16 @@ function runGitDiff(cwd, base, head) {
|
|
|
343
474
|
// Surface a clear error — caller (CI) will fall back to full run
|
|
344
475
|
// via `continue-on-error: true` on the classify step.
|
|
345
476
|
process.stderr.write(`gjsify affected: git diff failed (${r.status}): ${r.stderr.trim()}\n`);
|
|
477
|
+
// Under GJS `process.exit()` is DEFERRED (no atexit — the call
|
|
478
|
+
// returns), so in this synchronous, value-returning helper a bare
|
|
479
|
+
// exit fell through to the `return` below and handed the caller a
|
|
480
|
+
// diff parsed from a failed git run. `return process.exit(…)` cannot
|
|
481
|
+
// help here either — it would hand the caller the pending exit
|
|
482
|
+
// promise as a `string[]`. The throw is what actually stops the
|
|
483
|
+
// caller; on Node the exit(2) halts first and the throw is dead.
|
|
484
|
+
// oxlint-disable-next-line gjsify/deferred-process-exit -- the throw below IS the halt for the GJS path; see the comment above.
|
|
346
485
|
process.exit(2);
|
|
486
|
+
throw new Error(`gjsify affected: git diff failed (${r.status})`);
|
|
347
487
|
}
|
|
348
488
|
return r.stdout.split('\n').filter(Boolean);
|
|
349
489
|
}
|
|
@@ -409,8 +549,11 @@ function assertShellSafeWorkspaceName(name) {
|
|
|
409
549
|
` \`include-args\` token (the value is consumed by an UNQUOTED $INCLUDE_ARGS expansion — see the\n` +
|
|
410
550
|
` contract at the top of commands/affected.ts). Rename the workspace, or teach this command a\n` +
|
|
411
551
|
` transport the consumers can unquote. Refusing to emit a filter that would silently mis-match.\n`);
|
|
552
|
+
// NOT unreachable under GJS: `process.exit()` is DEFERRED there (no atexit
|
|
553
|
+
// — the call returns), so the throw below is what actually stops an unsafe
|
|
554
|
+
// name from reaching the emit site; the exit code rides the scheduled
|
|
555
|
+
// teardown. On Node the exit(2) halts first and the throw is dead code.
|
|
556
|
+
// oxlint-disable-next-line gjsify/deferred-process-exit -- the throw below IS the halt for the GJS path; see the comment above.
|
|
412
557
|
process.exit(2);
|
|
413
|
-
// Unreachable — `process.exit` terminates. Present so this function has no
|
|
414
|
-
// fall-through path that could ever return an unsafe name.
|
|
415
558
|
throw new Error(`gjsify affected: unsafe workspace name ${name}`);
|
|
416
559
|
}
|
package/lib/commands/check.js
CHANGED
|
@@ -112,7 +112,10 @@ export const checkCommand = {
|
|
|
112
112
|
console.log(`[check] cwd=${cwd} → npm run check`);
|
|
113
113
|
}
|
|
114
114
|
const r = spawnSync('npm', ['run', 'check'], { cwd, stdio: 'inherit' });
|
|
115
|
-
process.exit(
|
|
115
|
+
// `return` — a bare `process.exit()` is deferred under GJS (no
|
|
116
|
+
// atexit), so execution fell through into workspace mode and
|
|
117
|
+
// re-checked the whole monorepo after the single-package run.
|
|
118
|
+
return process.exit(r.status ?? 1);
|
|
116
119
|
}
|
|
117
120
|
// Fall through to workspace mode when the local package has no
|
|
118
121
|
// check script (cd'd into a non-package dir under the root).
|
|
@@ -120,7 +123,9 @@ export const checkCommand = {
|
|
|
120
123
|
// ---- Workspace mode ----
|
|
121
124
|
if (!workspaceRoot) {
|
|
122
125
|
console.error('gjsify check: no workspace root found from cwd. Run inside a workspace (or a package within one) with `npm run check` defined.');
|
|
123
|
-
|
|
126
|
+
// `return` — the deferred GJS exit otherwise ran discoverWorkspaces
|
|
127
|
+
// on a null root.
|
|
128
|
+
return process.exit(1);
|
|
124
129
|
}
|
|
125
130
|
const allWorkspaces = discoverWorkspaces(workspaceRoot);
|
|
126
131
|
// Always exclude @girs/* (type-only packages, no own tsc check).
|
|
@@ -137,7 +142,7 @@ export const checkCommand = {
|
|
|
137
142
|
});
|
|
138
143
|
if (targets.length === 0) {
|
|
139
144
|
console.error('gjsify check: no workspaces with a `check` script found.');
|
|
140
|
-
process.exit(1);
|
|
145
|
+
return process.exit(1);
|
|
141
146
|
}
|
|
142
147
|
if (args.verbose) {
|
|
143
148
|
console.log(`[check] root=${workspaceRoot} workspaces=${targets.length} parallel=${args.parallel ? 'yes' : 'no'}`);
|
|
@@ -145,16 +150,26 @@ export const checkCommand = {
|
|
|
145
150
|
// ---- Sequential mode ----
|
|
146
151
|
if (!args.parallel) {
|
|
147
152
|
let firstFail = 0;
|
|
153
|
+
let failCount = 0;
|
|
148
154
|
for (const ws of targets) {
|
|
149
155
|
if (args.verbose)
|
|
150
156
|
console.log(`[check] → ${ws.name}`);
|
|
151
157
|
const code = await runCheck(ws, null);
|
|
152
|
-
if (code !== 0
|
|
153
|
-
|
|
158
|
+
if (code !== 0) {
|
|
159
|
+
failCount++;
|
|
160
|
+
if (firstFail === 0)
|
|
161
|
+
firstFail = code;
|
|
162
|
+
}
|
|
154
163
|
}
|
|
164
|
+
// Count failures from the loop above — the previous
|
|
165
|
+
// `targets.filter(async …)` spelling RE-SPAWNED every workspace's
|
|
166
|
+
// check (fire-and-forget) just to print a count, and its async
|
|
167
|
+
// predicate is always truthy, so it counted ALL targets.
|
|
155
168
|
if (firstFail !== 0)
|
|
156
|
-
console.error(`gjsify check: failures in ${
|
|
157
|
-
|
|
169
|
+
console.error(`gjsify check: failures in ${failCount} workspace(s)`);
|
|
170
|
+
// `return` — a bare exit is deferred under GJS and fell through
|
|
171
|
+
// into parallel mode, re-running every check.
|
|
172
|
+
return process.exit(firstFail);
|
|
158
173
|
}
|
|
159
174
|
// ---- Parallel mode (default) ----
|
|
160
175
|
const concurrency = args.jobs ?? Math.max(1, cpus().length);
|
|
@@ -179,7 +194,9 @@ export const checkCommand = {
|
|
|
179
194
|
console.error(`\ngjsify check: ${failures.length} of ${targets.length} workspace(s) failed:`);
|
|
180
195
|
for (const f of failures)
|
|
181
196
|
console.error(` ✗ ${f.name} (exit ${f.code})`);
|
|
182
|
-
|
|
197
|
+
// `return` — the deferred GJS exit otherwise fell through to the
|
|
198
|
+
// success `process.exit(0)` below, clobbering the failure code.
|
|
199
|
+
return process.exit(1);
|
|
183
200
|
}
|
|
184
201
|
if (args.verbose)
|
|
185
202
|
console.log(`\ngjsify check: ${targets.length} workspace(s) green.`);
|
package/lib/commands/create.js
CHANGED
|
@@ -37,7 +37,9 @@ export const createCommand = {
|
|
|
37
37
|
if (!process.stdin.isTTY) {
|
|
38
38
|
const list = templates.map((t) => t.name).join(', ');
|
|
39
39
|
console.error(`Error: --template is required in non-interactive mode. Available templates: ${list || '(none)'}`);
|
|
40
|
-
|
|
40
|
+
// `return` — the deferred GJS exit otherwise fell through into
|
|
41
|
+
// the interactive template prompt on a non-TTY.
|
|
42
|
+
return process.exit(1);
|
|
41
43
|
}
|
|
42
44
|
const picked = await promptTemplate(templates);
|
|
43
45
|
template = picked.name;
|
package/lib/commands/debug.js
CHANGED
|
@@ -122,7 +122,9 @@ export const debugCommand = {
|
|
|
122
122
|
: undefined);
|
|
123
123
|
if (!busNameBase) {
|
|
124
124
|
console.error('gjsify debug: no bus name — set package.json#gjsify.devtools.busNameBase or pass --bus-name <org.example.App>');
|
|
125
|
-
process.exit(
|
|
125
|
+
// `return` — a bare `process.exit()` is deferred under GJS and the
|
|
126
|
+
// handler would continue without a bus name.
|
|
127
|
+
return process.exit(1);
|
|
126
128
|
}
|
|
127
129
|
const globals = args.globals ?? config.globals ?? 'auto';
|
|
128
130
|
const name = pkg.name ? `${pkg.name}-devtools` : 'gjsify-devtools';
|
|
@@ -73,8 +73,7 @@ export const flatpakCheckCommand = {
|
|
|
73
73
|
if (args.builderLint !== false) {
|
|
74
74
|
if (!existsSync(manifestPath)) {
|
|
75
75
|
console.error(`[gjsify flatpak check] manifest not found: ${manifestPath}`);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
return;
|
|
76
|
+
return process.exit(1);
|
|
78
77
|
}
|
|
79
78
|
const ok = await runLinter('flatpak-builder-lint', ['manifest', manifestPath], args.verbose ?? false);
|
|
80
79
|
if (!ok)
|
|
@@ -88,7 +87,9 @@ export const flatpakCheckCommand = {
|
|
|
88
87
|
}
|
|
89
88
|
if (failures > 0) {
|
|
90
89
|
console.error(`\n[gjsify flatpak check] ${failures} check(s) failed.`);
|
|
91
|
-
|
|
90
|
+
// `return` — the deferred GJS exit otherwise fell through and ALSO
|
|
91
|
+
// printed "all checks passed" after reporting failures.
|
|
92
|
+
return process.exit(1);
|
|
92
93
|
}
|
|
93
94
|
console.log('[gjsify flatpak check] all checks passed.');
|
|
94
95
|
},
|
|
@@ -87,7 +87,9 @@ export const flatpakDiffCommand = {
|
|
|
87
87
|
}
|
|
88
88
|
if (!remoteTag) {
|
|
89
89
|
console.warn('[gjsify flatpak diff] flathub manifest has no `tag` field on the inspected source.');
|
|
90
|
-
|
|
90
|
+
// `return` — the deferred GJS exit otherwise compared an undefined
|
|
91
|
+
// tag below and could report "in sync" on a broken manifest.
|
|
92
|
+
return process.exit(1);
|
|
91
93
|
}
|
|
92
94
|
if (remoteTag === localVersion) {
|
|
93
95
|
console.log(`✅ in sync (${localVersion})`);
|
|
@@ -31,13 +31,10 @@ function parseSri(integrity) {
|
|
|
31
31
|
return null;
|
|
32
32
|
const algorithm = integrity.slice(0, dash);
|
|
33
33
|
const b64 = integrity.slice(dash + 1).replace(/=+$/, '');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
34
|
+
// Base64 decoding is lenient on every runtime (Node skips invalid chars,
|
|
35
|
+
// @gjsify/buffer's table decode maps them to 0): malformed input yields
|
|
36
|
+
// short/garbage output — caught by the length check below — never a throw.
|
|
37
|
+
const hex = Buffer.from(b64, 'base64').toString('hex');
|
|
41
38
|
if (hex.length < 4)
|
|
42
39
|
return null;
|
|
43
40
|
return { algorithm, hex };
|
package/lib/commands/foreach.js
CHANGED
|
@@ -141,7 +141,9 @@ function signalTree(child, descendants, signal) {
|
|
|
141
141
|
child.kill(signal);
|
|
142
142
|
}
|
|
143
143
|
catch {
|
|
144
|
-
// already
|
|
144
|
+
// NOT the already-dead case — `ChildProcess.kill()` swallows ESRCH
|
|
145
|
+
// itself and returns false. This guards Node's residual throw paths
|
|
146
|
+
// (EPERM surfacing via an unhandled 'error' emit, unknown signal).
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
function killActiveChildren() {
|
|
@@ -292,7 +294,9 @@ export const foreachCommand = {
|
|
|
292
294
|
}
|
|
293
295
|
if (!cmd) {
|
|
294
296
|
console.error('gjsify foreach --exec: missing command. Pass it after `--`, e.g. `gjsify foreach --exec -- npm publish --tag latest`.');
|
|
295
|
-
process.exit(
|
|
297
|
+
// `return` — a bare `process.exit()` is deferred under GJS and
|
|
298
|
+
// the handler would keep running without a command.
|
|
299
|
+
return process.exit(1);
|
|
296
300
|
}
|
|
297
301
|
}
|
|
298
302
|
assertEveryIncludeMatches(allWorkspaces, args.include);
|
|
@@ -309,7 +313,8 @@ export const foreachCommand = {
|
|
|
309
313
|
if (!exec) {
|
|
310
314
|
if (!cmd) {
|
|
311
315
|
console.error('gjsify foreach: missing <script> positional. Pass --exec to run an arbitrary command instead.');
|
|
312
|
-
|
|
316
|
+
// `return` — see the deferred-exit note on the --exec guard.
|
|
317
|
+
return process.exit(1);
|
|
313
318
|
}
|
|
314
319
|
const scriptName = cmd;
|
|
315
320
|
selected = selected.filter((ws) => {
|
|
@@ -330,13 +335,16 @@ export const foreachCommand = {
|
|
|
330
335
|
const m = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(args.shard);
|
|
331
336
|
if (!m) {
|
|
332
337
|
console.error(`gjsify foreach: invalid --shard "${args.shard}" — expected "<index>/<total>" (1-based), e.g. 2/4.`);
|
|
333
|
-
|
|
338
|
+
// `return` — the deferred GJS exit otherwise ran the whole
|
|
339
|
+
// unsharded set.
|
|
340
|
+
return process.exit(1);
|
|
334
341
|
}
|
|
335
342
|
const index = Number(m[1]);
|
|
336
343
|
const total = Number(m[2]);
|
|
337
344
|
if (total < 1 || index < 1 || index > total) {
|
|
338
345
|
console.error(`gjsify foreach: invalid --shard "${args.shard}" — need 1 <= index <= total and total >= 1.`);
|
|
339
|
-
|
|
346
|
+
// `return` — see the deferred-exit note on the shard guard above.
|
|
347
|
+
return process.exit(1);
|
|
340
348
|
}
|
|
341
349
|
if (total > 1) {
|
|
342
350
|
const byName = [...selected].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
@@ -376,7 +384,8 @@ export const foreachCommand = {
|
|
|
376
384
|
// default covers whole script chains (CI sets it once for the
|
|
377
385
|
// build job) where an --exec invocation is simply not cacheable.
|
|
378
386
|
console.error('gjsify foreach: --cached only applies to script mode, not --exec.');
|
|
379
|
-
|
|
387
|
+
// `return` — the deferred GJS exit otherwise ran the --exec anyway.
|
|
388
|
+
return process.exit(1);
|
|
380
389
|
}
|
|
381
390
|
const cache = cached && !exec
|
|
382
391
|
? new BuildCacheRunner({
|
|
@@ -409,7 +418,9 @@ export const foreachCommand = {
|
|
|
409
418
|
// leave orphans holding the CI step's stdio open.
|
|
410
419
|
killActiveChildren();
|
|
411
420
|
console.error(err.message);
|
|
412
|
-
|
|
421
|
+
// `return` — the deferred GJS exit otherwise fell through to the
|
|
422
|
+
// success `process.exit(0)` below, clobbering the failure code.
|
|
423
|
+
return process.exit(1);
|
|
413
424
|
}
|
|
414
425
|
// ensureMainLoop() (called inside spawn) keeps GJS alive after every
|
|
415
426
|
// child exits — without an explicit process.exit() the success path
|
|
@@ -472,7 +483,14 @@ function assertEveryIncludeMatches(allWorkspaces, include) {
|
|
|
472
483
|
}
|
|
473
484
|
lines.push(` ${allWorkspaces.length} workspace(s) discovered. Refusing to run: a filter that selects nothing would`, ` otherwise exit 0 and look exactly like a successful run over the whole set.`);
|
|
474
485
|
console.error(lines.join('\n'));
|
|
486
|
+
// Under GJS `process.exit()` is DEFERRED (no atexit — the call returns), so
|
|
487
|
+
// a bare exit here RETURNED to the handler, which kept going and could
|
|
488
|
+
// spawn children for the patterns that DID match before the scheduled exit
|
|
489
|
+
// fired. The throw is what actually stops the run on GJS; on Node the
|
|
490
|
+
// exit(1) halts first and the throw is dead code.
|
|
491
|
+
// oxlint-disable-next-line gjsify/deferred-process-exit -- the throw below IS the halt for the GJS path; see the comment above.
|
|
475
492
|
process.exit(1);
|
|
493
|
+
throw new Error('gjsify foreach: --include matched no workspace');
|
|
476
494
|
}
|
|
477
495
|
/** Drop ONE matching pair of surrounding `'` or `"` — the quoting-bug probe. */
|
|
478
496
|
function stripSurroundingQuotes(s) {
|
|
@@ -60,8 +60,7 @@ export const generateInstallerCommand = {
|
|
|
60
60
|
const outputPath = resolve(process.cwd(), args.output);
|
|
61
61
|
if (existsSync(outputPath) && !args.force) {
|
|
62
62
|
console.error(`${args.output} already exists. Re-run with --force to overwrite.`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
return;
|
|
63
|
+
return process.exit(1);
|
|
65
64
|
}
|
|
66
65
|
const pkgJsonPath = resolve(process.cwd(), 'package.json');
|
|
67
66
|
let pkgJson = null;
|
|
@@ -76,8 +75,7 @@ export const generateInstallerCommand = {
|
|
|
76
75
|
const target = args.target ?? pkgJson?.name;
|
|
77
76
|
if (!target) {
|
|
78
77
|
console.error('No target package: pass `gjsify generate-installer <pkg>` or run inside a directory with a package.json.');
|
|
79
|
-
process.exit(1);
|
|
80
|
-
return;
|
|
78
|
+
return process.exit(1);
|
|
81
79
|
}
|
|
82
80
|
const binName = args['bin-name'] ?? pickDefaultBinName(pkgJson, target);
|
|
83
81
|
const bootstrapUrl = args['bootstrap-url'] ?? DEFAULT_BOOTSTRAP_URL;
|
package/lib/commands/install.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// `node_modules/` and symlinks `workspace:*` / `workspace:^` / `workspace:~`
|
|
18
18
|
// refs to their target source. Conflicting version ranges of the same
|
|
19
19
|
// external dep from different workspaces still share ONE hoisted root copy
|
|
20
|
-
// (a real per-workspace dedup pass is Phase D.8, see
|
|
20
|
+
// (a real per-workspace dedup pass is Phase D.8, see status/open-todos.md), but since
|
|
21
21
|
// ADR 0001 step 3 the resolver surfaces every such conflict loudly —
|
|
22
22
|
// `[gjsify] warning: version conflict for <pkg> …` names both ranges, the
|
|
23
23
|
// workspaces that requested them, and the version that actually won.
|
|
@@ -133,22 +133,27 @@ export const installCommand = {
|
|
|
133
133
|
if (args.packages && args.packages.length > 0) {
|
|
134
134
|
console.error('gjsify install --immutable does not accept package arguments. ' +
|
|
135
135
|
'Remove the package names or drop --immutable.');
|
|
136
|
-
process.exit(
|
|
136
|
+
// `return` — a bare `process.exit()` is deferred under GJS and
|
|
137
|
+
// the install would proceed past a refused flag combination.
|
|
138
|
+
return process.exit(1);
|
|
137
139
|
}
|
|
138
140
|
if (args.global) {
|
|
139
141
|
console.error('gjsify install --immutable is incompatible with --global.');
|
|
140
|
-
|
|
142
|
+
// `return` — see the deferred-exit note above.
|
|
143
|
+
return process.exit(1);
|
|
141
144
|
}
|
|
142
145
|
if (args['refresh-lockfile']) {
|
|
143
146
|
console.error('gjsify install --immutable is incompatible with --refresh-lockfile ' +
|
|
144
147
|
'(--immutable forbids rewriting the lockfile). Drop one.');
|
|
145
|
-
|
|
148
|
+
// `return` — see the deferred-exit note above.
|
|
149
|
+
return process.exit(1);
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
152
|
if (args.global) {
|
|
149
153
|
if (!args.packages || args.packages.length === 0) {
|
|
150
154
|
console.error('gjsify install --global requires at least one <pkg> argument.');
|
|
151
|
-
|
|
155
|
+
// `return` — see the deferred-exit note above.
|
|
156
|
+
return process.exit(1);
|
|
152
157
|
}
|
|
153
158
|
for (const flag of ['save-dev', 'save-peer', 'save-optional']) {
|
|
154
159
|
if (args[flag]) {
|
|
@@ -241,7 +246,10 @@ export const installCommand = {
|
|
|
241
246
|
const secs = Math.round(overallTimeoutMs / 100) / 10;
|
|
242
247
|
console.error(`gjsify install: timed out after ${secs}s — likely a registry slowdown or a wedged extract.\n` +
|
|
243
248
|
`Re-run, or override with --timeout <ms> (set --timeout 0 to disable the overall budget).`);
|
|
244
|
-
|
|
249
|
+
// `return` — the deferred GJS exit otherwise fell through into
|
|
250
|
+
// the rethrow below and reported the timeout twice. The
|
|
251
|
+
// `finally` still runs on the way out and clears the timers.
|
|
252
|
+
return process.exit(1);
|
|
245
253
|
}
|
|
246
254
|
throw err;
|
|
247
255
|
}
|
|
@@ -536,7 +544,7 @@ function syncLockfileRequested(cwd, specs) {
|
|
|
536
544
|
*
|
|
537
545
|
* Hoisting strategy is intentionally minimal — per-workspace dedup +
|
|
538
546
|
* nested `node_modules/` for cross-workspace version conflicts are the
|
|
539
|
-
* Phase D.8 follow-up tracked in
|
|
547
|
+
* Phase D.8 follow-up tracked in status/open-todos.md.
|
|
540
548
|
*/
|
|
541
549
|
async function workspaceInstall(cwd, args, signal) {
|
|
542
550
|
// Hold the root-prefix lock for the WHOLE workspace flow: bin shims,
|
package/lib/commands/login.js
CHANGED
|
@@ -63,7 +63,9 @@ export const loginCommand = {
|
|
|
63
63
|
catch (err) {
|
|
64
64
|
if (err instanceof LoginError) {
|
|
65
65
|
console.error(err.message);
|
|
66
|
-
|
|
66
|
+
// `return` — the deferred GJS exit otherwise fell through into
|
|
67
|
+
// the rethrow below and reported the login error twice.
|
|
68
|
+
return process.exit(1);
|
|
67
69
|
}
|
|
68
70
|
throw err;
|
|
69
71
|
}
|