@gjsify/cli 0.38.1 → 0.40.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.
Files changed (71) hide show
  1. package/dist/cli.gjs.mjs +212 -195
  2. package/lib/cli-app.js +11 -25
  3. package/lib/commands/affected-classify.d.ts +16 -0
  4. package/lib/commands/affected-classify.js +304 -0
  5. package/lib/commands/affected.js +2 -291
  6. package/lib/commands/flatpak/check.js +34 -43
  7. package/lib/commands/flatpak/ci.js +1 -1
  8. package/lib/commands/flatpak/deps.js +10 -19
  9. package/lib/commands/flatpak/diff.js +1 -1
  10. package/lib/commands/flatpak/init.js +1 -1
  11. package/lib/commands/flatpak/release.js +7 -11
  12. package/lib/commands/flatpak/scaffold.d.ts +2 -4
  13. package/lib/commands/flatpak/scaffold.js +25 -298
  14. package/lib/commands/flatpak/sync-flathub.js +18 -36
  15. package/lib/commands/foreach.js +34 -20
  16. package/lib/commands/gettext.js +7 -4
  17. package/lib/commands/index.d.ts +1 -0
  18. package/lib/commands/index.js +1 -0
  19. package/lib/commands/ship.d.ts +11 -0
  20. package/lib/commands/ship.js +217 -0
  21. package/lib/config.d.ts +12 -0
  22. package/lib/config.js +15 -0
  23. package/lib/types/config-data.d.ts +136 -46
  24. package/lib/utils/app-metadata.d.ts +51 -0
  25. package/lib/utils/app-metadata.js +338 -0
  26. package/lib/utils/cli-runtime-closure.d.ts +23 -0
  27. package/lib/utils/cli-runtime-closure.js +274 -0
  28. package/lib/utils/publish-headers.d.ts +13 -0
  29. package/lib/utils/publish-headers.js +30 -4
  30. package/lib/utils/ship/ar.d.ts +21 -0
  31. package/lib/utils/ship/ar.js +57 -0
  32. package/lib/utils/ship/bytes.d.ts +4 -0
  33. package/lib/utils/ship/bytes.js +23 -0
  34. package/lib/utils/ship/cpio.d.ts +22 -0
  35. package/lib/utils/ship/cpio.js +93 -0
  36. package/lib/utils/ship/deb.d.ts +24 -0
  37. package/lib/utils/ship/deb.js +118 -0
  38. package/lib/utils/ship/depends.d.ts +62 -0
  39. package/lib/utils/ship/depends.js +202 -0
  40. package/lib/utils/ship/discover.d.ts +21 -0
  41. package/lib/utils/ship/discover.js +129 -0
  42. package/lib/utils/ship/formats.d.ts +5 -0
  43. package/lib/utils/ship/formats.js +83 -0
  44. package/lib/utils/ship/gi-namespaces.d.ts +7 -0
  45. package/lib/utils/ship/gi-namespaces.js +49 -0
  46. package/lib/utils/ship/gzip.d.ts +2 -0
  47. package/lib/utils/ship/gzip.js +21 -0
  48. package/lib/utils/ship/launcher.d.ts +7 -0
  49. package/lib/utils/ship/launcher.js +37 -0
  50. package/lib/utils/ship/payload.d.ts +30 -0
  51. package/lib/utils/ship/payload.js +59 -0
  52. package/lib/utils/ship/plan.d.ts +34 -0
  53. package/lib/utils/ship/plan.js +167 -0
  54. package/lib/utils/ship/rpm-header.d.ts +29 -0
  55. package/lib/utils/ship/rpm-header.js +170 -0
  56. package/lib/utils/ship/rpm.d.ts +12 -0
  57. package/lib/utils/ship/rpm.js +377 -0
  58. package/lib/utils/ship/scripts.d.ts +31 -0
  59. package/lib/utils/ship/scripts.js +76 -0
  60. package/lib/utils/ship/settings.d.ts +60 -0
  61. package/lib/utils/ship/settings.js +184 -0
  62. package/lib/utils/ship/stage-writer.d.ts +17 -0
  63. package/lib/utils/ship/stage-writer.js +49 -0
  64. package/lib/utils/ship/types.d.ts +114 -0
  65. package/lib/utils/ship/types.js +10 -0
  66. package/lib/utils/ship/version.d.ts +17 -0
  67. package/lib/utils/ship/version.js +55 -0
  68. package/lib/utils/spawn.d.ts +11 -1
  69. package/lib/utils/spawn.js +28 -4
  70. package/package.json +19 -18
  71. /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
@@ -56,7 +56,8 @@ import { spawnSync } from 'node:child_process';
56
56
  // every spec and fires only inside a GitHub Actions step.
57
57
  import { appendFileSync } from 'node:fs';
58
58
  import { readStdinLines } from '../utils/stdin.js';
59
- import { discoverWorkspaces, buildDependencyGraph, buildReverseDependencyGraph, affectedClosure, workspacesForChangedFiles, } from '@gjsify/workspace';
59
+ import { discoverWorkspaces } from '@gjsify/workspace';
60
+ import { classifyAndExpand } from './affected-classify.js';
60
61
  import { findWorkspaceRoot } from '../utils/workspace-root.js';
61
62
  export const affectedCommand = {
62
63
  command: 'affected',
@@ -95,296 +96,6 @@ export const affectedCommand = {
95
96
  emit(args.format, result);
96
97
  },
97
98
  };
98
- /** Patterns that force a full run. First-match wins; order is intentional. */
99
- const GLOBAL_TRIGGERS = [
100
- // The classifier itself + everything in its plumbing.
101
- /^packages\/infra\/workspace\//,
102
- /^packages\/infra\/cli\//,
103
- /^packages\/infra\/rolldown-plugin-gjsify\//,
104
- /^packages\/infra\/resolve-npm\//,
105
- // The test framework every spec imports, as a *devDependency* — which the
106
- // prod-deps-only closure below does not walk, so a change to it would yield a
107
- // near-empty closure and silently skip every downstream test, while a bug in a
108
- // matcher can break assertions anywhere.
109
- /^packages\/gjs\/unit\//,
110
- // The composite action every CI job sets itself up with: a change alters the
111
- // environment of the whole matrix. Listed EXPLICITLY — it previously forced a
112
- // full run only by falling through to the "unmatched files" path, which is luck
113
- // rather than intent.
114
- /^\.github\/actions\//,
115
- // Cross-cutting dep + lockfile + root config.
116
- /^gjsify-lock\.json$/,
117
- /^package\.json$/,
118
- /^tsconfig[^/]*\.json$/,
119
- // The workflow file itself: a job-shape change is invisible until the workflow
120
- // re-runs, so a path-filtered job cannot safely apply the new shape to an
121
- // in-flight PR.
122
- /^\.github\/workflows\/main\.yml$/,
123
- /^scripts\/audit-runtimes\.mjs$/,
124
- ];
125
- /**
126
- * Inputs OWNED BY ANOTHER WORKFLOW — build-relevant, just not to `main.yml`.
127
- *
128
- * Adding a regex here is a claim with two halves and BOTH must hold: `main.yml`
129
- * must not read the path, and some other workflow must have it in its own
130
- * `paths:` filter so a break still reds a PR. The test is never "is this file
131
- * unimportant" — none of these are.
132
- */
133
- const OTHER_WORKFLOW_INPUTS = [
134
- // Each sibling workflow's own definition. `main.yml` is deliberately absent: it
135
- // is a GLOBAL_TRIGGER instead.
136
- /^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image|cancel-pr-runs)\.yml$/,
137
- // `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and the
138
- // classifier deciding which native packages a prebuild run rebuilds. It is in
139
- // both of that workflow's `paths:` filters; `main.yml` builds no prebuild and
140
- // runs no emulated leg.
141
- /^\.github\/prebuild-toolchain\//,
142
- // The REPO-SCOPED manifest-conformance rules (#847), gated by
143
- // `audit-runtimes.yml`, which runs on EVERY pull_request with no `paths:` filter;
144
- // `prebuilds.yml` also treats it as a shared input (#843).
145
- //
146
- // The distinction is real, not a technicality: `main.yml` DOES run
147
- // manifest-conformance code, but reaches it through
148
- // `packages/infra/manifest-conformance/` — the PACKAGE, a normal workspace that
149
- // maps and seeds like any other. Only the `scripts/`-side repo-scoped half is
150
- // ignored, and that is precisely the half `main.yml` does not load.
151
- /^scripts\/manifest-conformance\//,
152
- // The authored status data (ADR 0016) + its generator. Same two halves:
153
- // `main.yml` never runs `status:generate` and never opens `status/`, while
154
- // `audit-runtimes.yml` runs the `status-data` rule over it on every PR.
155
- //
156
- // Naming the directory makes the `.md` case intentional rather than incidental:
157
- // `status/*.md` already fell into IGNORE via the generic `/\.md$/i`, so the gap
158
- // showed up only on `status/status.json`, which matched nothing, landed in
159
- // `unmatched`, and paid for a FULL matrix run over a file `main.yml` cannot read.
160
- /^status\//,
161
- /^scripts\/generate-status\.mjs$/,
162
- ];
163
- /** Patterns that contribute no seed and don't force a full run. */
164
- const IGNORE = [
165
- /\.md$/i,
166
- /^refs\//,
167
- /^website\//,
168
- /^docs\//,
169
- ...OTHER_WORKFLOW_INPUTS,
170
- // Neither `@gjsify/node-gi` (the Node-native GI engine) nor `@gjsify/napi` (the
171
- // N-API host over SpiderMonkey) is a gjsify workspace member: the GJS-first
172
- // install/foreach tooling cannot build their node-gyp addons or Vala+C++/meson
173
- // shim, so `main.yml` neither builds nor tests them and their own workflows are
174
- // the source of truth. Without the carve-outs their files map to no workspace,
175
- // land in `unmatched`, and force a full run on every node-gi/napi PR.
176
- /^packages\/node-gi\//,
177
- /^packages\/napi\//,
178
- // Flatpak build/distribution tooling (SDK-extension manifest + metainfo). No
179
- // package-test consumers; its own `tests/e2e/flatpak-sdk-extension` runs on
180
- // `tests/e2e/**` and global triggers. Worth revisiting: ignoring `.githooks/`
181
- // for the same reason is what let a change walk past the suite guarding it (see
182
- // SCRIPT_COUPLINGS), and IGNORE wins over every other rule.
183
- /^flatpak\//,
184
- /^LICENSE/,
185
- /^\.gitignore$/,
186
- /^\.gjsify-[^/]*\.md$/,
187
- /^STATUS\.md$/,
188
- /^CHANGELOG\.md$/,
189
- /^AGENTS\.md$/,
190
- /^CLAUDE\.md$/,
191
- /^README\.md$/,
192
- ];
193
- /** Patterns that suggest a test-only change. */
194
- const TEST_PATHS = [/\.spec\.[mc]?[tj]sx?$/, /^tests\/(e2e|integration)\//];
195
- const SCRIPT_COUPLINGS = [
196
- {
197
- // `.githooks/pre-commit` is guarded by an e2e suite and reachable from no
198
- // workspace at all — not source, not a dependency, its consumer is git. So a
199
- // change to it selected NOTHING and the e2e tier stayed off (that turns on
200
- // for a `tests/e2e/**` touch): #1095 edited the hook's command line, merged
201
- // green, and turned `main` red on the push run. Same shape #1028 noted —
202
- // whoever changes the GUARDED thing walks past the guard, because the guard's
203
- // trigger names the TEST's path instead of the thing under test.
204
- //
205
- // No extra seeds: there is no workspace to rebuild. The tier is the point.
206
- match: /^\.githooks\//,
207
- seeds: [],
208
- runE2E: true,
209
- why: 'tests/e2e/git-hooks-cli-bundle-staleness drives .githooks/pre-commit',
210
- },
211
- {
212
- // `templates/*` ARE workspaces, so a template change already seeds its own
213
- // `@gjsify/template-<name>` — not the consumer that matters. That consumer is
214
- // `@gjsify/create-app`, unreachable twice over: it lives at
215
- // `packages/infra/create-gjsify/`, so no directory-name search finds it, and
216
- // it pulls the templates in through its own `scripts/process-template.mjs`
217
- // build step (which resolves each `workspace:^` specifier and copies the
218
- // result into `dist-templates/`), so there is no dependency edge either.
219
- // `dist-templates/` is a build-output cache candidate, so once stale it
220
- // stays stale.
221
- match: /^templates\//,
222
- seeds: ['@gjsify/create-app'],
223
- // `tests/e2e/create-app` is the ONLY thing that would notice — it scaffolds a
224
- // project out of `dist-templates/` and builds it. Without this a
225
- // templates-only PR got neither the regenerated templates nor that suite;
226
- // #853 passed only by also touching root `package.json`.
227
- runE2E: true,
228
- why: '@gjsify/create-app build → node scripts/process-template.mjs reads templates/',
229
- },
230
- ];
231
- function classifyAndExpand(workspaces, changedFiles) {
232
- const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
233
- if (files.length === 0) {
234
- return {
235
- global: false,
236
- reason: 'empty-diff',
237
- workspaces: [],
238
- runE2E: false,
239
- runIntegration: false,
240
- skipAll: true,
241
- };
242
- }
243
- // Ignored files go FIRST, before the global-trigger check: ignore wins over
244
- // global, so `packages/infra/cli/README.md` must not force a full run.
245
- const remaining = [];
246
- for (const f of files) {
247
- if (IGNORE.some((re) => re.test(f)))
248
- continue;
249
- remaining.push(f);
250
- }
251
- if (remaining.length === 0) {
252
- return {
253
- global: false,
254
- reason: 'ignored-only',
255
- workspaces: [],
256
- runE2E: false,
257
- runIntegration: false,
258
- skipAll: true,
259
- };
260
- }
261
- // Global triggers short-circuit — checked on the non-ignored remainder.
262
- for (const f of remaining) {
263
- for (const re of GLOBAL_TRIGGERS) {
264
- if (re.test(f)) {
265
- return {
266
- global: true,
267
- reason: `global-trigger ${re.source} matched ${f}`,
268
- workspaces: workspaces.map((w) => w.name),
269
- runE2E: true,
270
- runIntegration: true,
271
- skipAll: false,
272
- };
273
- }
274
- }
275
- }
276
- const { matched, unmatched } = workspacesForChangedFiles(workspaces, remaining);
277
- // Seeds no graph edge can reach. BEFORE the `unmatched` bail-out, so the table
278
- // also works for a coupled directory that is not itself a workspace — such a
279
- // file would otherwise force a full run and never consult the declared seeds.
280
- const knownNames = new Set(workspaces.map((w) => w.name));
281
- const couplingSeeds = new Set();
282
- const couplingAccounted = new Set();
283
- let couplingRunE2E = false;
284
- for (const c of SCRIPT_COUPLINGS) {
285
- const hits = remaining.filter((f) => c.match.test(f));
286
- if (hits.length === 0)
287
- continue;
288
- for (const h of hits)
289
- couplingAccounted.add(h);
290
- for (const s of c.seeds) {
291
- // A declared seed that is not a workspace means the table drifted from the
292
- // tree (renamed, moved, removed). Fail towards the full run and SAY SO: a
293
- // missing rebuild here is invisible, so this must never degrade quietly
294
- // back into the bug the table exists to prevent.
295
- if (!knownNames.has(s)) {
296
- return {
297
- global: true,
298
- reason: `script-coupling seed ${s} is not a workspace (${c.match.source} → ${c.why}); ` +
299
- `SCRIPT_COUPLINGS in commands/affected.ts is stale`,
300
- workspaces: workspaces.map((w) => w.name),
301
- runE2E: true,
302
- runIntegration: true,
303
- skipAll: false,
304
- };
305
- }
306
- couplingSeeds.add(s);
307
- }
308
- if (c.runE2E)
309
- couplingRunE2E = true;
310
- }
311
- // Unmatched-but-not-ignored (a new top-level dotfile, an uncarved `scripts/`
312
- // file) falls back to the full run. A file a coupling claimed does not count.
313
- const stillUnmatched = unmatched.filter((f) => !couplingAccounted.has(f));
314
- if (stillUnmatched.length > 0) {
315
- return {
316
- global: true,
317
- reason: `unmatched files (${stillUnmatched.length}): ${stillUnmatched.slice(0, 3).join(', ')}${stillUnmatched.length > 3 ? '…' : ''}`,
318
- workspaces: workspaces.map((w) => w.name),
319
- runE2E: true,
320
- runIntegration: true,
321
- skipAll: false,
322
- };
323
- }
324
- for (const s of couplingSeeds)
325
- matched.add(s);
326
- // Every remaining file is a spec / e2e / integration path under ONE workspace, so
327
- // skip the closure: test code has no downstream consumers. A coupling disables
328
- // this — its extra seeds exist precisely because a consumer DOES care.
329
- const testOnly = couplingSeeds.size === 0 && remaining.every((f) => TEST_PATHS.some((re) => re.test(f)));
330
- if (testOnly && matched.size === 1) {
331
- const only = [...matched][0];
332
- // An e2e- or integration-only change still needs its own job.
333
- const touchedE2E = remaining.some((f) => f.startsWith('tests/e2e/'));
334
- const touchedIntegration = remaining.some((f) => f.startsWith('tests/integration/'));
335
- return {
336
- global: false,
337
- reason: `test-only (${remaining.length} file(s) in ${only})`,
338
- workspaces: [only],
339
- runE2E: touchedE2E,
340
- runIntegration: touchedIntegration || isIntegrationWorkspace(only),
341
- skipAll: false,
342
- };
343
- }
344
- // PRODUCTION dependencies only. The closure answers "whose tests must re-run
345
- // because a package they depend on changed", which is a RUNTIME relationship;
346
- // walking devDependency edges conflated it with the build/test toolchain, so ANY
347
- // single-package change fanned out to ~210 of 221 workspaces and selective CI
348
- // bought almost nothing. Prod-only collapses a typical seed to a handful (sqlite
349
- // 210→4, fetch 210→34) while keeping every real runtime dependent.
350
- //
351
- // Safe against under-selection: no `*.spec.ts` imports a CROSS-PACKAGE sibling
352
- // via a devDependency (the only such imports are self-imports, covered by the
353
- // package being its own seed), `@gjsify/unit` is a GLOBAL_TRIGGER above, and
354
- // every push-to-main plus the nightly cron still run the FULL suite.
355
- const reverse = buildReverseDependencyGraph(workspaces, { includeDev: false });
356
- const closure = affectedClosure(reverse, [...matched]);
357
- // `includeDev: true`, deliberately broader than the reverse closure: integration
358
- // packages declare the pillars they exercise as `dependencies`, but the wider
359
- // walk tolerates a devDep-declared edge too. Over-running an integration suite is
360
- // cheap; missing one is not.
361
- const forward = buildDependencyGraph(workspaces, { includeDev: true });
362
- let runIntegration = false;
363
- for (const [from, deps] of forward.edges) {
364
- if (!isIntegrationWorkspace(from))
365
- continue;
366
- for (const dep of deps) {
367
- if (closure.has(dep)) {
368
- closure.add(from);
369
- runIntegration = true;
370
- break;
371
- }
372
- }
373
- }
374
- const runE2E = remaining.some((f) => f.startsWith('tests/e2e/')) || couplingRunE2E;
375
- return {
376
- global: false,
377
- reason: `closure (${closure.size} ws from ${matched.size} seed(s))` +
378
- (couplingSeeds.size > 0 ? `, ${couplingSeeds.size} via script-coupling` : ''),
379
- workspaces: [...closure].sort(),
380
- runE2E,
381
- runIntegration,
382
- skipAll: false,
383
- };
384
- }
385
- function isIntegrationWorkspace(name) {
386
- return name.startsWith('@gjsify/integration-');
387
- }
388
99
  function runGitDiff(cwd, base, head) {
389
100
  // `base...head` lists changed paths on `head` relative to the MERGE-BASE, which
390
101
  // matches what a GitHub PR diff shows and survives stacked PRs without picking up
@@ -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 = {
@@ -52,7 +52,7 @@ export const flatpakCheckCommand = {
52
52
  },
53
53
  handler: async (args) => {
54
54
  const cfg = new Config();
55
- const configData = await cfg.forBuild({}).catch(() => ({}));
55
+ const configData = await cfg.forCommand().catch(() => ({}));
56
56
  const flatpak = configData.flatpak ?? {};
57
57
  const cwd = process.cwd();
58
58
  const appId = resolveAppId(args.manifest, flatpak, cwd);
@@ -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;
@@ -58,7 +58,7 @@ export const flatpakCiCommand = {
58
58
  handler: async (args) => {
59
59
  const cwd = process.cwd();
60
60
  const cfg = new Config();
61
- const configData = await cfg.forBuild({}).catch(() => ({}));
61
+ const configData = await cfg.forCommand().catch(() => ({}));
62
62
  const flatpak = configData.flatpak ?? {};
63
63
  const pkg = readPackageJson(cwd);
64
64
  const appId = flatpak.appId ?? (looksLikeAppId(pkg.name) ? pkg.name : undefined);
@@ -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
  };
@@ -60,7 +60,7 @@ export const flatpakDiffCommand = {
60
60
  handler: async (args) => {
61
61
  const cwd = process.cwd();
62
62
  const cfg = new Config();
63
- const configData = await cfg.forBuild({}).catch(() => ({}));
63
+ const configData = await cfg.forCommand().catch(() => ({}));
64
64
  const flatpak = configData.flatpak ?? {};
65
65
  const appId = args.appId ?? flatpak.appId ?? readPackageJson(cwd).name;
66
66
  if (!appId) {
@@ -97,7 +97,7 @@ export const flatpakInitCommand = {
97
97
  },
98
98
  handler: async (args) => {
99
99
  const cfg = new Config();
100
- const configData = await cfg.forBuild({}).catch(() => ({}));
100
+ const configData = await cfg.forCommand().catch(() => ({}));
101
101
  const flatpak = configData.flatpak ?? {};
102
102
  const cwd = process.cwd();
103
103
  const pkg = readPackageJson(cwd);
@@ -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
  }
@@ -1,4 +1,6 @@
1
1
  import type { ConfigDataFlatpak } from '../../types/config-data.js';
2
+ import { type MissingFieldError } from '../../utils/app-metadata.js';
3
+ export type { MissingFieldError };
2
4
  export interface ScaffoldInputs {
3
5
  appId: string;
4
6
  name: string;
@@ -6,10 +8,6 @@ export interface ScaffoldInputs {
6
8
  kind: 'app' | 'cli';
7
9
  flatpak: ConfigDataFlatpak;
8
10
  }
9
- export interface MissingFieldError {
10
- field: string;
11
- hint: string;
12
- }
13
11
  /**
14
12
  * Validate that the config has the minimum set of fields required for
15
13
  * MetaInfo XML rendering. Returns the list of missing fields with