@gjsify/cli 0.43.0 → 0.45.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 (87) hide show
  1. package/dist/cli.gjs.mjs +453 -425
  2. package/lib/actions/build.js +2 -1
  3. package/lib/commands/affected-classify.js +7 -5
  4. package/lib/commands/build.js +6 -0
  5. package/lib/commands/flatpak/ci.js +10 -2
  6. package/lib/commands/flatpak/init.js +15 -16
  7. package/lib/commands/flatpak/utils.d.ts +0 -26
  8. package/lib/commands/flatpak/utils.js +4 -29
  9. package/lib/commands/gettext.d.ts +2 -1
  10. package/lib/commands/gettext.js +105 -47
  11. package/lib/commands/onboard.d.ts +7 -0
  12. package/lib/commands/onboard.js +349 -96
  13. package/lib/commands/ship.d.ts +3 -0
  14. package/lib/commands/ship.js +911 -70
  15. package/lib/commands/trust.d.ts +6 -0
  16. package/lib/commands/trust.js +1 -1
  17. package/lib/config.js +25 -0
  18. package/lib/types/cli-build-options.d.ts +11 -0
  19. package/lib/types/config-data.d.ts +137 -3
  20. package/lib/utils/flatpak-runtime.d.ts +41 -0
  21. package/lib/utils/flatpak-runtime.js +54 -0
  22. package/lib/utils/msgfmt-merge.d.ts +35 -0
  23. package/lib/utils/msgfmt-merge.js +102 -0
  24. package/lib/utils/npm-otp.d.ts +27 -2
  25. package/lib/utils/npm-otp.js +82 -7
  26. package/lib/utils/onboard-discovery.d.ts +68 -0
  27. package/lib/utils/onboard-discovery.js +178 -0
  28. package/lib/utils/onboard-probe.d.ts +118 -1
  29. package/lib/utils/onboard-probe.js +153 -4
  30. package/lib/utils/prompt-output.d.ts +12 -0
  31. package/lib/utils/prompt-output.js +54 -0
  32. package/lib/utils/prompt.js +22 -2
  33. package/lib/utils/publishable-packages.d.ts +1 -1
  34. package/lib/utils/publishable-packages.js +1 -0
  35. package/lib/utils/resolve-npm-package.d.ts +56 -0
  36. package/lib/utils/resolve-npm-package.js +69 -0
  37. package/lib/utils/ship/app-runtime.d.ts +214 -0
  38. package/lib/utils/ship/app-runtime.js +429 -0
  39. package/lib/utils/ship/depends.d.ts +54 -7
  40. package/lib/utils/ship/depends.js +120 -6
  41. package/lib/utils/ship/discover.js +18 -1
  42. package/lib/utils/ship/dmg.d.ts +71 -0
  43. package/lib/utils/ship/dmg.js +181 -0
  44. package/lib/utils/ship/flatpak-config.d.ts +66 -0
  45. package/lib/utils/ship/flatpak-config.js +132 -0
  46. package/lib/utils/ship/flatpak.d.ts +45 -0
  47. package/lib/utils/ship/flatpak.js +148 -0
  48. package/lib/utils/ship/formats.d.ts +116 -3
  49. package/lib/utils/ship/formats.js +869 -11
  50. package/lib/utils/ship/launcher.d.ts +13 -3
  51. package/lib/utils/ship/launcher.js +362 -17
  52. package/lib/utils/ship/layout.d.ts +268 -0
  53. package/lib/utils/ship/layout.js +351 -0
  54. package/lib/utils/ship/localize-metadata.d.ts +22 -0
  55. package/lib/utils/ship/localize-metadata.js +126 -0
  56. package/lib/utils/ship/mime.js +1 -9
  57. package/lib/utils/ship/msi.d.ts +126 -0
  58. package/lib/utils/ship/msi.js +417 -0
  59. package/lib/utils/ship/node-runtime.d.ts +71 -0
  60. package/lib/utils/ship/node-runtime.js +122 -0
  61. package/lib/utils/ship/payload.d.ts +156 -5
  62. package/lib/utils/ship/payload.js +580 -14
  63. package/lib/utils/ship/plan.d.ts +21 -0
  64. package/lib/utils/ship/plan.js +20 -7
  65. package/lib/utils/ship/plist.d.ts +58 -0
  66. package/lib/utils/ship/plist.js +198 -0
  67. package/lib/utils/ship/rpm.js +9 -3
  68. package/lib/utils/ship/schemas.d.ts +61 -0
  69. package/lib/utils/ship/schemas.js +118 -0
  70. package/lib/utils/ship/scripts.js +7 -3
  71. package/lib/utils/ship/settings.d.ts +19 -1
  72. package/lib/utils/ship/settings.js +19 -1
  73. package/lib/utils/ship/share-dirs.d.ts +23 -0
  74. package/lib/utils/ship/share-dirs.js +46 -0
  75. package/lib/utils/ship/signing.d.ts +195 -0
  76. package/lib/utils/ship/signing.js +317 -0
  77. package/lib/utils/ship/stage-manifest.d.ts +50 -13
  78. package/lib/utils/ship/stage-manifest.js +137 -14
  79. package/lib/utils/ship/stage-writer.d.ts +48 -0
  80. package/lib/utils/ship/stage-writer.js +71 -1
  81. package/lib/utils/ship/types.d.ts +264 -16
  82. package/lib/utils/ship/xml.d.ts +7 -0
  83. package/lib/utils/ship/xml.js +25 -0
  84. package/lib/utils/ship/zip.d.ts +69 -0
  85. package/lib/utils/ship/zip.js +245 -0
  86. package/lib/utils/trust-registry.js +5 -0
  87. package/package.json +19 -19
@@ -20,6 +20,12 @@ export interface TrustRequestResult {
20
20
  status: number;
21
21
  json: unknown;
22
22
  text: string;
23
+ /**
24
+ * Response headers, so a caller can honour npm's own `Retry-After` on a 429
25
+ * instead of guessing a backoff. Optional because test doubles construct this
26
+ * shape by hand and a missing header is a well-defined "it said nothing".
27
+ */
28
+ headers?: Headers;
23
29
  }
24
30
  /** A `(method, url, body?) => TrustRequestResult` function bound to a registry auth + OTP context. */
25
31
  export type TrustRequester = (method: string, url: string, body?: unknown) => Promise<TrustRequestResult>;
@@ -279,7 +279,7 @@ export function createTrustRequester(ctx) {
279
279
  catch {
280
280
  /* non-JSON body — leave json undefined */
281
281
  }
282
- return { status: res.status, json, text };
282
+ return { status: res.status, json, text, headers: res.headers };
283
283
  };
284
284
  }
285
285
  function firstLine(s) {
package/lib/config.js CHANGED
@@ -4,6 +4,7 @@ import { basename, dirname } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
6
6
  import { buildAppForRuntime } from './utils/runtimes.js';
7
+ import { DIALECT_APPS, isSourceDialect, SOURCE_DIALECTS } from '@gjsify/rolldown-plugin-gjsify';
7
8
  /**
8
9
  * Does the failed `import()` of a config look like a MODULE-RESOLUTION failure
9
10
  * (the config imports something GJS's ESM loader can't resolve for an external
@@ -268,6 +269,18 @@ export class Config {
268
269
  configData.exclude = cliArgs.exclude || [];
269
270
  if (cliArgs.consoleShim !== undefined)
270
271
  configData.consoleShim = cliArgs.consoleShim;
272
+ // `--dialect` narrows here rather than at the call site. yargs `choices`
273
+ // already rejects an unknown value typed on the command line, but a
274
+ // `gjsify.dialect` in package.json or `.gjsifyrc.*` never passes through
275
+ // yargs — so the one place both spellings meet is the one place that
276
+ // checks, and it names the alternatives instead of silently ignoring.
277
+ if (cliArgs.dialect !== undefined) {
278
+ if (!isSourceDialect(cliArgs.dialect)) {
279
+ throw new Error(`gjsify build: "${cliArgs.dialect}" is not a source dialect. Known: ${SOURCE_DIALECTS.join(', ')}. ` +
280
+ 'This is the dialect the SOURCE is written in — not gjsify.runtimes, which declares the runtime a package RUNS on.');
281
+ }
282
+ configData.dialect = cliArgs.dialect;
283
+ }
271
284
  // Default `--app` FOLLOWS the host runtime the CLI executes in: gjs when
272
285
  // run under gjs, node when run under node/bun/deno (both consume the
273
286
  // `--app node` bundle). Applied post-merge (not as a yargs `default:`)
@@ -278,6 +291,18 @@ export class Config {
278
291
  if (cliArgs.app !== undefined)
279
292
  configData.app = cliArgs.app;
280
293
  configData.app ??= buildAppForRuntime(hostRuntime());
294
+ // A dialect only composes on the targets that have a plugin for it, and
295
+ // saying so is the whole point: `--dialect react-native --app browser`
296
+ // parses, resolves and then composes NOTHING, so the alias never happens,
297
+ // the support gate never runs, and the build succeeds while doing none of
298
+ // what was asked. That is the shape this repo keeps paying for — a flag
299
+ // accepted and ignored. Checked here rather than in the plugin because the
300
+ // plugin only ever sees the targets it was composed on.
301
+ if (configData.dialect !== undefined && !DIALECT_APPS.has(configData.app)) {
302
+ throw new Error(`gjsify build: --dialect ${configData.dialect} has no effect on --app ${configData.app}. ` +
303
+ `It composes on ${[...DIALECT_APPS].join(' and ')} only. ` +
304
+ 'Drop the dialect, or build one of those targets.');
305
+ }
281
306
  if (cliArgs.globals !== undefined)
282
307
  configData.globals = cliArgs.globals;
283
308
  // Fallback applied post-merge (not as a yargs `default:`) so a
@@ -25,6 +25,17 @@ export interface CliBuildOptions {
25
25
  * working ANSI colors). Disable with `--no-console-shim`. Default: true.
26
26
  */
27
27
  consoleShim?: boolean;
28
+ /**
29
+ * The dialect the SOURCE is written in — `--dialect react-native` (ADR 0032):
30
+ * alias `react-native` onto `@gjsify/react-native` and fail the build on an
31
+ * import whose support-table status is not `supported` or `partial`. `gjs` and
32
+ * `node` targets. Default: unset — a tree with a phone leg has the real
33
+ * `react-native` on purpose.
34
+ *
35
+ * A `string` here and a `SourceDialect` in `ConfigData`: yargs hands back
36
+ * whatever was typed, and the narrowing happens once, where the value is read.
37
+ */
38
+ dialect?: string;
28
39
  /**
29
40
  * Comma-separated global identifiers the code needs, e.g.
30
41
  * `"fetch,Buffer,process,URL,crypto"`. Each maps to the corresponding
@@ -1,5 +1,5 @@
1
1
  import type { RolldownOptions, OutputOptions, RolldownPluginOption } from 'rolldown';
2
- import type { App } from '@gjsify/rolldown-plugin-gjsify';
2
+ import type { App, SourceDialect } from '@gjsify/rolldown-plugin-gjsify';
3
3
  import type { ConfigDataLibrary, ConfigDataTypescript } from './index.js';
4
4
  /**
5
5
  * Plugin entry resolvable by package name from the project's `node_modules`, so
@@ -82,6 +82,20 @@ export interface ConfigData {
82
82
  * working ANSI colors). Default: true.
83
83
  */
84
84
  consoleShim?: boolean;
85
+ /**
86
+ * The dialect the SOURCE is written in (ADR 0032 § 2 + § 8). `'react-native'`
87
+ * aliases `react-native` onto `@gjsify/react-native` and gates every named
88
+ * import against the support table at BUILD time. `gjs` and `node` app builds.
89
+ * Default: unset.
90
+ *
91
+ * Opt-in and never inferred from the dependency list: a monorepo with a phone
92
+ * leg installs the real `react-native` on purpose, and redirecting the
93
+ * specifier for every build in that tree would change what that leg resolves.
94
+ *
95
+ * NOT `gjsify.runtimes['react-native']`, which answers the opposite question —
96
+ * see the header of `SourceDialect`.
97
+ */
98
+ dialect?: SourceDialect;
85
99
  /** Comma-separated global identifiers to register. Format: see CliBuildOptions. */
86
100
  globals?: string;
87
101
  /**
@@ -416,7 +430,15 @@ export interface ConfigDataShip extends AppMetadata {
416
430
  release?: string;
417
431
  /** `Maintainer:` / `Packager:` as `Name <email>`. Default: `package.json#author`. */
418
432
  maintainer?: string;
419
- /** Formats to build when `--target` is not given. Default `['deb', 'rpm']`. */
433
+ /**
434
+ * Formats to build when `--target` is not given. Default `['deb', 'rpm']`.
435
+ *
436
+ * A project-level DEFAULT, so it is filtered to the layout `gjsify ship <os>`
437
+ * assembles rather than refused against it — a name here that wraps another
438
+ * OS's layout is dropped, and `gjsify ship` prints which. A `--target` on the
439
+ * command line is a claim about ONE run and is refused instead. Without that
440
+ * split, declaring this key made `gjsify ship darwin --stage` impossible.
441
+ */
420
442
  targets?: string[];
421
443
  /** Output root, relative to the project. Default `'ship'`. */
422
444
  outDir?: string;
@@ -443,6 +465,18 @@ export interface ConfigDataShip extends AppMetadata {
443
465
  * start.
444
466
  */
445
467
  minGjsVersion?: string;
468
+ /**
469
+ * Minimum Node major the emitted dependency asks for, when the payload is a
470
+ * `--app node` bundle. Default `'24'` — the LTS line the
471
+ * `@gjsify/node-runtime-*` packages bundle for macOS and Windows.
472
+ *
473
+ * Lower it only if the bundle genuinely runs on an older Node. The default
474
+ * excludes every current DEB stable/LTS (trixie 20, Ubuntu 24.04 18, Ubuntu
475
+ * 26.04 22), so it makes a `.deb` that apt refuses there rather than one that
476
+ * installs and then dies on the first unsupported syntax — the same trade
477
+ * `minGjsVersion` makes, and `gjsify ship` warns about it either way.
478
+ */
479
+ minNodeVersion?: string;
446
480
  /**
447
481
  * Runtime dependencies appended to the set derived from the bundle's
448
482
  * `gi://` imports — the escape hatch for a namespace the table does not
@@ -496,8 +530,108 @@ export interface ConfigDataShip extends AppMetadata {
496
530
  extraFiles?: Record<string, string>;
497
531
  /** Arguments the launcher appends before the user's own. */
498
532
  execArgs?: string[];
533
+ /**
534
+ * How `--target flatpak` builds the app: the runtime it links against and
535
+ * what the finished app is allowed to do.
536
+ *
537
+ * The new home of the `gjsify.flatpak` BUILD keys (ADR 0024 § 8). The old
538
+ * spelling still resolves and warns — the window, and what deliberately did
539
+ * NOT move, is `utils/ship/flatpak-config.ts`.
540
+ */
541
+ flatpak?: ShipFlatpakOptions;
542
+ /**
543
+ * The signing identity this project uses by default, per OS.
544
+ *
545
+ * `--sign <identity>` on the command line wins. Absent means unsigned, which
546
+ * is the default path and a legitimate output (ADR 0024 § A13).
547
+ */
548
+ sign?: ShipSignOptions;
549
+ }
550
+ /**
551
+ * `gjsify.ship.sign` — an IDENTITY per OS, and never a certificate.
552
+ *
553
+ * KEYED PER OS, and the two keys must not be one field: a Developer ID string
554
+ * and an Authenticode subject name live in different namespaces, and the value
555
+ * that resolves is chosen by the LAYOUT being packed (`FormatDescriptor.layoutOs`
556
+ * and the stage manifest's `target.os`), not by the host doing the packing
557
+ * (ADR 0024 § A12).
558
+ *
559
+ * `linux` is deliberately absent: a `.deb` and an `.rpm` carry no per-file
560
+ * signature — the artifact is signed as a whole by the repository that serves
561
+ * it, with that repository's key. A key here would look like it did something.
562
+ *
563
+ * WHAT IS NOT HERE, and each is a decision rather than an omission:
564
+ * - a certificate, a `.p12` path or a password. `codesign` and `signtool` are
565
+ * handed a NAME and resolve the private key themselves, so `gjsify ship`
566
+ * never sees a secret and nothing on this surface has to be redacted from a
567
+ * log line (§ A14).
568
+ * - an `entitlements` key. § A16 leaves library validation vs. hardened runtime
569
+ * explicitly unmeasured, and a config key for an unmeasured mechanism is a
570
+ * promise the implementation cannot keep.
571
+ * - a notarisation credential. That is a per-RUN account credential, not a
572
+ * machine-held identity, and a config key for one is an invitation to commit
573
+ * it — `--notarize <keychain-profile>` is the only way to pass it.
574
+ */
575
+ export interface ShipSignOptions {
576
+ /** `codesign --sign <identity>`. `'-'` is ad-hoc and needs no developer identity. */
577
+ darwin?: {
578
+ identity?: string;
579
+ };
580
+ /** `signtool sign /n <identity>` — the certificate's SUBJECT NAME in a Windows store. */
581
+ win32?: {
582
+ identity?: string;
583
+ };
584
+ }
585
+ /**
586
+ * The Flatpak knobs `gjsify ship` reads. Every one has a derived default.
587
+ *
588
+ * There is no `modules` / `extraModules` here, and that is a decision rather
589
+ * than an omission: under `ship` the module list IS the staged payload
590
+ * (`buildsystem: simple` + `cp -a stage/.`), and an escape hatch injecting
591
+ * arbitrary build modules would put the second staging model back in the tree —
592
+ * the one thing ADR 0024 § 8 gates the whole migration on. A project that has
593
+ * to BUILD something inside the sandbox still has `gjsify flatpak init` +
594
+ * `gjsify flatpak build`, unchanged.
595
+ */
596
+ export interface ShipFlatpakOptions {
597
+ /**
598
+ * Runtime family. Default `'gnome'` — a GJS bundle needs GLib/GObject/GIO at
599
+ * runtime, and no GJS interpreter ships in the Freedesktop runtime.
600
+ */
601
+ runtime?: 'gnome' | 'freedesktop';
602
+ /** Runtime/SDK version, e.g. `'50'` for GNOME or `'24.08'` for Freedesktop. */
603
+ runtimeVersion?: string;
604
+ /**
605
+ * The branch the app is exported under — the last segment of
606
+ * `app/<id>/<arch>/<branch>`. Default `'stable'`, which is what Flathub
607
+ * publishes and what `flatpak install` resolves without being told.
608
+ */
609
+ branch?: string;
610
+ /** Extra SDK extensions, e.g. `['org.freedesktop.Sdk.Extension.llvm17']`. */
611
+ sdkExtensions?: string[];
612
+ /**
613
+ * Path components prepended to `PATH` inside the build sandbox. Derived from
614
+ * `sdkExtensions` when unset, because an extension whose `bin` is not on
615
+ * PATH is one the build cannot use.
616
+ */
617
+ appendPath?: string[];
618
+ /** Finish-args (capabilities). Default depends on `kind`: GUI args for an app, none for a CLI. */
619
+ finishArgs?: string[];
620
+ /** Cleanup globs applied to `/app`, e.g. `['/include', '/lib/pkgconfig']`. */
621
+ cleanup?: string[];
499
622
  }
500
- /** Flatpak-toolchain config for the `gjsify flatpak` subcommand group. */
623
+ /**
624
+ * Flatpak-toolchain config for the `gjsify flatpak` subcommand group.
625
+ *
626
+ * Six of these keys — `runtime`, `runtimeVersion`, `sdkExtensions`,
627
+ * `appendPath`, `finishArgs`, `cleanup` — have a second home at
628
+ * {@link ShipFlatpakOptions}, and `gjsify ship` warns when it reads them from
629
+ * here (ADR 0024 § 8; the window is `utils/ship/flatpak-config.ts`). They are
630
+ * NOT marked `@deprecated`: for the subcommands in this group they are still
631
+ * the only spelling, and those commands have not moved under `ship` yet
632
+ * (`status/open-todos.md`). Nothing else in this block is deprecated at all —
633
+ * the {@link AppMetadata} half is a designed alias, not a legacy one.
634
+ */
501
635
  export interface ConfigDataFlatpak extends AppMetadata {
502
636
  /** Reverse-DNS app id, e.g. `eu.jumplink.Learn6502`. Defaults to `package.json#name` if it looks like a reverse-DNS id. */
503
637
  appId?: string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Default GNOME-Platform runtime version. Bumped per release window.
3
+ * GNOME 50 = April 2026 stable; tracked in
4
+ * https://docs.flathub.org/docs/for-app-authors/requirements.
5
+ */
6
+ export declare const DEFAULT_GNOME_RUNTIME_VERSION = "50";
7
+ /** Default Freedesktop-Platform runtime version (LTS-ish). */
8
+ export declare const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = "24.08";
9
+ /** Permissive GUI defaults for GTK4 + Adwaita apps. */
10
+ export declare const DEFAULT_GUI_FINISH_ARGS: string[];
11
+ /** Lean defaults for headless CLI tools — no display, no GPU. */
12
+ export declare const DEFAULT_CLI_FINISH_ARGS: string[];
13
+ /**
14
+ * The runtime keys, structurally — not `ConfigDataFlatpak`.
15
+ *
16
+ * Two config blocks answer this question now (`gjsify.flatpak` and
17
+ * `gjsify.ship.flatpak`, § the deprecation window in `utils/ship/flatpak-config.ts`),
18
+ * and a resolver typed to one of them would have to be duplicated for the other.
19
+ */
20
+ export interface RuntimeSelection {
21
+ runtime?: string;
22
+ runtimeVersion?: string;
23
+ }
24
+ export interface ResolvedRuntime {
25
+ runtime: 'gnome' | 'freedesktop';
26
+ runtimeId: string;
27
+ sdk: string;
28
+ runtimeVersion: string;
29
+ }
30
+ /**
31
+ * Pick the runtime + sdk + version triple from config + CLI overrides.
32
+ * `--runtime` and `--runtime-version` flags win over config values.
33
+ */
34
+ export declare function resolveRuntime(config: RuntimeSelection | undefined, overrides: RuntimeSelection): ResolvedRuntime;
35
+ /**
36
+ * `append-path` for the build sandbox, derived from the declared SDK extensions.
37
+ *
38
+ * `/app/bin` is appended last so a build command finds the app's own binaries
39
+ * without knowing where they landed.
40
+ */
41
+ export declare function deriveAppendPath(sdkExtensions: readonly string[]): string[];
@@ -0,0 +1,54 @@
1
+ // Which runtime a Flatpak is built against, and what the finished app may do.
2
+ //
3
+ // Lifted out of `commands/flatpak/` when `gjsify ship --target flatpak` became
4
+ // the second caller (ADR 0024 § 8). Same move, same reason, as
5
+ // `utils/app-metadata.ts`: these are facts about the APP, not about whichever
6
+ // command happens to render them, and the second copy is the one that drifts —
7
+ // a `DEFAULT_GNOME_RUNTIME_VERSION` bumped in one place and not the other
8
+ // produces a `ship` Flatpak and a `flatpak init` Flatpak targeting different
9
+ // GNOME releases from the same `package.json`.
10
+ /**
11
+ * Default GNOME-Platform runtime version. Bumped per release window.
12
+ * GNOME 50 = April 2026 stable; tracked in
13
+ * https://docs.flathub.org/docs/for-app-authors/requirements.
14
+ */
15
+ export const DEFAULT_GNOME_RUNTIME_VERSION = '50';
16
+ /** Default Freedesktop-Platform runtime version (LTS-ish). */
17
+ export const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = '24.08';
18
+ /** Permissive GUI defaults for GTK4 + Adwaita apps. */
19
+ export const DEFAULT_GUI_FINISH_ARGS = ['--device=dri', '--share=ipc', '--socket=fallback-x11', '--socket=wayland'];
20
+ /** Lean defaults for headless CLI tools — no display, no GPU. */
21
+ export const DEFAULT_CLI_FINISH_ARGS = [];
22
+ /**
23
+ * Pick the runtime + sdk + version triple from config + CLI overrides.
24
+ * `--runtime` and `--runtime-version` flags win over config values.
25
+ */
26
+ export function resolveRuntime(config, overrides) {
27
+ const runtime = (overrides.runtime ?? config?.runtime ?? 'gnome');
28
+ if (runtime !== 'gnome' && runtime !== 'freedesktop') {
29
+ throw new Error(`gjsify flatpak: unknown runtime "${runtime}" (expected "gnome" or "freedesktop")`);
30
+ }
31
+ const runtimeVersion = overrides.runtimeVersion ??
32
+ config?.runtimeVersion ??
33
+ (runtime === 'gnome' ? DEFAULT_GNOME_RUNTIME_VERSION : DEFAULT_FREEDESKTOP_RUNTIME_VERSION);
34
+ if (runtime === 'gnome') {
35
+ return { runtime, runtimeId: 'org.gnome.Platform', sdk: 'org.gnome.Sdk', runtimeVersion };
36
+ }
37
+ return { runtime, runtimeId: 'org.freedesktop.Platform', sdk: 'org.freedesktop.Sdk', runtimeVersion };
38
+ }
39
+ /**
40
+ * `append-path` for the build sandbox, derived from the declared SDK extensions.
41
+ *
42
+ * `/app/bin` is appended last so a build command finds the app's own binaries
43
+ * without knowing where they landed.
44
+ */
45
+ export function deriveAppendPath(sdkExtensions) {
46
+ const out = [];
47
+ for (const ext of sdkExtensions) {
48
+ const m = /^org\.freedesktop\.Sdk\.Extension\.([A-Za-z0-9-]+)$/.exec(ext);
49
+ if (m)
50
+ out.push(`/usr/lib/sdk/${m[1]}/bin`);
51
+ }
52
+ out.push('/app/bin');
53
+ return out;
54
+ }
@@ -0,0 +1,35 @@
1
+ /** Which of msgfmt's two template writers to run. */
2
+ export type MsgfmtMergeMode = '--desktop' | '--xml';
3
+ /** One catalogue: the language tag msgfmt is TOLD, and the `.po` holding it. */
4
+ export interface MsgfmtCatalogue {
5
+ /** Passed as `--locale=`. Named explicitly so no `LINGUAS` file is needed — constraint 2. */
6
+ locale: string;
7
+ /** Path to a `.po`. msgfmt reads only this format here: a `.mo` gives `syntax error`, exit 1. */
8
+ po: string;
9
+ }
10
+ export interface MsgfmtMergeOptions {
11
+ mode: MsgfmtMergeMode;
12
+ /** The file the FIRST call substitutes into. Every later call substitutes into the previous output. */
13
+ template: string;
14
+ /** The suffix every intermediate keeps — load-bearing for `--xml`, see constraint 4. */
15
+ extension: string;
16
+ catalogues: readonly MsgfmtCatalogue[];
17
+ /** A directory this function may fill with intermediates; the caller owns its removal. */
18
+ workDir: string;
19
+ /** Called with each msgfmt argv, for `--verbose`. */
20
+ onCall?: (args: readonly string[]) => void;
21
+ }
22
+ /**
23
+ * Fold every catalogue into one file, chaining `msgfmt` once per language.
24
+ *
25
+ * Returns the PATH of the last file written — the caller decides whether to read
26
+ * it, post-process it or copy it. With no catalogues that is `template` itself,
27
+ * so a project with nothing to merge never spawns msgfmt at all.
28
+ *
29
+ * `stdio: 'pipe'` so a failure's stderr survives on the thrown error rather than
30
+ * going to the terminal and leaving the caller with an exit code.
31
+ *
32
+ * Throws when two catalogues name the same locale — constraint 5, which msgfmt
33
+ * itself reports as two successes and one invalid file.
34
+ */
35
+ export declare function mergeCatalogues(options: MsgfmtMergeOptions): string;
@@ -0,0 +1,102 @@
1
+ // Merging gettext catalogues INTO a template — the `msgfmt --desktop` / `--xml` half.
2
+ //
3
+ // Two callers need this and they are not variants of each other: `gjsify gettext`
4
+ // substitutes a template a user wrote, `gjsify ship` substitutes metadata it just
5
+ // rendered. What they share is every constraint below, all five measured against
6
+ // GNU gettext 0.26, and each one fails a DIFFERENT way — two of them silently.
7
+ // Written twice, the silent ones are the copies that lose them.
8
+ //
9
+ // 1. `--desktop` and `--xml` REQUIRE `--template`:
10
+ // `msgfmt: --desktop requires a "--template template" specification`, exit 1.
11
+ // Neither format can be produced from `.po` files alone, so a caller without a
12
+ // template has nothing to ask for.
13
+ //
14
+ // 2. The bulk form (`-d <podir>`) fails SILENTLY. With no `LINGUAS` file beside
15
+ // the `.po` files:
16
+ // msgfmt --desktop --template=app.desktop.in -d po -o out.desktop
17
+ // exit 0 · stderr `po/LINGUAS does not exist` · out.desktop UNTRANSLATED
18
+ // and `desktop-file-validate out.desktop` then exits 0 as well. Exit 0, an
19
+ // English file and a passing oracle is the worst of the three outcomes, which
20
+ // is why this module names each language with `--locale=` instead: that form
21
+ // needs no `LINGUAS` and cannot degrade to a no-op without a non-zero exit.
22
+ //
23
+ // 3. The template and the output must never be the SAME path. msgfmt truncates
24
+ // `--output-file` before it reads the template, so chaining in place destroys
25
+ // it: measured, `--desktop` wrote a 0-byte file and exited 0, `--xml` printed
26
+ // `cannot read <file>: Document is empty` and died on SIGSEGV (exit 139).
27
+ // Hence the numbered intermediates below rather than one file rewritten.
28
+ //
29
+ // 4. `--xml` finds its ITS rules by FILENAME PATTERN, not by reading the document.
30
+ // gettext walks `/usr/share/gettext/its/*.loc`, where each rule pairs a glob
31
+ // with a root element; AppStream's is `pattern="*.metainfo.xml"` +
32
+ // `localName="component"`, so the same bytes named `foo.xml` die with
33
+ // `msgfmt: cannot locate ITS rules for foo.xml` (exit 1). `extension` is
34
+ // therefore part of this function's contract, not a temp-file detail.
35
+ //
36
+ // Stated as "`--xml` needs a `.metainfo.xml` name" the rule would be wrong in
37
+ // the direction that matters next: `shared-mime-info.loc` pairs `pattern="*.xml"`
38
+ // with `localName="mime-info"`, so a MIME package needs no suffix care at all.
39
+ // The constraint belongs to the AppStream rule, not to `--xml`.
40
+ //
41
+ // 5. A locale may appear at most ONCE in a chain. Two calls naming the same
42
+ // `--locale` both APPEND — neither replaces the other's key — and the result
43
+ // is a file BOTH oracles reject, from two exit-0 msgfmt calls:
44
+ // msgfmt --desktop --template=app.desktop.in --locale=de -o d0 a.po # 0
45
+ // msgfmt --desktop --template=d0 --locale=de -o d1 b.po # 0
46
+ // desktop-file-validate d1
47
+ // → error: file contains multiple keys named "Name[de]" # 1
48
+ // appstreamcli validate --no-net x1.metainfo.xml (same shape, --xml)
49
+ // → E: tag-duplicated name (lang=de) · Validation failed # 3
50
+ // So this is worse than constraint 2: not an untranslated file that installs
51
+ // fine, but an INVALID one, produced without a single non-zero exit. It is
52
+ // refused here rather than deduplicated here, because the caller holding two
53
+ // catalogues for one language is the only party that can say which
54
+ // translation wins — `utils/ship/localize-metadata.ts` answers it with
55
+ // `msgcat --use-first`.
56
+ import { execFileSync } from 'node:child_process';
57
+ import { join } from 'node:path';
58
+ /**
59
+ * Fold every catalogue into one file, chaining `msgfmt` once per language.
60
+ *
61
+ * Returns the PATH of the last file written — the caller decides whether to read
62
+ * it, post-process it or copy it. With no catalogues that is `template` itself,
63
+ * so a project with nothing to merge never spawns msgfmt at all.
64
+ *
65
+ * `stdio: 'pipe'` so a failure's stderr survives on the thrown error rather than
66
+ * going to the terminal and leaving the caller with an exit code.
67
+ *
68
+ * Throws when two catalogues name the same locale — constraint 5, which msgfmt
69
+ * itself reports as two successes and one invalid file.
70
+ */
71
+ export function mergeCatalogues(options) {
72
+ let current = options.template;
73
+ // BEFORE the first spawn, so a caller that got this wrong is told rather than
74
+ // handed a half-written chain to clean up. `seen` is a Set of the locale, not
75
+ // of the catalogue: the same language reached through two `.po` files is
76
+ // exactly the case that produces the duplicate key.
77
+ const seen = new Set();
78
+ for (const { locale } of options.catalogues) {
79
+ if (seen.has(locale)) {
80
+ throw new Error(`mergeCatalogues: two catalogues both claim locale "${locale}". msgfmt APPENDS each ` +
81
+ `--locale, so the chain would emit a duplicate key (desktop-file-validate: ` +
82
+ `"multiple keys named Name[${locale}]"; appstreamcli: "tag-duplicated") from two ` +
83
+ 'exit-0 calls. Merge them into one .po first (msgcat --use-first) and pass that.');
84
+ }
85
+ seen.add(locale);
86
+ }
87
+ for (const [index, catalogue] of options.catalogues.entries()) {
88
+ const next = join(options.workDir, `${index}${options.extension}`);
89
+ const args = [
90
+ options.mode,
91
+ `--template=${current}`,
92
+ `--locale=${catalogue.locale}`,
93
+ `--output-file=${next}`,
94
+ catalogue.po,
95
+ ];
96
+ options.onCall?.(args);
97
+ execFileSync('msgfmt', args, { stdio: 'pipe' });
98
+ // Every language after the first merges INTO what the previous one wrote.
99
+ current = next;
100
+ }
101
+ return current;
102
+ }
@@ -31,6 +31,23 @@ export declare class OtpProvider {
31
31
  private cached;
32
32
  private readonly promptFn;
33
33
  private readonly registry;
34
+ /**
35
+ * Bumped every time a NEW code becomes current. Concurrent callers compare the
36
+ * epoch they last saw against this one to tell "my code was rejected" from
37
+ * "somebody already replaced it while I was waiting" — the second needs no
38
+ * prompt, it needs the other one's code.
39
+ */
40
+ private generation;
41
+ /**
42
+ * The prompt currently on screen, if any. THE fix for a sweep that probes with
43
+ * concurrency > 1: `promptLine` puts the terminal in RAW mode and echoes each
44
+ * keystroke by hand (utils/prompt.ts), so two live prompts mean two `data`
45
+ * listeners on the same stdin and every digit is echoed TWICE. Measured on a
46
+ * real 703-package sweep at the default concurrency of 4: five prompts stacked
47
+ * up and the typed code came back as `666644440000999944444444` — four echoes
48
+ * per keystroke. Whoever arrives second awaits this promise instead.
49
+ */
50
+ private pending;
34
51
  /**
35
52
  * @param seed Initial cached code (from `--otp`), or undefined.
36
53
  * @param promptFn How to read a fresh code. Defaults to the TTY-aware
@@ -41,17 +58,25 @@ export declare class OtpProvider {
41
58
  constructor(seed?: string, promptFn?: OtpPromptFn, options?: OtpProviderOptions);
42
59
  /** The currently cached OTP code, if any. */
43
60
  current(): string | undefined;
61
+ /**
62
+ * How many times a new code has become current. Capture it before an attempt
63
+ * and hand it back to {@link refresh} / {@link invalidate} so a concurrent
64
+ * caller neither prompts for a code somebody already typed nor throws that
65
+ * code away on its own stale rejection.
66
+ */
67
+ epoch(): number;
44
68
  /**
45
69
  * Prompt for a fresh code, cache it (in-process + file cache), and return the
46
70
  * trimmed input — `''` when nothing was entered, which callers read as "give up".
47
71
  */
48
- refresh(question?: string): Promise<string>;
72
+ refresh(question?: string, sinceEpoch?: number): Promise<string>;
73
+ private promptOnce;
49
74
  /**
50
75
  * Drop the cached code (in-process AND the cross-invocation file entry) after npm
51
76
  * rejects it, so the NEXT command re-prompts instead of replaying a single-use
52
77
  * code the registry already burned.
53
78
  */
54
- invalidate(): void;
79
+ invalidate(sinceEpoch?: number): void;
55
80
  }
56
81
  export interface OtpRetryOptions {
57
82
  /**