@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
@@ -630,7 +630,7 @@ export class BuildAction {
630
630
  throw new Error('gjsify build: an .html entry needs an explicit --outfile ' +
631
631
  '(e.g. --outfile dist-app/app.js); the emitted index.html is written beside it.');
632
632
  }
633
- const { consoleShim, globals } = this.configData;
633
+ const { consoleShim, globals, dialect } = this.configData;
634
634
  const userExternal = Array.isArray(userBundler.external) ? userBundler.external : undefined;
635
635
  const userBanner = typeof userBundler.output?.banner === 'string' ? userBundler.output.banner : undefined;
636
636
  const pluginOpts = {
@@ -640,6 +640,7 @@ export class BuildAction {
640
640
  exclude,
641
641
  reflection: typescript?.reflection,
642
642
  consoleShim,
643
+ ...(dialect !== undefined ? { dialect } : {}),
643
644
  ...(aliases ? { aliases } : {}),
644
645
  ...(opts.preserveDefaultExport ? { preserveDefaultExport: true } : {}),
645
646
  ...(opts.toolchainAnchor !== undefined ? { toolchainAnchor: opts.toolchainAnchor } : {}),
@@ -85,11 +85,13 @@ const IGNORE = [
85
85
  /^docs\//,
86
86
  ...OTHER_WORKFLOW_INPUTS,
87
87
  // Neither `@gjsify/node-gi` (the Node-native GI engine) nor `@gjsify/napi` (the
88
- // N-API host over SpiderMonkey) is a gjsify workspace member: the GJS-first
89
- // install/foreach tooling cannot build their node-gyp addons or Vala+C++/meson
90
- // shim, so `main.yml` neither builds nor tests them and their own workflows are
91
- // the source of truth. Without the carve-outs their files map to no workspace,
92
- // land in `unmatched`, and force a full run on every node-gi/napi PR.
88
+ // N-API host over SpiderMonkey) is a gjsify workspace member — WHY is ADR 0031,
89
+ // which this comment used to carry alone. The short of it: node-gi's `build` is
90
+ // `node-gyp rebuild`, so a member would enter `foreach build` in a container with
91
+ // no GTK/GI toolchain. `main.yml` therefore neither builds nor tests them and
92
+ // their own workflows are the source of truth. Without the carve-outs their files
93
+ // map to no workspace, land in `unmatched`, and force a full run on every
94
+ // node-gi/napi PR.
93
95
  /^packages\/node-gi\//,
94
96
  /^packages\/napi\//,
95
97
  // Flatpak build/distribution tooling (SDK-extension manifest + metainfo). No
@@ -1,5 +1,6 @@
1
1
  import { Config } from '../config.js';
2
2
  import { BuildAction } from '../actions/build.js';
3
+ import { SOURCE_DIALECTS } from '@gjsify/rolldown-plugin-gjsify';
3
4
  // NO `normalize: true` ON THE GLOB-BEARING OPTIONS — `entry-points` and
4
5
  // `exclude` are PATTERNS, not paths.
5
6
  //
@@ -106,6 +107,11 @@ export const buildCommand = {
106
107
  type: 'string',
107
108
  choices: ['silent', 'error', 'warning', 'info', 'debug', 'verbose'],
108
109
  default: 'warning',
110
+ })
111
+ .option('dialect', {
112
+ description: "The dialect the SOURCE is written in. 'react-native' aliases 'react-native' onto '@gjsify/react-native' and fails the build on an import whose ADR 0032 support-table status is not supported/partial. GJS and Node app builds. This is NOT gjsify.runtimes['react-native'], which declares the runtime a package RUNS on — the two answer opposite questions.",
113
+ type: 'string',
114
+ choices: [...SOURCE_DIALECTS],
109
115
  })
110
116
  .option('console-shim', {
111
117
  description: 'Inject a console shim into GJS builds for clean output without the GLib prefix and with working ANSI colors. Use --no-console-shim to disable. Only applies to GJS app builds.',
@@ -9,7 +9,9 @@
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
10
  import { dirname, resolve } from 'node:path';
11
11
  import { Config } from '../../config.js';
12
- import { defaultCiContainer, looksLikeAppId, readPackageJson, resolveRuntime } from './utils.js';
12
+ import { defaultCiContainer, looksLikeAppId, readPackageJson } from './utils.js';
13
+ import { resolveRuntime } from '../../utils/flatpak-runtime.js';
14
+ import { pickFlatpakBuildKeys } from '../../utils/ship/flatpak-config.js';
13
15
  export const flatpakCiCommand = {
14
16
  command: 'ci',
15
17
  description: 'Scaffold .github/workflows/flatpak.yml using the flathub-infra container + flatpak-builder@v6 action.',
@@ -68,7 +70,13 @@ export const flatpakCiCommand = {
68
70
  }
69
71
  const manifest = args.manifest ?? `${appId}.json`;
70
72
  const bundle = args.bundle ?? `${appId}.flatpak`;
71
- const { runtime, runtimeVersion } = resolveRuntime(flatpak, {});
73
+ // Through the deprecation window, not straight off `gjsify.flatpak`: a
74
+ // project told to move these keys to `gjsify.ship.flatpak` must not lose
75
+ // them here — the CI container tag is derived from the runtime, so
76
+ // reading the old block alone scaffolds a workflow against the default
77
+ // GNOME release rather than the pinned one.
78
+ const buildKeys = pickFlatpakBuildKeys(configData.ship?.flatpak, flatpak).values;
79
+ const { runtime, runtimeVersion } = resolveRuntime(buildKeys, {});
72
80
  const runtimeImage = args.runtimeImage ??
73
81
  flatpak.ciContainer ??
74
82
  defaultCiContainer(runtime, runtimeVersion);
@@ -13,7 +13,9 @@
13
13
  // with `skip-icons-check`.
14
14
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
15
15
  import { dirname, resolve } from 'node:path';
16
- import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, looksLikeAppId, readPackageJson, resolveRuntime, } from './utils.js';
16
+ import { looksLikeAppId, readPackageJson } from './utils.js';
17
+ import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, deriveAppendPath, resolveRuntime, } from '../../utils/flatpak-runtime.js';
18
+ import { pickFlatpakBuildKeys } from '../../utils/ship/flatpak-config.js';
17
19
  import { renderDesktop, renderFlathubJson, renderMetainfoApp, renderMetainfoCli, validateScaffoldInputs, } from './scaffold.js';
18
20
  import { Config } from '../../config.js';
19
21
  import { OxcNotFoundError, hasOxcDevDep, runOxfmt } from '../../utils/oxc-resolve.js';
@@ -109,17 +111,24 @@ export const flatpakInitCommand = {
109
111
  'or rename the package to a reverse-DNS id like org.example.MyApp.');
110
112
  }
111
113
  const kind = args.kind ?? flatpak.kind ?? (args.cliOnly ? 'cli' : 'app');
112
- const { runtime, runtimeId, sdk, runtimeVersion } = resolveRuntime(flatpak, {
114
+ // The six BUILD keys come through the deprecation window
115
+ // (`utils/ship/flatpak-config.ts`), not straight off `gjsify.flatpak`.
116
+ // These commands did NOT move, but the keys did: a project that followed
117
+ // `ship`'s own advice and moved them would otherwise lose them here and
118
+ // get a committed manifest against a different runtime version with
119
+ // different finish-args, silently, at exit 0.
120
+ const buildKeys = pickFlatpakBuildKeys(configData.ship?.flatpak, flatpak).values;
121
+ const { runtime, runtimeId, sdk, runtimeVersion } = resolveRuntime(buildKeys, {
113
122
  runtime: args.runtime,
114
123
  runtimeVersion: args.runtimeVersion,
115
124
  });
116
- const sdkExtensions = mergeArrays(flatpak.sdkExtensions, args.sdkExtension);
117
- const appendPath = flatpak.appendPath ?? (sdkExtensions?.length ? deriveAppendPath(sdkExtensions) : undefined);
125
+ const sdkExtensions = mergeArrays(buildKeys.sdkExtensions, args.sdkExtension);
126
+ const appendPath = buildKeys.appendPath ?? (sdkExtensions?.length ? deriveAppendPath(sdkExtensions) : undefined);
118
127
  const command = args.command ?? flatpak.command ?? appId;
119
128
  const explicitFinishArgs = args.finishArg;
120
129
  const finishArgs = explicitFinishArgs !== undefined
121
130
  ? explicitFinishArgs
122
- : (flatpak.finishArgs ?? (kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS));
131
+ : (buildKeys.finishArgs ?? (kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS));
123
132
  const manifest = {
124
133
  id: appId,
125
134
  runtime: runtimeId,
@@ -133,7 +142,7 @@ export const flatpakInitCommand = {
133
142
  }
134
143
  manifest.command = command;
135
144
  manifest['finish-args'] = finishArgs;
136
- const cleanup = flatpak.cleanup;
145
+ const cleanup = buildKeys.cleanup;
137
146
  if (cleanup?.length)
138
147
  manifest.cleanup = cleanup;
139
148
  // Module assembly. Two precedence rules:
@@ -253,16 +262,6 @@ function mergeArrays(a, b) {
253
262
  return undefined;
254
263
  return [...(a ?? []), ...(b ?? [])];
255
264
  }
256
- function deriveAppendPath(sdkExtensions) {
257
- const out = [];
258
- for (const ext of sdkExtensions) {
259
- const m = /^org\.freedesktop\.Sdk\.Extension\.([A-Za-z0-9-]+)$/.exec(ext);
260
- if (m)
261
- out.push(`/usr/lib/sdk/${m[1]}/bin`);
262
- }
263
- out.push('/app/bin');
264
- return out;
265
- }
266
265
  function deriveModuleName(appId) {
267
266
  const parts = appId.split('.');
268
267
  return parts[parts.length - 1] || appId;
@@ -1,32 +1,6 @@
1
- import type { ConfigDataFlatpak } from '../../types/config-data.js';
2
- /**
3
- * Default GNOME-Platform runtime version. Bumped per release window.
4
- * GNOME 50 = April 2026 stable; tracked in
5
- * https://docs.flathub.org/docs/for-app-authors/requirements.
6
- */
7
- export declare const DEFAULT_GNOME_RUNTIME_VERSION = "50";
8
- /** Default Freedesktop-Platform runtime version (LTS-ish). */
9
- export declare const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = "24.08";
10
- /** Permissive GUI defaults for GTK4 + Adwaita apps. */
11
- export declare const DEFAULT_GUI_FINISH_ARGS: string[];
12
- /** Lean defaults for headless CLI tools — no display, no GPU. */
13
- export declare const DEFAULT_CLI_FINISH_ARGS: string[];
14
1
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
15
2
  export declare function readPackageJson(dir: string): Record<string, unknown>;
16
3
  /** True if a name string looks like a reverse-DNS Flatpak app id. */
17
4
  export declare function looksLikeAppId(value: unknown): value is string;
18
- /**
19
- * Pick the runtime + sdk + version triple from config + CLI overrides.
20
- * `--runtime` and `--runtime-version` flags win over config values.
21
- */
22
- export declare function resolveRuntime(flatpak: ConfigDataFlatpak | undefined, overrides: {
23
- runtime?: string;
24
- runtimeVersion?: string;
25
- }): {
26
- runtime: 'gnome' | 'freedesktop';
27
- runtimeId: string;
28
- sdk: string;
29
- runtimeVersion: string;
30
- };
31
5
  /** Default container image for the GitHub Actions workflow. */
32
6
  export declare function defaultCiContainer(runtime: 'gnome' | 'freedesktop', runtimeVersion: string): string;
@@ -1,18 +1,10 @@
1
1
  // Shared helpers for the `gjsify flatpak <sub>` subcommand group.
2
+ //
3
+ // The runtime/SDK/finish-args half moved to `utils/flatpak-runtime.ts` when
4
+ // `gjsify ship --target flatpak` became its second caller — see that file's
5
+ // header. What is left here is what only these subcommands ask.
2
6
  import { readFileSync } from 'node:fs';
3
7
  import { resolve } from 'node:path';
4
- /**
5
- * Default GNOME-Platform runtime version. Bumped per release window.
6
- * GNOME 50 = April 2026 stable; tracked in
7
- * https://docs.flathub.org/docs/for-app-authors/requirements.
8
- */
9
- export const DEFAULT_GNOME_RUNTIME_VERSION = '50';
10
- /** Default Freedesktop-Platform runtime version (LTS-ish). */
11
- export const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = '24.08';
12
- /** Permissive GUI defaults for GTK4 + Adwaita apps. */
13
- export const DEFAULT_GUI_FINISH_ARGS = ['--device=dri', '--share=ipc', '--socket=fallback-x11', '--socket=wayland'];
14
- /** Lean defaults for headless CLI tools — no display, no GPU. */
15
- export const DEFAULT_CLI_FINISH_ARGS = [];
16
8
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
17
9
  export function readPackageJson(dir) {
18
10
  const path = resolve(dir, 'package.json');
@@ -34,23 +26,6 @@ export function readPackageJson(dir) {
34
26
  export function looksLikeAppId(value) {
35
27
  return typeof value === 'string' && /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*){2,}$/.test(value);
36
28
  }
37
- /**
38
- * Pick the runtime + sdk + version triple from config + CLI overrides.
39
- * `--runtime` and `--runtime-version` flags win over config values.
40
- */
41
- export function resolveRuntime(flatpak, overrides) {
42
- const runtime = (overrides.runtime ?? flatpak?.runtime ?? 'gnome');
43
- if (runtime !== 'gnome' && runtime !== 'freedesktop') {
44
- throw new Error(`gjsify flatpak: unknown runtime "${runtime}" (expected "gnome" or "freedesktop")`);
45
- }
46
- const runtimeVersion = overrides.runtimeVersion ??
47
- flatpak?.runtimeVersion ??
48
- (runtime === 'gnome' ? DEFAULT_GNOME_RUNTIME_VERSION : DEFAULT_FREEDESKTOP_RUNTIME_VERSION);
49
- if (runtime === 'gnome') {
50
- return { runtime, runtimeId: 'org.gnome.Platform', sdk: 'org.gnome.Sdk', runtimeVersion };
51
- }
52
- return { runtime, runtimeId: 'org.freedesktop.Platform', sdk: 'org.freedesktop.Sdk', runtimeVersion };
53
- }
54
29
  /** Default container image for the GitHub Actions workflow. */
55
30
  export function defaultCiContainer(runtime, runtimeVersion) {
56
31
  const tag = `${runtime}-${runtimeVersion}`;
@@ -1,10 +1,11 @@
1
1
  import type { Command } from '../types/index.js';
2
- type GettextFormat = 'mo' | 'xml' | 'desktop' | 'json';
2
+ type GettextFormat = 'mo' | 'xml' | 'desktop';
3
3
  interface GettextOptions {
4
4
  poDir: string;
5
5
  outDir: string;
6
6
  domain: string;
7
7
  format?: GettextFormat;
8
+ template?: string;
8
9
  metainfo?: string;
9
10
  filename?: string;
10
11
  removeXmlComments?: boolean;
@@ -1,7 +1,9 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
+ import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
3
4
  import { basename, join, resolve } from 'node:path';
4
5
  import { promisify } from 'node:util';
6
+ import { mergeCatalogues } from '../utils/msgfmt-merge.js';
5
7
  const execFileAsync = promisify(execFile);
6
8
  async function listLanguages(poDir) {
7
9
  const entries = await readdir(poDir);
@@ -25,40 +27,79 @@ async function fileExists(path) {
25
27
  return false;
26
28
  }
27
29
  }
28
- async function compileBulkXml(opts) {
30
+ /**
31
+ * Fold every catalogue into ONE template.
32
+ *
33
+ * The chain, and the four measured msgfmt constraints that shape it, live in
34
+ * `utils/msgfmt-merge.ts` — `gjsify ship` folds the catalogues it stages into the
35
+ * metadata it generates through the same function, and a second copy of those
36
+ * constraints here would be the copy that drifts.
37
+ *
38
+ * What is local to this command: the output keeps the CALLER's `--filename`,
39
+ * while every INTERMEDIATE is named after the TEMPLATE. Those have to be two
40
+ * different names, because msgfmt finds its ITS rules by filename pattern
41
+ * (constraint 4 over there) and from the second catalogue on the intermediates
42
+ * ARE templates. Named after `--filename` instead, the command's answer depended
43
+ * on how many catalogues there were: measured, `--format xml --template
44
+ * app.metainfo.xml.in --filename out.xml` wrote a translated file for a one-language
45
+ * `po/` and died on a two-language one with `cannot locate ITS rules for
46
+ * <tmpdir>/0-out.xml` — naming an internal path over a rule the user could act on.
47
+ * The template's suffix is what msgfmt validated for call 1, so it is the one
48
+ * suffix known to work for calls 2..n.
49
+ */
50
+ async function compileMerged(opts) {
51
+ const languages = await listLanguages(opts.poDir);
52
+ if (languages.length === 0) {
53
+ console.warn(`[gjsify gettext] no .po files found in ${opts.poDir}`);
54
+ return;
55
+ }
29
56
  const outputFile = join(opts.outDir, opts.filename);
30
57
  await ensureDir(opts.outDir);
31
- const args = [`--output-file=${outputFile}`, '--xml', `--template=${opts.template}`, '-d', opts.poDir];
32
- if (opts.verbose) {
33
- console.log(`[gjsify gettext] msgfmt ${args.join(' ')}`);
58
+ const workDir = await mkdtemp(join(tmpdir(), 'gjsify-gettext-'));
59
+ try {
60
+ const merged = await readFile(mergeCatalogues({
61
+ mode: `--${opts.format}`,
62
+ template: opts.template,
63
+ // The TEMPLATE's name, minus the `.in` convention — see the header.
64
+ // `basename`, so an intermediate stays inside `workDir` rather than
65
+ // following the directory part of the path the user passed.
66
+ extension: `-${basename(opts.template).replace(/\.in$/, '')}`,
67
+ catalogues: languages.map((lang) => ({ locale: lang, po: join(opts.poDir, `${lang}.po`) })),
68
+ workDir,
69
+ onCall: opts.verbose ? (args) => console.log(`[gjsify gettext] msgfmt ${args.join(' ')}`) : undefined,
70
+ }), 'utf-8');
71
+ await writeFile(outputFile, opts.removeXmlComments && opts.format === 'xml' ? stripXmlComments(merged) : merged);
34
72
  }
35
- await execFileAsync('msgfmt', args);
36
- if (opts.removeXmlComments) {
37
- const content = await readFile(outputFile, 'utf-8');
38
- await writeFile(outputFile, stripXmlComments(content));
73
+ finally {
74
+ await rm(workDir, { recursive: true, force: true });
39
75
  }
40
76
  if (opts.verbose) {
41
- console.log(`[gjsify gettext] wrote ${outputFile}`);
77
+ console.log(`[gjsify gettext] merged ${languages.length} language(s) into ${outputFile}`);
42
78
  }
43
79
  }
44
- async function compilePerLanguage(opts) {
80
+ /**
81
+ * Compile each `<lang>.po` into `<outDir>/<lang>/LC_MESSAGES/<filename>`.
82
+ *
83
+ * `mo` only. The other two formats SUBSTITUTE a template rather than producing a
84
+ * catalogue, and there is no per-language file for them to write — which is the
85
+ * defect this signature now makes unrepresentable: the old shared loop passed
86
+ * `--desktop`/`--xml` with no `--template`, and msgfmt refuses that outright
87
+ * (`--desktop requires a "--template template" specification`, exit 1). Every
88
+ * `gjsify gettext --format=desktop` invocation had therefore always failed; the
89
+ * only e2e coverage passes `--format mo`, so nothing ever ran the other branch.
90
+ */
91
+ async function compileCatalogues(opts) {
45
92
  const languages = await listLanguages(opts.poDir);
46
93
  if (languages.length === 0) {
47
94
  console.warn(`[gjsify gettext] no .po files found in ${opts.poDir}`);
48
95
  return;
49
96
  }
50
97
  for (const lang of languages) {
51
- const poFile = join(opts.poDir, `${lang}.po`);
52
- const langDir = opts.format === 'mo' ? join(opts.outDir, lang, 'LC_MESSAGES') : join(opts.outDir, lang);
98
+ const langDir = join(opts.outDir, lang, 'LC_MESSAGES');
53
99
  await ensureDir(langDir);
54
- const outputFile = join(langDir, opts.filename);
55
- // msgfmt produces the binary .mo format by default — there is no
56
- // `--mo` flag (only --xml, --desktop, --properties-output, ...).
57
- const args = [`--output-file=${outputFile}`];
58
- if (opts.format !== 'mo') {
59
- args.push(`--${opts.format}`);
60
- }
61
- args.push(poFile);
100
+ // msgfmt produces the binary .mo format by default — there is no `--mo`
101
+ // flag (only --xml, --desktop, --properties-output, ...).
102
+ const args = [`--output-file=${join(langDir, opts.filename)}`, join(opts.poDir, `${lang}.po`)];
62
103
  if (opts.verbose) {
63
104
  console.log(`[gjsify gettext] msgfmt ${args.join(' ')}`);
64
105
  }
@@ -68,24 +109,22 @@ async function compilePerLanguage(opts) {
68
109
  console.log(`[gjsify gettext] compiled ${languages.length} language(s) into ${opts.outDir}`);
69
110
  }
70
111
  }
71
- function defaultFilename(domain, format, metainfoTemplate) {
112
+ function defaultFilename(domain, format, template) {
72
113
  switch (format) {
73
114
  case 'mo':
74
115
  return `${domain}.mo`;
75
- case 'json':
76
- return `${domain}.json`;
77
116
  case 'desktop':
78
117
  return `${domain}.desktop`;
79
118
  case 'xml': {
80
119
  // Mirror the template filename but without the trailing `.in` (convention for
81
120
  // pre-processed metainfo templates: `org.foo.Bar.metainfo.xml.in`).
82
- if (metainfoTemplate) {
121
+ if (template) {
83
122
  // `basename`, not a hand-rolled `lastIndexOf('/')`: this is a path the USER
84
123
  // passed, and on win32 it separates with `\`, which the slice read as part
85
124
  // of the name (#1143). `node:path` is the right owner for host tooling —
86
125
  // though it only answers correctly under Node until #1146 makes
87
126
  // `@gjsify/path` select win32 per host.
88
- return basename(metainfoTemplate.replace(/\.in$/, ''));
127
+ return basename(template.replace(/\.in$/, ''));
89
128
  }
90
129
  return `${domain}.xml`;
91
130
  }
@@ -93,7 +132,8 @@ function defaultFilename(domain, format, metainfoTemplate) {
93
132
  }
94
133
  export const gettextCommand = {
95
134
  command: 'gettext <poDir> <outDir>',
96
- description: 'Compile gettext .po files to .mo (per-language locale tree) or substitute a metainfo template via msgfmt --xml.',
135
+ description: 'Compile gettext .po files to .mo (per-language locale tree), or merge every catalogue into a ' +
136
+ '.desktop / AppStream template via msgfmt --desktop / --xml.',
97
137
  builder: (yargs) => {
98
138
  return yargs
99
139
  .positional('poDir', {
@@ -103,7 +143,7 @@ export const gettextCommand = {
103
143
  demandOption: true,
104
144
  })
105
145
  .positional('outDir', {
106
- description: 'Output directory (locale tree for --format=mo, plain dir for xml/desktop/json)',
146
+ description: 'Output directory (locale tree for --format=mo, plain dir for xml/desktop)',
107
147
  type: 'string',
108
148
  normalize: true,
109
149
  demandOption: true,
@@ -117,13 +157,21 @@ export const gettextCommand = {
117
157
  .option('format', {
118
158
  description: 'Output format',
119
159
  type: 'string',
120
- choices: ['mo', 'xml', 'desktop', 'json'],
160
+ choices: ['mo', 'xml', 'desktop'],
121
161
  default: 'mo',
162
+ })
163
+ .option('template', {
164
+ description: 'Required for --format=xml|desktop: the file msgfmt substitutes into. Its SUFFIX matters — ' +
165
+ 'msgfmt --xml finds its ITS rules by filename pattern, so an AppStream template must be ' +
166
+ 'named `*.metainfo.xml[.in]`.',
167
+ type: 'string',
168
+ normalize: true,
122
169
  })
123
170
  .option('metainfo', {
124
- description: 'For --format=xml: path to the template (`.metainfo.xml.in`) used as msgfmt --template',
171
+ description: 'Deprecated alias for --template.',
125
172
  type: 'string',
126
173
  normalize: true,
174
+ hidden: true,
127
175
  })
128
176
  .option('filename', {
129
177
  description: 'Override the output filename (defaults to <domain>.<ext>)',
@@ -146,8 +194,12 @@ export const gettextCommand = {
146
194
  const outDir = resolve(args.outDir);
147
195
  const domain = args.domain;
148
196
  const format = args.format ?? 'mo';
149
- const metainfo = args.metainfo ? resolve(args.metainfo) : undefined;
150
- const filename = args.filename ?? defaultFilename(domain, format, metainfo);
197
+ // `--metainfo` predates the option being useful for `.desktop` too. Kept as a
198
+ // hidden alias rather than removed: it is the spelling `cli-reference.md`
199
+ // documented, so removing it would break the invocation the docs taught.
200
+ const templateArg = args.template ?? args.metainfo;
201
+ const template = templateArg ? resolve(templateArg) : undefined;
202
+ const filename = args.filename ?? defaultFilename(domain, format, template);
151
203
  const verbose = !!args.verbose;
152
204
  const removeXmlComments = !!args['remove-xml-comments'];
153
205
  if (!(await fileExists(poDir))) {
@@ -155,28 +207,34 @@ export const gettextCommand = {
155
207
  process.exitCode = 1;
156
208
  return;
157
209
  }
210
+ // REFUSED, not worked around. `--xml` and `--desktop` substitute into a
211
+ // template, and msgfmt rejects both without one (`--desktop requires a
212
+ // "--template template" specification`, exit 1). The old code fell back to a
213
+ // per-language loop that passed no template at all, so the fallback could only
214
+ // ever reproduce that same error one layer further down — with the user's
215
+ // `--format` reported as the thing that failed rather than the missing template.
216
+ if (format !== 'mo' && template === undefined) {
217
+ console.error(`[gjsify gettext] --format=${format} needs a template to substitute into: pass ` +
218
+ '`--template <file>`. msgfmt has no way to produce this format from .po files alone.' +
219
+ (format === 'xml'
220
+ ? ' Name it `*.metainfo.xml[.in]` — msgfmt --xml finds its ITS rules by filename pattern.'
221
+ : ''));
222
+ process.exitCode = 1;
223
+ return;
224
+ }
158
225
  try {
159
- if (format === 'xml' && metainfo) {
160
- await compileBulkXml({
161
- poDir,
162
- outDir,
163
- domain,
164
- template: metainfo,
165
- filename,
166
- removeXmlComments,
167
- verbose,
168
- });
226
+ if (format === 'mo') {
227
+ await compileCatalogues({ poDir, outDir, filename, verbose });
169
228
  }
170
229
  else {
171
- if (format === 'xml') {
172
- console.warn('[gjsify gettext] --format=xml without --metainfo: falling back to per-language XML files');
173
- }
174
- await compilePerLanguage({
230
+ await compileMerged({
175
231
  poDir,
176
232
  outDir,
177
- domain,
178
233
  format,
234
+ // Narrowed by the refusal above.
235
+ template: template,
179
236
  filename,
237
+ removeXmlComments,
180
238
  verbose,
181
239
  });
182
240
  }
@@ -6,6 +6,13 @@ interface OnboardOptions {
6
6
  registry?: string;
7
7
  otp?: string;
8
8
  concurrency: number;
9
+ 'write-concurrency': number;
10
+ packages?: string[];
11
+ include?: string[];
12
+ exclude?: string[];
13
+ access: string;
14
+ build: boolean;
15
+ verbose?: boolean;
9
16
  'dry-run'?: boolean;
10
17
  json?: boolean;
11
18
  yes?: boolean;