@gjsify/cli 0.38.1 → 0.39.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.
@@ -11,7 +11,7 @@
11
11
  // `flatpak install -y flathub org.flatpak.Builder`.
12
12
  import { existsSync, readdirSync } from 'node:fs';
13
13
  import { resolve, join } from 'node:path';
14
- import { spawn } from 'node:child_process';
14
+ import { spawnToCompletion } from '../../utils/spawn.js';
15
15
  import { readPackageJson, looksLikeAppId } from './utils.js';
16
16
  import { Config } from '../../config.js';
17
17
  export const flatpakCheckCommand = {
@@ -123,48 +123,39 @@ function resolveManifestPath(explicit, appId, cwd) {
123
123
  throw new Error('gjsify flatpak check: no manifest path given and could not auto-detect. ' +
124
124
  'Pass the manifest as a positional argument.');
125
125
  }
126
- function runLinter(bin, args, verbose) {
127
- return new Promise((resolveP) => {
128
- const child = spawn(bin, args, {
129
- stdio: verbose ? 'inherit' : ['ignore', 'pipe', 'pipe'],
126
+ async function runLinter(bin, args, verbose) {
127
+ // `completion: 'return'` — the handler's SUCCESS path ends with a `console.log`
128
+ // and returns, so under GJS an async spawn leaves `ensureMainLoop`'s loop armed
129
+ // and the command parks at 0 % CPU after every check has passed. The failure
130
+ // path exits, which is why the hang would have shown up only on a clean run.
131
+ let result;
132
+ try {
133
+ result = await spawnToCompletion(bin, args, {
134
+ completion: 'return',
135
+ stdio: verbose ? 'inherit' : 'capture',
130
136
  });
131
- let stdout = '';
132
- let stderr = '';
133
- if (!verbose) {
134
- child.stdout?.setEncoding('utf-8');
135
- child.stderr?.setEncoding('utf-8');
136
- child.stdout?.on('data', (c) => {
137
- stdout += c;
138
- });
139
- child.stderr?.on('data', (c) => {
140
- stderr += c;
141
- });
137
+ }
138
+ catch (err) {
139
+ const e = err;
140
+ if (e.code === 'ENOENT') {
141
+ console.error(`[gjsify flatpak check] ${bin} not found in PATH.\n` +
142
+ `Install via:\n` +
143
+ ` flatpak install -y flathub org.flatpak.Builder\n` +
144
+ ` alias ${bin}="flatpak run --command=${bin} org.flatpak.Builder"`);
142
145
  }
143
- child.on('error', (err) => {
144
- const e = err;
145
- if (e.code === 'ENOENT') {
146
- console.error(`[gjsify flatpak check] ${bin} not found in PATH.\n` +
147
- `Install via:\n` +
148
- ` flatpak install -y flathub org.flatpak.Builder\n` +
149
- ` alias ${bin}="flatpak run --command=${bin} org.flatpak.Builder"`);
150
- }
151
- else {
152
- console.error(`[gjsify flatpak check] ${bin} failed to spawn: ${e.message}`);
153
- }
154
- resolveP(false);
155
- });
156
- child.on('exit', (code) => {
157
- const ok = code === 0;
158
- const tag = ok ? 'OK' : 'FAIL';
159
- console.log(`[gjsify flatpak check] ${tag}: ${bin} ${args.join(' ')}`);
160
- if (!ok && !verbose) {
161
- if (stdout.trim())
162
- console.log(stdout.trimEnd());
163
- if (stderr.trim())
164
- console.error(stderr.trimEnd());
165
- }
166
- resolveP(ok);
167
- });
168
- });
146
+ else {
147
+ console.error(`[gjsify flatpak check] ${bin} failed to spawn: ${e.message}`);
148
+ }
149
+ return false;
150
+ }
151
+ const ok = result.code === 0;
152
+ console.log(`[gjsify flatpak check] ${ok ? 'OK' : 'FAIL'}: ${bin} ${args.join(' ')}`);
153
+ if (!ok && !verbose) {
154
+ if (result.stdout?.trim())
155
+ console.log(result.stdout.trimEnd());
156
+ if (result.stderr?.trim())
157
+ console.error(result.stderr.trimEnd());
158
+ }
159
+ return ok;
169
160
  }
170
161
  void join;
@@ -7,7 +7,7 @@
7
7
  // https://github.com/flatpak/flatpak-builder-tools — installed via
8
8
  // `pipx install flatpak-node-generator` or `pip install --user
9
9
  // flatpak-node-generator`.
10
- import { spawn } from 'node:child_process';
10
+ import { describeExit, spawnToCompletion } from '../../utils/spawn.js';
11
11
  import { existsSync } from 'node:fs';
12
12
  import { dirname, resolve } from 'node:path';
13
13
  import { mkdirSync } from 'node:fs';
@@ -64,25 +64,16 @@ export const flatpakDepsCommand = {
64
64
  if (args.verbose) {
65
65
  console.log(`[gjsify flatpak deps] flatpak-node-generator ${cmdArgs.join(' ')}`);
66
66
  }
67
- await new Promise((res, rej) => {
68
- const child = spawn('flatpak-node-generator', cmdArgs, { stdio: 'inherit' });
69
- child.on('error', (err) => {
70
- if (err.code === 'ENOENT') {
71
- rej(new Error('gjsify flatpak deps: flatpak-node-generator not found. ' +
72
- 'Install via `pipx install flatpak-node-generator` ' +
73
- '(see https://github.com/flatpak/flatpak-builder-tools/tree/master/node).'));
74
- }
75
- else {
76
- rej(err);
77
- }
78
- });
79
- child.on('exit', (code) => {
80
- if (code === 0)
81
- res();
82
- else
83
- rej(new Error(`gjsify flatpak deps: flatpak-node-generator exited with status ${code}`));
84
- });
67
+ // `completion: 'return'` — this handler returns; see `utils/spawn.ts`.
68
+ const result = await spawnToCompletion('flatpak-node-generator', cmdArgs, {
69
+ completion: 'return',
70
+ notFound: () => new Error('gjsify flatpak deps: flatpak-node-generator not found. ' +
71
+ 'Install via `pipx install flatpak-node-generator` ' +
72
+ '(see https://github.com/flatpak/flatpak-builder-tools/tree/master/node).'),
85
73
  });
74
+ if (result.code !== 0) {
75
+ throw new Error(`gjsify flatpak deps: flatpak-node-generator exited with ${describeExit(result)}`);
76
+ }
86
77
  console.log(`[gjsify flatpak deps] wrote ${out}`);
87
78
  },
88
79
  };
@@ -13,7 +13,8 @@
13
13
  // behaviour to running the sub-commands directly. The orchestrator
14
14
  // stops at the first failure and reports which step blocked, with a
15
15
  // clear command the user can re-run by hand.
16
- import { execFile, spawn } from 'node:child_process';
16
+ import { execFile } from 'node:child_process';
17
+ import { describeExit, spawnToCompletion } from '../../utils/spawn.js';
17
18
  import { promisify } from 'node:util';
18
19
  import { fileURLToPath } from 'node:url';
19
20
  import { resolve, dirname } from 'node:path';
@@ -139,14 +140,9 @@ function resolveCliEntry() {
139
140
  }
140
141
  /** Spawn `node <args>` and reject on non-zero exit. */
141
142
  async function runNode(args, cwd) {
142
- await new Promise((resolvePromise, reject) => {
143
- const child = spawn('node', args, { cwd, stdio: 'inherit' });
144
- child.on('error', reject);
145
- child.on('exit', (code) => {
146
- if (code === 0)
147
- resolvePromise();
148
- else
149
- reject(new Error(`sub-command exited with code ${code}: node ${args.join(' ')}`));
150
- });
151
- });
143
+ // `completion: 'return'` — the handler returns; see `utils/spawn.ts`.
144
+ const result = await spawnToCompletion('node', args, { completion: 'return', cwd });
145
+ if (result.code !== 0) {
146
+ throw new Error(`sub-command exited with ${describeExit(result)}: node ${args.join(' ')}`);
147
+ }
152
148
  }
@@ -17,7 +17,8 @@
17
17
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
18
  import { join, resolve } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
- import { execFile, spawn } from 'node:child_process';
20
+ import { execFile } from 'node:child_process';
21
+ import { describeExit, spawnToCompletion } from '../../utils/spawn.js';
21
22
  import { promisify } from 'node:util';
22
23
  import { Config } from '../../config.js';
23
24
  import { readPackageJson } from './utils.js';
@@ -204,28 +205,19 @@ async function detectDefaultBranch(cloneDir, verbose) {
204
205
  }
205
206
  return 'master';
206
207
  }
207
- function gitInRepo(cwd, args, verbose) {
208
+ // `completion: 'return'` on both helpers — the handler returns; see `utils/spawn.ts`.
209
+ async function gitInRepo(cwd, args, verbose) {
208
210
  if (verbose)
209
211
  console.log(`[gjsify flatpak sync-flathub] git ${args.join(' ')} (in ${cwd})`);
210
- return new Promise((res, rej) => {
211
- const child = spawn('git', args, { cwd, stdio: 'inherit' });
212
- child.on('error', (err) => {
213
- if (err.code === 'ENOENT') {
214
- rej(new Error('[gjsify flatpak sync-flathub] `git` not found. Install it from your distro.'));
215
- }
216
- else {
217
- rej(err);
218
- }
219
- });
220
- child.on('exit', (code) => {
221
- if (code === 0)
222
- res();
223
- else
224
- rej(new Error(`git ${args[0]} exited ${code}`));
225
- });
212
+ const result = await spawnToCompletion('git', args, {
213
+ completion: 'return',
214
+ cwd,
215
+ notFound: () => new Error('[gjsify flatpak sync-flathub] `git` not found. Install it from your distro.'),
226
216
  });
217
+ if (result.code !== 0)
218
+ throw new Error(`git ${args[0]} exited ${describeExit(result)}`);
227
219
  }
228
- function ghCreate(cloneDir, flathubRepo, branch, version, body, verbose) {
220
+ async function ghCreate(cloneDir, flathubRepo, branch, version, body, verbose) {
229
221
  const args = [
230
222
  'pr',
231
223
  'create',
@@ -240,23 +232,13 @@ function ghCreate(cloneDir, flathubRepo, branch, version, body, verbose) {
240
232
  ];
241
233
  if (verbose)
242
234
  console.log(`[gjsify flatpak sync-flathub] gh ${args.join(' ')} (in ${cloneDir})`);
243
- return new Promise((res, rej) => {
244
- const child = spawn('gh', args, { cwd: cloneDir, stdio: 'inherit' });
245
- child.on('error', (err) => {
246
- if (err.code === 'ENOENT') {
247
- rej(new Error('[gjsify flatpak sync-flathub] `gh` (GitHub CLI) not found. Install via `dnf install gh` (Fedora), `apt install gh` (Debian/Ubuntu), or `flatpak install -y flathub com.github.cli`.'));
248
- }
249
- else {
250
- rej(err);
251
- }
252
- });
253
- child.on('exit', (code) => {
254
- if (code === 0)
255
- res();
256
- else
257
- rej(new Error(`gh pr create exited ${code}`));
258
- });
235
+ const result = await spawnToCompletion('gh', args, {
236
+ completion: 'return',
237
+ cwd: cloneDir,
238
+ notFound: () => new Error('[gjsify flatpak sync-flathub] `gh` (GitHub CLI) not found. Install via `dnf install gh` (Fedora), `apt install gh` (Debian/Ubuntu), or `flatpak install -y flathub com.github.cli`.'),
259
239
  });
240
+ if (result.code !== 0)
241
+ throw new Error(`gh pr create exited ${describeExit(result)}`);
260
242
  }
261
243
  /**
262
244
  * Surgically edit a Flathub manifest to update the git source's `tag` +
@@ -1,6 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
3
- import { join, resolve } from 'node:path';
3
+ import { basename, join, resolve } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  const execFileAsync = promisify(execFile);
6
6
  async function listLanguages(poDir) {
@@ -80,9 +80,12 @@ function defaultFilename(domain, format, metainfoTemplate) {
80
80
  // Mirror the template filename but without the trailing `.in` (convention for
81
81
  // pre-processed metainfo templates: `org.foo.Bar.metainfo.xml.in`).
82
82
  if (metainfoTemplate) {
83
- const base = metainfoTemplate.replace(/\.in$/, '');
84
- const slash = base.lastIndexOf('/');
85
- return slash >= 0 ? base.slice(slash + 1) : base;
83
+ // `basename`, not a hand-rolled `lastIndexOf('/')`: this is a path the USER
84
+ // passed, and on win32 it separates with `\`, which the slice read as part
85
+ // of the name (#1143). `node:path` is the right owner for host tooling —
86
+ // though it only answers correctly under Node until #1146 makes
87
+ // `@gjsify/path` select win32 per host.
88
+ return basename(metainfoTemplate.replace(/\.in$/, ''));
86
89
  }
87
90
  return `${domain}.xml`;
88
91
  }
@@ -32,8 +32,14 @@ export interface SpawnToCompletionOptions {
32
32
  * stderr**, for a parent whose own stdout is machine-readable: `gjsify pack
33
33
  * --json` emits JSON there, and interleaved lifecycle-script log lines make it
34
34
  * unparseable for anything piping into `JSON.parse`.
35
+ *
36
+ * `'capture'` collects both streams into {@link SpawnCompletion} and emits
37
+ * NEITHER, for a caller that reports the child's output only on failure. Unlike
38
+ * `'pipe'` it works on both paths, so it composes with `completion: 'return'` —
39
+ * `'pipe'` hands back a live `ChildProcess` the blocking path cannot produce,
40
+ * which is why that one is `'exit'`-only.
35
41
  */
36
- stdio?: 'inherit' | 'pipe' | 'inherit-stderr';
42
+ stdio?: 'inherit' | 'pipe' | 'inherit-stderr' | 'capture';
37
43
  /**
38
44
  * Run `cmd` through the platform shell so `&&`, `|` and env-var references work.
39
45
  * `cmd` is then a whole command LINE and `args` must be empty — npm/yarn script
@@ -65,6 +71,10 @@ export interface SpawnCompletion {
65
71
  code: number | null;
66
72
  /** The terminating signal, or `null` on a normal exit. */
67
73
  signal: NodeJS.Signals | null;
74
+ /** The child's stdout — only under `stdio: 'capture'`, `''` when it wrote none. */
75
+ stdout?: string;
76
+ /** The child's stderr — only under `stdio: 'capture'`, `''` when it wrote none. */
77
+ stderr?: string;
68
78
  }
69
79
  /**
70
80
  * Run `cmd args…` to completion and resolve with its exit code and signal.
@@ -122,6 +122,16 @@ export function spawnToCompletion(cmd, args, opts) {
122
122
  });
123
123
  if (r.error)
124
124
  return Promise.reject(mapSpawnError(r.error, opts));
125
+ // `'capture'` hands both streams back instead of emitting them — this path
126
+ // already pipes, so it is the same read, routed to the caller.
127
+ if (opts.stdio === 'capture') {
128
+ return Promise.resolve({
129
+ code: r.status,
130
+ signal: r.signal ?? null,
131
+ stdout: String(r.stdout ?? ''),
132
+ stderr: String(r.stderr ?? ''),
133
+ });
134
+ }
125
135
  // Captured stdout goes to the parent's STDERR under `'inherit-stderr'`, the
126
136
  // destination the streaming path routes it to, so the two agree on where a
127
137
  // lifecycle script's chatter lands.
@@ -140,9 +150,11 @@ export function spawnToCompletion(cmd, args, opts) {
140
150
  // `['inherit', 2, 2]` is stdin inherited, stdout AND stderr → fd 2.
141
151
  stdio: opts.stdio === 'pipe'
142
152
  ? ['ignore', 'pipe', 'pipe']
143
- : opts.stdio === 'inherit-stderr'
144
- ? ['inherit', 2, 2]
145
- : 'inherit',
153
+ : opts.stdio === 'capture'
154
+ ? ['inherit', 'pipe', 'pipe']
155
+ : opts.stdio === 'inherit-stderr'
156
+ ? ['inherit', 2, 2]
157
+ : 'inherit',
146
158
  };
147
159
  if (opts.shell)
148
160
  spawnOpts.shell = true;
@@ -152,10 +164,22 @@ export function spawnToCompletion(cmd, args, opts) {
152
164
  spawnOpts.windowsVerbatimArguments = true;
153
165
  const child = spawn(win.cmd, win.args, spawnOpts);
154
166
  opts.onSpawn?.(child);
167
+ // Collected only under `'capture'`; the other modes never attach a reader,
168
+ // so an inherited stream is untouched.
169
+ let captured;
170
+ if (opts.stdio === 'capture') {
171
+ // Bound to a const the listeners close over: narrowing `captured` does
172
+ // not survive into a callback, and `captured!` would be a lie the day
173
+ // someone reorders these lines.
174
+ const buf = { stdout: '', stderr: '' };
175
+ captured = buf;
176
+ child.stdout?.on('data', (d) => (buf.stdout += String(d)));
177
+ child.stderr?.on('data', (d) => (buf.stderr += String(d)));
178
+ }
155
179
  // `close` (not `exit`) so piped stdio is fully drained before the caller's
156
180
  // flushers run; with `stdio: 'inherit'` the two are emitted back to back.
157
181
  child.on('close', (code, signal) => {
158
- resolvePromise({ code, signal });
182
+ resolvePromise(captured ? { code, signal, ...captured } : { code, signal });
159
183
  });
160
184
  child.on('error', (err) => {
161
185
  reject(mapSpawnError(err, opts));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.38.1",
3
+ "version": "0.39.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -134,20 +134,20 @@
134
134
  "cli"
135
135
  ],
136
136
  "dependencies": {
137
- "@gjsify/buffer": "^0.38.1",
138
- "@gjsify/create-app": "^0.38.1",
139
- "@gjsify/node-globals": "^0.38.1",
140
- "@gjsify/node-polyfills": "^0.38.1",
141
- "@gjsify/npm-registry": "^0.38.1",
142
- "@gjsify/resolve-npm": "^0.38.1",
143
- "@gjsify/rolldown-plugin-gjsify": "^0.38.1",
144
- "@gjsify/vite-plugin-blueprint": "^0.38.1",
145
- "@gjsify/rolldown-plugin-pnp": "^0.38.1",
146
- "@gjsify/semver": "^0.38.1",
147
- "@gjsify/tar": "^0.38.1",
148
- "@gjsify/tsc": "^0.38.1",
149
- "@gjsify/web-polyfills": "^0.38.1",
150
- "@gjsify/workspace": "^0.38.1",
137
+ "@gjsify/buffer": "^0.39.0",
138
+ "@gjsify/create-app": "^0.39.0",
139
+ "@gjsify/node-globals": "^0.39.0",
140
+ "@gjsify/node-polyfills": "^0.39.0",
141
+ "@gjsify/npm-registry": "^0.39.0",
142
+ "@gjsify/resolve-npm": "^0.39.0",
143
+ "@gjsify/rolldown-plugin-gjsify": "^0.39.0",
144
+ "@gjsify/vite-plugin-blueprint": "^0.39.0",
145
+ "@gjsify/rolldown-plugin-pnp": "^0.39.0",
146
+ "@gjsify/semver": "^0.39.0",
147
+ "@gjsify/tar": "^0.39.0",
148
+ "@gjsify/tsc": "^0.39.0",
149
+ "@gjsify/web-polyfills": "^0.39.0",
150
+ "@gjsify/workspace": "^0.39.0",
151
151
  "cosmiconfig": "^9.0.2",
152
152
  "get-tsconfig": "^4.14.0",
153
153
  "pkg-types": "^2.3.1",
@@ -155,13 +155,13 @@
155
155
  "yargs": "^18.0.0"
156
156
  },
157
157
  "devDependencies": {
158
- "@gjsify/unit": "^0.38.1",
158
+ "@gjsify/unit": "^0.39.0",
159
159
  "@types/yargs": "^17.0.35",
160
160
  "typescript": "^6.0.3"
161
161
  },
162
162
  "peerDependencies": {
163
- "@gjsify/oxfmt-native": "^0.38.1",
164
- "@gjsify/rolldown-native": "^0.38.1"
163
+ "@gjsify/oxfmt-native": "^0.39.0",
164
+ "@gjsify/rolldown-native": "^0.39.0"
165
165
  },
166
166
  "peerDependenciesMeta": {
167
167
  "@gjsify/oxfmt-native": {