@gjsify/cli 0.25.1 → 0.26.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/dist/cli.gjs.mjs +171 -171
- package/lib/cli-app.js +7 -10
- package/lib/commands/affected.js +14 -2
- 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/dlx-cache.js +4 -5
- package/lib/utils/install-backend-native.js +1 -1
- package/lib/utils/install-lock.js +5 -7
- 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
|
@@ -343,7 +343,16 @@ function runGitDiff(cwd, base, head) {
|
|
|
343
343
|
// Surface a clear error — caller (CI) will fall back to full run
|
|
344
344
|
// via `continue-on-error: true` on the classify step.
|
|
345
345
|
process.stderr.write(`gjsify affected: git diff failed (${r.status}): ${r.stderr.trim()}\n`);
|
|
346
|
+
// Under GJS `process.exit()` is DEFERRED (no atexit — the call
|
|
347
|
+
// returns), so in this synchronous, value-returning helper a bare
|
|
348
|
+
// exit fell through to the `return` below and handed the caller a
|
|
349
|
+
// diff parsed from a failed git run. `return process.exit(…)` cannot
|
|
350
|
+
// help here either — it would hand the caller the pending exit
|
|
351
|
+
// promise as a `string[]`. The throw is what actually stops the
|
|
352
|
+
// caller; on Node the exit(2) halts first and the throw is dead.
|
|
353
|
+
// oxlint-disable-next-line gjsify/deferred-process-exit -- the throw below IS the halt for the GJS path; see the comment above.
|
|
346
354
|
process.exit(2);
|
|
355
|
+
throw new Error(`gjsify affected: git diff failed (${r.status})`);
|
|
347
356
|
}
|
|
348
357
|
return r.stdout.split('\n').filter(Boolean);
|
|
349
358
|
}
|
|
@@ -409,8 +418,11 @@ function assertShellSafeWorkspaceName(name) {
|
|
|
409
418
|
` \`include-args\` token (the value is consumed by an UNQUOTED $INCLUDE_ARGS expansion — see the\n` +
|
|
410
419
|
` contract at the top of commands/affected.ts). Rename the workspace, or teach this command a\n` +
|
|
411
420
|
` transport the consumers can unquote. Refusing to emit a filter that would silently mis-match.\n`);
|
|
421
|
+
// NOT unreachable under GJS: `process.exit()` is DEFERRED there (no atexit
|
|
422
|
+
// — the call returns), so the throw below is what actually stops an unsafe
|
|
423
|
+
// name from reaching the emit site; the exit code rides the scheduled
|
|
424
|
+
// teardown. On Node the exit(2) halts first and the throw is dead code.
|
|
425
|
+
// oxlint-disable-next-line gjsify/deferred-process-exit -- the throw below IS the halt for the GJS path; see the comment above.
|
|
412
426
|
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
427
|
throw new Error(`gjsify affected: unsafe workspace name ${name}`);
|
|
416
428
|
}
|
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
|
}
|
package/lib/commands/onboard.js
CHANGED
|
@@ -116,8 +116,7 @@ export const onboardCommand = {
|
|
|
116
116
|
process.stdout.write(`${JSON.stringify({ ok: false, error: 'auth', message: msg })}\n`);
|
|
117
117
|
else
|
|
118
118
|
console.error(msg);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
return;
|
|
119
|
+
return process.exit(1);
|
|
121
120
|
}
|
|
122
121
|
// Login may have written a fresh token — reload so the probes/ops use it.
|
|
123
122
|
npmrc = await loadNpmrc(cwd);
|
|
@@ -136,8 +135,7 @@ export const onboardCommand = {
|
|
|
136
135
|
process.stdout.write(`${JSON.stringify({ ok: false, error: 'repository', message: msg })}\n`);
|
|
137
136
|
else
|
|
138
137
|
console.error(msg);
|
|
139
|
-
process.exit(1);
|
|
140
|
-
return;
|
|
138
|
+
return process.exit(1);
|
|
141
139
|
}
|
|
142
140
|
const workflow = normalizeWorkflowFile(args.workflow);
|
|
143
141
|
const environment = args.environment;
|
|
@@ -155,8 +153,7 @@ export const onboardCommand = {
|
|
|
155
153
|
process.stdout.write(`${JSON.stringify({ ok: false, error: 'no-packages', message: msg })}\n`);
|
|
156
154
|
else
|
|
157
155
|
console.error(msg);
|
|
158
|
-
process.exit(1);
|
|
159
|
-
return;
|
|
156
|
+
return process.exit(1);
|
|
160
157
|
}
|
|
161
158
|
log(`gjsify onboard: ${selected.length} package(s) | repo=${repository} workflow=${workflow}`);
|
|
162
159
|
if (dryRun)
|
package/lib/commands/publish.js
CHANGED
|
@@ -125,7 +125,9 @@ export const publishCommand = {
|
|
|
125
125
|
const rawPkg = JSON.parse(readFileSync(rawPkgPath, 'utf-8'));
|
|
126
126
|
if (typeof rawPkg.name !== 'string') {
|
|
127
127
|
process.stderr.write(`gjsify publish --check-trusted: ${rawPkgPath} has no \`name\` field\n`);
|
|
128
|
-
process.exit(
|
|
128
|
+
// `return` — a bare `process.exit()` is deferred under GJS and
|
|
129
|
+
// the check would continue on a nameless manifest.
|
|
130
|
+
return process.exit(2);
|
|
129
131
|
}
|
|
130
132
|
if (rawPkg.private === true) {
|
|
131
133
|
const out = { ok: true, action: 'check-trusted', name: rawPkg.name, skipped: 'private' };
|
|
@@ -432,13 +434,11 @@ function reportPublishOutcome(outcome, asJson) {
|
|
|
432
434
|
}
|
|
433
435
|
case 'otp-required': {
|
|
434
436
|
console.error(`gjsify publish: npm requires a 2FA one-time code — re-run with \`--otp <code>\``);
|
|
435
|
-
process.exit(1);
|
|
436
|
-
return;
|
|
437
|
+
return process.exit(1);
|
|
437
438
|
}
|
|
438
439
|
case 'oidc-failed': {
|
|
439
440
|
handleOidcFailure(outcome.error, outcome.name, asJson);
|
|
440
|
-
process.exit(1);
|
|
441
|
-
return;
|
|
441
|
+
return process.exit(1);
|
|
442
442
|
}
|
|
443
443
|
case 'oidc-no-token': {
|
|
444
444
|
const msg = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
@@ -446,8 +446,7 @@ function reportPublishOutcome(outcome, asJson) {
|
|
|
446
446
|
if (asJson) {
|
|
447
447
|
process.stdout.write(`${JSON.stringify({ ok: false, name: outcome.name, version: outcome.version, error: 'oidc-failed-no-token' }, null, 2)}\n`);
|
|
448
448
|
}
|
|
449
|
-
process.exit(1);
|
|
450
|
-
return;
|
|
449
|
+
return process.exit(1);
|
|
451
450
|
}
|
|
452
451
|
case 'diagnostic': {
|
|
453
452
|
if (asJson) {
|
|
@@ -463,14 +462,12 @@ function reportPublishOutcome(outcome, asJson) {
|
|
|
463
462
|
else {
|
|
464
463
|
process.stderr.write(`${outcome.diag.message}\n`);
|
|
465
464
|
}
|
|
466
|
-
process.exit(1);
|
|
467
|
-
return;
|
|
465
|
+
return process.exit(1);
|
|
468
466
|
}
|
|
469
467
|
case 'error': {
|
|
470
468
|
console.error(`gjsify publish: ${outcome.name}@${outcome.version} — ${outcome.status} ${outcome.statusText}`);
|
|
471
469
|
console.error(outcome.text);
|
|
472
|
-
process.exit(1);
|
|
473
|
-
return;
|
|
470
|
+
return process.exit(1);
|
|
474
471
|
}
|
|
475
472
|
}
|
|
476
473
|
}
|
|
@@ -65,15 +65,13 @@ export const selfUpdateCommand = {
|
|
|
65
65
|
catch (err) {
|
|
66
66
|
const msg = err instanceof Error ? err.message : String(err);
|
|
67
67
|
console.error(`Failed to fetch packument: ${msg}`);
|
|
68
|
-
process.exit(1);
|
|
69
|
-
return;
|
|
68
|
+
return process.exit(1);
|
|
70
69
|
}
|
|
71
70
|
const target = resolveTag(packument, args.tag);
|
|
72
71
|
if (!target) {
|
|
73
72
|
console.error(`Unknown dist-tag '${args.tag}' on ${PACKAGE_NAME}. ` +
|
|
74
73
|
`Known tags: ${Object.keys(packument['dist-tags'] ?? {}).join(', ') || '(none)'}`);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
return;
|
|
74
|
+
return process.exit(1);
|
|
77
75
|
}
|
|
78
76
|
console.log(`Latest matching --tag ${args.tag}: v${target}`);
|
|
79
77
|
// A version match is NOT sufficient to declare "up to date": a global
|
|
@@ -103,8 +101,7 @@ export const selfUpdateCommand = {
|
|
|
103
101
|
}
|
|
104
102
|
if (args.check) {
|
|
105
103
|
console.log(currentVersion ? `Update available: v${currentVersion} → v${target}` : `Install required: → v${target}`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
return;
|
|
104
|
+
return process.exit(1);
|
|
108
105
|
}
|
|
109
106
|
// `@gjsify/cli` ships two runtimes from one package: the Node `bin`
|
|
110
107
|
// (`lib/index.js`, which imports its deps from `node_modules`) and the
|
|
@@ -141,8 +138,7 @@ export const selfUpdateCommand = {
|
|
|
141
138
|
const msg = err instanceof Error ? err.message : String(err);
|
|
142
139
|
console.error(`self-update: install failed — ${msg}`);
|
|
143
140
|
console.error(`Re-run \`gjsify self-update\` to retry.`);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
return;
|
|
141
|
+
return process.exit(1);
|
|
146
142
|
}
|
|
147
143
|
// Lay down (or repair) the GJS-native engine bridges in lockstep with
|
|
148
144
|
// the bundle — same set + best-effort handling as the global installer.
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pinned invocations for the three runners that cache an unpinned bin.
|
|
4
|
+
*
|
|
5
|
+
* The deno line carries TWO flags because deno stacks two independent refusals,
|
|
6
|
+
* and dropping either one leaves the hint broken in exactly the window where it
|
|
7
|
+
* is reached most: `--reload` busts the `DENO_DIR` copy, and `--min-dep-age 0`
|
|
8
|
+
* waives deno's supply-chain policy of refusing any version published within
|
|
9
|
+
* the last 24 h (`minimumDependencyAge`, default `24h`). Without the second
|
|
10
|
+
* flag `@latest` silently resolves to the newest release OLDER THAN A DAY — so
|
|
11
|
+
* a user who hits a bug, is told to run `@latest`, and does, gets the SAME
|
|
12
|
+
* pre-fix binary back and a stale error message with it. Measured twice on one
|
|
13
|
+
* day against 0.25.1 (minutes old) falling back to 0.24.1. `@latest` is kept
|
|
14
|
+
* rather than a baked version so the hint cannot go stale itself; the age
|
|
15
|
+
* waiver is what makes `@latest` mean latest.
|
|
16
|
+
*/
|
|
17
|
+
export declare const PIN_HINT: string;
|
|
2
18
|
interface ShowcaseOptions {
|
|
3
19
|
name?: string;
|
|
4
20
|
json: boolean;
|
package/lib/commands/showcase.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ensurePkgDir } from './dlx.js';
|
|
|
7
7
|
import { parseSpec } from '../utils/parse-spec.js';
|
|
8
8
|
import { resolveNodeEntry } from '../utils/resolve-gjs-entry.js';
|
|
9
9
|
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
10
|
-
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, defaultExampleRuntime, } from '../utils/runtimes.js';
|
|
10
|
+
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, defaultExampleRuntime, requiresGjsSystemDeps, } from '../utils/runtimes.js';
|
|
11
11
|
function readCliVersion() {
|
|
12
12
|
try {
|
|
13
13
|
const pkgUrl = new URL('../../package.json', import.meta.url);
|
|
@@ -34,11 +34,25 @@ function readCliVersion() {
|
|
|
34
34
|
function runningAs(cliVersion) {
|
|
35
35
|
return cliVersion ? `[gjsify ${cliVersion}] ` : '';
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Pinned invocations for the three runners that cache an unpinned bin.
|
|
39
|
+
*
|
|
40
|
+
* The deno line carries TWO flags because deno stacks two independent refusals,
|
|
41
|
+
* and dropping either one leaves the hint broken in exactly the window where it
|
|
42
|
+
* is reached most: `--reload` busts the `DENO_DIR` copy, and `--min-dep-age 0`
|
|
43
|
+
* waives deno's supply-chain policy of refusing any version published within
|
|
44
|
+
* the last 24 h (`minimumDependencyAge`, default `24h`). Without the second
|
|
45
|
+
* flag `@latest` silently resolves to the newest release OLDER THAN A DAY — so
|
|
46
|
+
* a user who hits a bug, is told to run `@latest`, and does, gets the SAME
|
|
47
|
+
* pre-fix binary back and a stale error message with it. Measured twice on one
|
|
48
|
+
* day against 0.25.1 (minutes old) falling back to 0.24.1. `@latest` is kept
|
|
49
|
+
* rather than a baked version so the hint cannot go stale itself; the age
|
|
50
|
+
* waiver is what makes `@latest` mean latest.
|
|
51
|
+
*/
|
|
52
|
+
export const PIN_HINT = 'A package runner reuses a CACHED copy of an unpinned bin. Force the current one:\n' +
|
|
39
53
|
' npx @gjsify/cli@latest showcase <name>\n' +
|
|
40
54
|
' bunx @gjsify/cli@latest showcase <name>\n' +
|
|
41
|
-
' deno run -A --reload npm:@gjsify/cli@latest showcase <name>';
|
|
55
|
+
' deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase <name>';
|
|
42
56
|
export const showcaseCommand = {
|
|
43
57
|
command: 'showcase [name]',
|
|
44
58
|
description: 'List or run curated gjsify showcase applications.',
|
|
@@ -119,33 +133,39 @@ export const showcaseCommand = {
|
|
|
119
133
|
// `m_should_exit` core dump. Returning halts the handler here.
|
|
120
134
|
return process.exit(1);
|
|
121
135
|
}
|
|
136
|
+
// Resolve the runtime BEFORE the system-dependency gate: that gate is a
|
|
137
|
+
// question ABOUT the runtime, and asking it first gated every showcase
|
|
138
|
+
// on a gjs binary (see `requiresGjsSystemDeps`).
|
|
139
|
+
const runtime = (args.runtime ?? defaultExampleRuntime());
|
|
140
|
+
if (!isExampleRuntime(runtime)) {
|
|
141
|
+
console.error(`Unknown --runtime "${runtime}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
|
|
142
|
+
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
143
|
+
return process.exit(1);
|
|
144
|
+
}
|
|
122
145
|
// System dependency check before delegating — only system libs (gjs,
|
|
123
|
-
// gtk4, …)
|
|
146
|
+
// gtk4, …), and only for a runtime that consumes the `--app gjs`
|
|
147
|
+
// bundle. The showcase's npm deps (incl. `@gjsify/webgl` with the
|
|
124
148
|
// gwebgl Vala prebuild) are fetched by `gjsify dlx` into the npm
|
|
125
149
|
// cache, and `runGjsBundle()` picks the prebuild up from the bundle
|
|
126
150
|
// dir via `detectNativePackages()`. Pre-flight-checking npm deps
|
|
127
151
|
// here would fail for `npx @gjsify/cli showcase` (no project
|
|
128
152
|
// node_modules, CLI doesn't dep on the showcase libs).
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
if (requiresGjsSystemDeps(runtime)) {
|
|
154
|
+
const results = runMinimalChecks();
|
|
155
|
+
const missingHard = results.filter((r) => !r.found && r.severity === 'required');
|
|
156
|
+
if (missingHard.length > 0) {
|
|
157
|
+
console.error('Missing system dependencies:\n');
|
|
158
|
+
for (const dep of missingHard) {
|
|
159
|
+
console.error(` ✗ ${dep.name}`);
|
|
160
|
+
}
|
|
161
|
+
const pm = detectPackageManager();
|
|
162
|
+
const cmd = buildInstallCommand(pm, missingHard);
|
|
163
|
+
if (cmd) {
|
|
164
|
+
console.error(`\nInstall with:\n ${cmd}`);
|
|
165
|
+
}
|
|
166
|
+
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
167
|
+
return process.exit(1);
|
|
140
168
|
}
|
|
141
|
-
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
142
|
-
return process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
const runtime = (args.runtime ?? defaultExampleRuntime());
|
|
145
|
-
if (!isExampleRuntime(runtime)) {
|
|
146
|
-
console.error(`Unknown --runtime "${runtime}" (expected: ${EXAMPLE_RUNTIMES.join(', ')}).`);
|
|
147
|
-
// `return` — bare `process.exit` falls through under GJS (see above).
|
|
148
|
-
return process.exit(1);
|
|
149
169
|
}
|
|
150
170
|
const cliVersion = readCliVersion();
|
|
151
171
|
// Non-gjs runtime: resolve the showcase's `--app node` bundle and run it
|