@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.
@@ -136,8 +136,22 @@ export const storybookCommand = {
136
136
  };
137
137
  const initialCount = writeEntry();
138
138
  if (initialCount === 0) {
139
- console.error(`No *.story.ts files found under ${relative(cwd, storiesDir) || storiesDir}`);
140
- process.exit(1);
139
+ // `return process.exit()`: a bare `process.exit()` does NOT halt
140
+ // synchronously under GJS (no atexit, the GLib loop is still
141
+ // armed), so execution fell through to the "Found 0 story file(s)"
142
+ // log and on into the build — and the deferred exit never carried
143
+ // its code, so `gjsify storybook` in a directory with no stories
144
+ // printed BOTH messages and exited 0. Measured: exit 1 under bun,
145
+ // exit 0 under gjs, same command. Same trap as `showcase.ts`.
146
+ console.error(`No *.story.ts files found under ${relative(cwd, storiesDir) || storiesDir}\n\n` +
147
+ ` \`gjsify storybook\` renders the stories of the CURRENT project: it discovers\n` +
148
+ ` \`*.story.ts\` files and launches a component browser over them. Point it at a\n` +
149
+ ` different directory with --stories <dir> (or \`gjsify.storybook.stories\` in\n` +
150
+ ` package.json) if yours live elsewhere.\n\n` +
151
+ ` To SEE a storybook without authoring one first, run the curated Libadwaita\n` +
152
+ ` component browser — it is a showcase, so it needs no project at all:\n\n` +
153
+ ` gjsify showcase adwaita-storybook`);
154
+ return process.exit(1);
141
155
  }
142
156
  console.log(`Found ${initialCount} story file(s) under ${relative(cwd, storiesDir) || '.'} (runtime: ${runtime})`);
143
157
  // In-process build — runtime-agnostic (Node uses npm rolldown, GJS uses
@@ -18,8 +18,7 @@ export const systemCheckCommand = {
18
18
  if (args.json) {
19
19
  console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
20
20
  // Only required deps influence the exit code.
21
- process.exit(missingRequired.length > 0 ? 1 : 0);
22
- return;
21
+ return process.exit(missingRequired.length > 0 ? 1 : 0);
23
22
  }
24
23
  console.log('System dependency check\n');
25
24
  const required = results.filter((r) => r.severity === 'required');
@@ -60,7 +60,9 @@ export const testCommand = {
60
60
  console.error(`[gjsify test] no test entry at ${relative(cwd, entry)} — ` +
61
61
  `add an \`src/test.mts\` that aggregates your \`@gjsify/unit\` suites, ` +
62
62
  `or set \`gjsify.test.entry\` in package.json.`);
63
- process.exit(1);
63
+ // `return` — a bare `process.exit()` is deferred under GJS and the
64
+ // handler would try to build the missing entry anyway.
65
+ return process.exit(1);
64
66
  }
65
67
  const requested = args.runtime === 'gjs'
66
68
  ? ['gjs']
@@ -124,7 +126,9 @@ export const testCommand = {
124
126
  console.log(`[gjsify test] ${summary}`);
125
127
  const anyFailed = results.some((r) => !r.ok);
126
128
  if (anyFailed) {
127
- process.exit(1);
129
+ // `return` — the deferred GJS exit otherwise fell through to the
130
+ // success `process.exit(0)` below, clobbering the failure code.
131
+ return process.exit(1);
128
132
  }
129
133
  // Explicit success exit: under GJS the spawn-armed main loop would
130
134
  // otherwise park this process after the summary. (runGjsBundle
@@ -79,7 +79,9 @@ export const trustCommand = {
79
79
  const npmrc = await loadNpmrc(cwd);
80
80
  if (!hasAnyCredential(npmrc)) {
81
81
  console.error('gjsify trust: no npm token configured. Run `gjsify login` first.');
82
- process.exit(1);
82
+ // `return` — a bare `process.exit()` is deferred under GJS and the
83
+ // handler would keep going without credentials.
84
+ return process.exit(1);
83
85
  }
84
86
  // Pre-flight: a present-but-DEAD token would otherwise surface as a
85
87
  // confusing per-package `401 "You must be logged in to publish
@@ -97,13 +99,13 @@ export const trustCommand = {
97
99
  console.error(`gjsify trust: npm auth check failed (${msg}).\n` +
98
100
  'Your ~/.npmrc token is dead or revoked — refresh it with `npm login`, then re-run.\n' +
99
101
  '(`gjsify trust` uses the ~/.npmrc bearer token directly — unlike `npm trust`, which logs in via the browser.)');
100
- process.exit(1);
101
- return;
102
+ return process.exit(1);
102
103
  }
103
104
  if (!who.username) {
104
105
  console.error('gjsify trust: your npm token appears dead or revoked (registry returned an empty whoami).\n' +
105
106
  'Refresh it with `npm login`, then re-run.');
106
- process.exit(1);
107
+ // `return` — see the deferred-exit note on the guard above.
108
+ return process.exit(1);
107
109
  }
108
110
  console.log(`gjsify trust: authenticated as ${who.username}`);
109
111
  }
@@ -117,11 +119,13 @@ export const trustCommand = {
117
119
  if (!repository) {
118
120
  console.error('gjsify trust: could not determine the GitHub repository.\n' +
119
121
  'Pass --repository owner/repo (no `origin` remote to infer from).');
120
- process.exit(1);
122
+ // `return` — see the deferred-exit note on the guard above.
123
+ return process.exit(1);
121
124
  }
122
125
  if (!validateRepository(repository)) {
123
126
  console.error(`gjsify trust: --repository must be "owner/repo", got "${repository}".`);
124
- process.exit(1);
127
+ // `return` — see the deferred-exit note on the guard above.
128
+ return process.exit(1);
125
129
  }
126
130
  const workflow = normalizeWorkflowFile(args.workflow);
127
131
  const environment = args.environment;
@@ -148,7 +152,8 @@ export const trustCommand = {
148
152
  }).filter((ws) => ws.name);
149
153
  if (selected.length === 0) {
150
154
  console.error('gjsify trust: no publishable packages matched.');
151
- process.exit(1);
155
+ // `return` — see the deferred-exit note on the guard above.
156
+ return process.exit(1);
152
157
  }
153
158
  console.log(`gjsify trust: ${selected.length} package(s) | repo=${repository} workflow=${workflow}`);
154
159
  if (dryRun)
@@ -104,7 +104,9 @@ export const tscCommand = {
104
104
  if (!bundlePath && !nodeTscPath) {
105
105
  console.error('gjsify tsc: neither @gjsify/tsc (GJS bundle) nor npm `typescript` (Node) is installed.');
106
106
  console.error(' Install with: gjsify install --save-dev @gjsify/tsc (or add `typescript`).');
107
- process.exit(1);
107
+ // `return` — a bare `process.exit()` is deferred under GJS and the
108
+ // handler would try to spawn a compiler that is not there.
109
+ return process.exit(1);
108
110
  }
109
111
  // Mirror `gjsify run`'s native-env composition so any future
110
112
  // typescript-the-bundle code path that reaches a native bridge
@@ -46,8 +46,7 @@ export const uninstallCommand = {
46
46
  if (!args.global) {
47
47
  console.error('gjsify uninstall currently only supports --global. ' +
48
48
  'For project-local removal, edit package.json + re-run `gjsify install`.');
49
- process.exit(1);
50
- return;
49
+ return process.exit(1);
51
50
  }
52
51
  const layout = defaultGlobalLayout();
53
52
  const dryRun = args['dry-run'] ?? false;
@@ -69,7 +69,9 @@ export const whoamiCommand = {
69
69
  'Then verify: gjsify whoami',
70
70
  ].join('\n') + '\n');
71
71
  }
72
- process.exit(1);
72
+ // `return` — a bare `process.exit()` is deferred under GJS and the
73
+ // handler would probe the registry without a token.
74
+ return process.exit(1);
73
75
  }
74
76
  let result;
75
77
  try {
@@ -85,8 +87,9 @@ export const whoamiCommand = {
85
87
  process.stderr.write(`gjsify whoami: ${message}\n`);
86
88
  process.stderr.write(`Registry: ${registryClean}\n`);
87
89
  }
88
- process.exit(1);
89
- return; // unreachable but pleases the type narrower
90
+ // `return process.exit` also keeps the type narrower happy: the
91
+ // never-typed call marks this branch as terminated.
92
+ return process.exit(1);
90
93
  }
91
94
  if (result.username && result.username.length > 0) {
92
95
  // Branch 1 — live token.
@@ -92,11 +92,10 @@ export function symlinkSwap(cacheDir, prepareDir) {
92
92
  catch (err) {
93
93
  const code = err.code;
94
94
  if (code === 'EBUSY' || code === 'EPERM' || code === 'EEXIST') {
95
- // Race lost — clean up our tmp and use whoever won.
96
- try {
97
- rmSync(tmpLink);
98
- }
99
- catch { }
95
+ // Race lost — clean up our tmp and use whoever won. force:true is
96
+ // the non-throwing spelling of "remove if still there"; any other
97
+ // failure on our own pid-unique link is real and should surface.
98
+ rmSync(tmpLink, { force: true });
100
99
  return realpathSync(linkPath);
101
100
  }
102
101
  throw err;
@@ -458,7 +458,7 @@ specOrigins) {
458
458
  *
459
459
  * This makes the current single-root-slot behavior honest instead of silent;
460
460
  * the real fix — a per-workspace dedup pass that gives each conflicting
461
- * requester its own nested copy — is Phase D.8 (see STATUS.md).
461
+ * requester its own nested copy — is Phase D.8 (see status/open-todos.md).
462
462
  */
463
463
  function emitTopLevelConflictWarnings(topLevelRanges, root) {
464
464
  for (const [name, ranges] of topLevelRanges) {
@@ -84,13 +84,11 @@ function lockingDisabled() {
84
84
  * we cannot prove abandoned; the mtime staleness budget still applies).
85
85
  */
86
86
  function isPidAlive(pid) {
87
- try {
88
- if (existsSync('/proc/self'))
89
- return existsSync(`/proc/${pid}`);
90
- }
91
- catch {
92
- /* fall through to the signal probe */
93
- }
87
+ // `existsSync` never throws by contract: Node returns false on any error,
88
+ // and the GJS shim is a typeof guard + Gio `query_exists` (both without a
89
+ // throw path in the GIR). Off Linux the `if` is simply false.
90
+ if (existsSync('/proc/self'))
91
+ return existsSync(`/proc/${pid}`);
94
92
  try {
95
93
  process.kill(pid, 0);
96
94
  return true;
@@ -30,6 +30,7 @@
30
30
  // same hex bytes share the same cache entry; that is the invariant pnpm
31
31
  // relies on too. Tarballs without an integrity hash (older registries)
32
32
  // fall through to a no-op cache and download every time.
33
+ import { Buffer } from 'node:buffer';
33
34
  import { existsSync, statSync } from 'node:fs';
34
35
  import { homedir } from 'node:os';
35
36
  import { join } from 'node:path';
@@ -53,14 +54,10 @@ function parseSri(integrity) {
53
54
  return null;
54
55
  const algorithm = integrity.slice(0, dashIdx);
55
56
  const b64 = integrity.slice(dashIdx + 1).replace(/=+$/, '');
56
- // Decode base64 → hex; throws on malformed input which we swallow.
57
- let hex;
58
- try {
59
- hex = Buffer.from(b64, 'base64').toString('hex');
60
- }
61
- catch {
62
- return null;
63
- }
57
+ // Base64 decoding is lenient on every runtime (Node skips invalid chars,
58
+ // @gjsify/buffer's table decode maps them to 0): malformed input yields
59
+ // short/garbage output — caught by the length check below — never a throw.
60
+ const hex = Buffer.from(b64, 'base64').toString('hex');
64
61
  if (hex.length < 4)
65
62
  return null;
66
63
  return { algorithm, hex };
@@ -45,7 +45,7 @@
45
45
  // `gjsify/register-class-order` rule wired via `.oxlintrc.json` jsPlugins)
46
46
  // lives in the Node launcher, so a native oxlint bridge could only run the
47
47
  // Rust rule subset. Lint stays Node-spawned until that trade-off is decided
48
- // (tracked in STATUS.md Open TODOs).
48
+ // (tracked in status/open-todos.md).
49
49
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
50
50
  import { join, resolve } from 'node:path';
51
51
  import { createRequire } from 'node:module';
@@ -82,7 +82,12 @@ async function runRawSession(fn) {
82
82
  out.write(r.echo);
83
83
  if (r.interrupt) {
84
84
  cleanup();
85
- process.exit(130);
85
+ // `return` — a bare `process.exit()` is deferred under GJS (no
86
+ // atexit), so the loop kept consuming the rest of the chunk
87
+ // after Ctrl-C and could still resolve the pending prompt.
88
+ // Returning stops input handling here; the deferred exit then
89
+ // carries 130 on the way out.
90
+ return process.exit(130);
86
91
  }
87
92
  if (r.done) {
88
93
  const { resolve } = pending;
@@ -26,6 +26,24 @@ export declare const RUNTIMES: Record<ExampleRuntime, RuntimeSpec>;
26
26
  export declare function isExampleRuntime(value: string): value is ExampleRuntime;
27
27
  /** The `gjsify build --app <target>` a runtime consumes (`gjs`, else `node`). */
28
28
  export declare function buildAppForRuntime(runtime: ExampleRuntime): 'gjs' | 'node';
29
+ /**
30
+ * Does running an example/showcase on `runtime` require the GJS SYSTEM
31
+ * dependencies — i.e. a `gjs` binary on PATH?
32
+ *
33
+ * DERIVED from `buildApp`, deliberately not a second table: "consumes the
34
+ * `--app gjs` bundle" and "needs a gjs interpreter" are the same fact, and a
35
+ * hand-written second list drifts the first time a runtime is added.
36
+ *
37
+ * The caller must resolve the runtime BEFORE gating on it. `gjsify showcase`
38
+ * called `runMinimalChecks()` — which marks `gjs` `required` — three statements
39
+ * ABOVE the line resolving `args.runtime`, so `--runtime node` aborted with
40
+ * "Missing system dependencies: ✗ GJS" before reaching the `runtime !== 'gjs'`
41
+ * branch that never touches gjs. Every showcase was unreachable on a host
42
+ * without gjs (Windows, plain Node/bun/deno) — including the default path,
43
+ * since `defaultExampleRuntime()` falls back to the host runtime for exactly
44
+ * those hosts.
45
+ */
46
+ export declare function requiresGjsSystemDeps(runtime: ExampleRuntime): boolean;
29
47
  /** Whether `<probe> --version` succeeds (i.e. the runtime binary is on PATH). */
30
48
  export declare function isRuntimeOnPath(probe: string): boolean;
31
49
  /**
@@ -41,6 +41,26 @@ export function isExampleRuntime(value) {
41
41
  export function buildAppForRuntime(runtime) {
42
42
  return RUNTIMES[runtime].buildApp;
43
43
  }
44
+ /**
45
+ * Does running an example/showcase on `runtime` require the GJS SYSTEM
46
+ * dependencies — i.e. a `gjs` binary on PATH?
47
+ *
48
+ * DERIVED from `buildApp`, deliberately not a second table: "consumes the
49
+ * `--app gjs` bundle" and "needs a gjs interpreter" are the same fact, and a
50
+ * hand-written second list drifts the first time a runtime is added.
51
+ *
52
+ * The caller must resolve the runtime BEFORE gating on it. `gjsify showcase`
53
+ * called `runMinimalChecks()` — which marks `gjs` `required` — three statements
54
+ * ABOVE the line resolving `args.runtime`, so `--runtime node` aborted with
55
+ * "Missing system dependencies: ✗ GJS" before reaching the `runtime !== 'gjs'`
56
+ * branch that never touches gjs. Every showcase was unreachable on a host
57
+ * without gjs (Windows, plain Node/bun/deno) — including the default path,
58
+ * since `defaultExampleRuntime()` falls back to the host runtime for exactly
59
+ * those hosts.
60
+ */
61
+ export function requiresGjsSystemDeps(runtime) {
62
+ return buildAppForRuntime(runtime) === 'gjs';
63
+ }
44
64
  /** Whether `<probe> --version` succeeds (i.e. the runtime binary is on PATH). */
45
65
  export function isRuntimeOnPath(probe) {
46
66
  try {
@@ -75,14 +75,13 @@ function readStdinTextGioFile() {
75
75
  throw new Error('Gio.File.load_contents("/dev/stdin") returned false');
76
76
  return new TextDecoder().decode(contents);
77
77
  }
78
- /** The GJS `imports.gi` namespace, or `undefined` when not running on GJS. */
78
+ /**
79
+ * The GJS `imports.gi` namespace, or `undefined` when not running on GJS.
80
+ * Same optional-chained probe as `isGjs()` — off GJS `imports` is simply
81
+ * absent and the read yields `undefined`; there is no throw path.
82
+ */
79
83
  function giNamespace() {
80
- try {
81
- return globalThis.imports?.gi;
82
- }
83
- catch {
84
- return undefined;
85
- }
84
+ return globalThis.imports?.gi;
86
85
  }
87
86
  /**
88
87
  * Read the whole of stdin as UTF-8. Works on Node and under the GJS bundle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.25.1",
3
+ "version": "0.26.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -130,19 +130,19 @@
130
130
  "cli"
131
131
  ],
132
132
  "dependencies": {
133
- "@gjsify/buffer": "^0.25.1",
134
- "@gjsify/create-app": "^0.25.1",
135
- "@gjsify/node-globals": "^0.25.1",
136
- "@gjsify/node-polyfills": "^0.25.1",
137
- "@gjsify/npm-registry": "^0.25.1",
138
- "@gjsify/resolve-npm": "^0.25.1",
139
- "@gjsify/rolldown-plugin-gjsify": "^0.25.1",
140
- "@gjsify/rolldown-plugin-pnp": "^0.25.1",
141
- "@gjsify/semver": "^0.25.1",
142
- "@gjsify/tar": "^0.25.1",
143
- "@gjsify/tsc": "^0.25.1",
144
- "@gjsify/web-polyfills": "^0.25.1",
145
- "@gjsify/workspace": "^0.25.1",
133
+ "@gjsify/buffer": "^0.26.0",
134
+ "@gjsify/create-app": "^0.26.0",
135
+ "@gjsify/node-globals": "^0.26.0",
136
+ "@gjsify/node-polyfills": "^0.26.0",
137
+ "@gjsify/npm-registry": "^0.26.0",
138
+ "@gjsify/resolve-npm": "^0.26.0",
139
+ "@gjsify/rolldown-plugin-gjsify": "^0.26.0",
140
+ "@gjsify/rolldown-plugin-pnp": "^0.26.0",
141
+ "@gjsify/semver": "^0.26.0",
142
+ "@gjsify/tar": "^0.26.0",
143
+ "@gjsify/tsc": "^0.26.0",
144
+ "@gjsify/web-polyfills": "^0.26.0",
145
+ "@gjsify/workspace": "^0.26.0",
146
146
  "cosmiconfig": "^9.0.2",
147
147
  "get-tsconfig": "^4.14.0",
148
148
  "pkg-types": "^2.3.1",
@@ -150,13 +150,13 @@
150
150
  "yargs": "^18.0.0"
151
151
  },
152
152
  "devDependencies": {
153
- "@gjsify/unit": "^0.25.1",
153
+ "@gjsify/unit": "^0.26.0",
154
154
  "@types/yargs": "^17.0.35",
155
155
  "typescript": "^6.0.3"
156
156
  },
157
157
  "peerDependencies": {
158
- "@gjsify/oxfmt-native": "^0.25.1",
159
- "@gjsify/rolldown-native": "^0.25.1"
158
+ "@gjsify/oxfmt-native": "^0.26.0",
159
+ "@gjsify/rolldown-native": "^0.26.0"
160
160
  },
161
161
  "peerDependenciesMeta": {
162
162
  "@gjsify/oxfmt-native": {