@gjsify/cli 0.39.0 → 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 (63) hide show
  1. package/dist/cli.gjs.mjs +197 -180
  2. package/lib/cli-app.js +11 -25
  3. package/lib/commands/flatpak/check.js +1 -1
  4. package/lib/commands/flatpak/ci.js +1 -1
  5. package/lib/commands/flatpak/diff.js +1 -1
  6. package/lib/commands/flatpak/init.js +1 -1
  7. package/lib/commands/flatpak/scaffold.d.ts +2 -4
  8. package/lib/commands/flatpak/scaffold.js +25 -298
  9. package/lib/commands/flatpak/sync-flathub.js +1 -1
  10. package/lib/commands/foreach.js +34 -20
  11. package/lib/commands/index.d.ts +1 -0
  12. package/lib/commands/index.js +1 -0
  13. package/lib/commands/ship.d.ts +11 -0
  14. package/lib/commands/ship.js +217 -0
  15. package/lib/config.d.ts +12 -0
  16. package/lib/config.js +15 -0
  17. package/lib/types/config-data.d.ts +136 -46
  18. package/lib/utils/app-metadata.d.ts +51 -0
  19. package/lib/utils/app-metadata.js +338 -0
  20. package/lib/utils/cli-runtime-closure.d.ts +23 -0
  21. package/lib/utils/cli-runtime-closure.js +274 -0
  22. package/lib/utils/publish-headers.d.ts +13 -0
  23. package/lib/utils/publish-headers.js +30 -4
  24. package/lib/utils/ship/ar.d.ts +21 -0
  25. package/lib/utils/ship/ar.js +57 -0
  26. package/lib/utils/ship/bytes.d.ts +4 -0
  27. package/lib/utils/ship/bytes.js +23 -0
  28. package/lib/utils/ship/cpio.d.ts +22 -0
  29. package/lib/utils/ship/cpio.js +93 -0
  30. package/lib/utils/ship/deb.d.ts +24 -0
  31. package/lib/utils/ship/deb.js +118 -0
  32. package/lib/utils/ship/depends.d.ts +62 -0
  33. package/lib/utils/ship/depends.js +202 -0
  34. package/lib/utils/ship/discover.d.ts +21 -0
  35. package/lib/utils/ship/discover.js +129 -0
  36. package/lib/utils/ship/formats.d.ts +5 -0
  37. package/lib/utils/ship/formats.js +83 -0
  38. package/lib/utils/ship/gi-namespaces.d.ts +7 -0
  39. package/lib/utils/ship/gi-namespaces.js +49 -0
  40. package/lib/utils/ship/gzip.d.ts +2 -0
  41. package/lib/utils/ship/gzip.js +21 -0
  42. package/lib/utils/ship/launcher.d.ts +7 -0
  43. package/lib/utils/ship/launcher.js +37 -0
  44. package/lib/utils/ship/payload.d.ts +30 -0
  45. package/lib/utils/ship/payload.js +59 -0
  46. package/lib/utils/ship/plan.d.ts +34 -0
  47. package/lib/utils/ship/plan.js +167 -0
  48. package/lib/utils/ship/rpm-header.d.ts +29 -0
  49. package/lib/utils/ship/rpm-header.js +170 -0
  50. package/lib/utils/ship/rpm.d.ts +12 -0
  51. package/lib/utils/ship/rpm.js +377 -0
  52. package/lib/utils/ship/scripts.d.ts +31 -0
  53. package/lib/utils/ship/scripts.js +76 -0
  54. package/lib/utils/ship/settings.d.ts +60 -0
  55. package/lib/utils/ship/settings.js +184 -0
  56. package/lib/utils/ship/stage-writer.d.ts +17 -0
  57. package/lib/utils/ship/stage-writer.js +49 -0
  58. package/lib/utils/ship/types.d.ts +114 -0
  59. package/lib/utils/ship/types.js +10 -0
  60. package/lib/utils/ship/version.d.ts +17 -0
  61. package/lib/utils/ship/version.js +55 -0
  62. package/package.json +19 -18
  63. /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
package/lib/cli-app.js CHANGED
@@ -15,12 +15,10 @@
15
15
  // This module MUST NOT execute anything at load time (no top-level
16
16
  // `parseAsync`) — `run.ts` imports it, and a top-level run would re-dispatch
17
17
  // the original argv on import.
18
- import { readFileSync } from 'node:fs';
19
- import { dirname, join } from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
18
  import yargs from 'yargs';
22
19
  import { classifyCliFailure } from './cli-fail.js';
23
- import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
20
+ import { cliVersion } from './utils/publish-headers.js';
21
+ import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
24
22
  import { APP_NAME } from './constants.js';
25
23
  import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
26
24
  import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
@@ -49,22 +47,6 @@ function runtimeLabel() {
49
47
  }
50
48
  return 'unknown runtime';
51
49
  }
52
- // Read the version from package.json adjacent to the bundle. yargs's
53
- // auto-version-discovery (its `pkg-up`-driven default) doesn't reach
54
- // through the bundled `dist/cli.gjs.mjs` path on GJS — falls back to
55
- // "unknown". Both layouts are covered:
56
- // - dev (tsx, `yarn workspace`): src/index.ts → ../package.json
57
- // - bundled (install -g): dist/cli.gjs.mjs → ../package.json
58
- function readBundleVersion() {
59
- try {
60
- const here = dirname(fileURLToPath(import.meta.url));
61
- const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
62
- return typeof pkg.version === 'string' ? pkg.version : 'unknown';
63
- }
64
- catch {
65
- return 'unknown';
66
- }
67
- }
68
50
  /**
69
51
  * Build + run the gjsify yargs command surface for the given argv (already
70
52
  * stripped of the `node`/`gjs` + script prefix — i.e. what `hideBin` returns).
@@ -75,15 +57,18 @@ function readBundleVersion() {
75
57
  * exit before any async work runs.
76
58
  */
77
59
  export async function runCli(argv) {
78
- // Under GJS, make a GJS-runnable `gjsify` available on PATH for child
79
- // processes (workspace/foreach orchestration + compound `gjsify run`
80
- // scripts) so node-free builds don't fall back to the Node bin. No-op on
81
- // Node.
60
+ // Make a runnable `gjsify` available on PATH for child processes
61
+ // (workspace/foreach orchestration + compound `gjsify run` scripts). TWO
62
+ // cases, not one — the "No-op on Node" this comment used to claim stopped
63
+ // being true when the bootstrap case landed: under GJS, so node-free builds
64
+ // don't fall back to the Node bin; and a BOOTSTRAP CLI under Node, where the
65
+ // tree's own `node_modules/.bin/gjsify` dangles until `build:infra` — which
66
+ // is what a cold macOS/Windows leg runs — has produced what it dispatches to.
82
67
  ensureGjsifyShimOnPath();
83
68
  const cli = yargs(argv);
84
69
  await cli
85
70
  .scriptName(APP_NAME)
86
- .version(readBundleVersion())
71
+ .version(cliVersion())
87
72
  .strict()
88
73
  // Sits beside `.strict()` because that is what produces most of what it
89
74
  // classifies. See `cli-fail.ts` for the four yargs measurements this
@@ -122,6 +107,7 @@ export async function runCli(argv) {
122
107
  .command(gettext.command, gettext.description, gettext.builder, gettext.handler)
123
108
  .command(gsettings.command, gsettings.description, gsettings.builder, gsettings.handler)
124
109
  .command(flatpak.command, flatpak.description, flatpak.builder, flatpak.handler)
110
+ .command(ship.command, ship.description, ship.builder, ship.handler)
125
111
  .command(foreach.command, foreach.description, foreach.builder, foreach.handler)
126
112
  .command(workspace.command, workspace.description, workspace.builder, workspace.handler)
127
113
  .command(pack.command, pack.description, pack.builder, pack.handler)
@@ -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);
@@ -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);
@@ -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);
@@ -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
@@ -1,328 +1,55 @@
1
- // MetaInfo XML / .desktop / flathub.json scaffolding for
2
- // `gjsify flatpak init`. Phase F.9.6 onwards builds the MetaInfo XML
3
- // directly in TypeScript instead of substituting into a static template —
4
- // the AppStream surface (description blocks, per-release rich notes,
5
- // translator hints, kudos, supports/requires/recommends, content_rating
6
- // attributes, provides) has too many optional nested sections for a
7
- // template+placeholder approach to stay legible.
1
+ // Flathub-specific scaffolding for `gjsify flatpak init`, plus the thin adapter
2
+ // that hands `gjsify.flatpak`'s metadata half to the shared renderers.
8
3
  //
9
- // `.desktop` and `flathub.json` keep their static templates (they're
10
- // flat key=value or empty-object files where substitution is fine).
4
+ // The MetaInfo XML and the `.desktop` entry moved to `utils/app-metadata.ts`
5
+ // (ADR 0024 § 8): they describe the APP, not the Flatpak, and `gjsify ship`
6
+ // stages the same two files into `share/metainfo/` and `share/applications/`.
7
+ // What stays here is genuinely Flathub's — `flathub.json`, a policy file no
8
+ // other format has.
11
9
  import { readFileSync } from 'node:fs';
10
+ import { renderDesktopEntry, renderMetainfoApp as renderMetainfoAppShared, renderMetainfoCli as renderMetainfoCliShared, validateAppMetadata, } from '../../utils/app-metadata.js';
12
11
  /**
13
- * Lazy template loaders for the two artefacts that stay template-based.
14
- * `static-read-inliner` matches this shape and inlines the templates
15
- * into the GJS bundle at build time.
12
+ * Lazy template loaders. `static-read-inliner` matches this shape and inlines
13
+ * the templates into the GJS bundle at build time.
16
14
  */
17
- function loadDesktopTemplate() {
18
- return readFileSync(new URL('../../templates/flatpak/desktop.tmpl', import.meta.url), 'utf-8');
19
- }
20
15
  function loadFlathubAppTemplate() {
21
16
  return readFileSync(new URL('../../templates/flatpak/flathub-app.json.tmpl', import.meta.url), 'utf-8');
22
17
  }
23
18
  function loadFlathubCliTemplate() {
24
19
  return readFileSync(new URL('../../templates/flatpak/flathub-cli.json.tmpl', import.meta.url), 'utf-8');
25
20
  }
21
+ /** `gjsify.flatpak` IS an `AppMetadata` plus Flatpak's own keys. */
22
+ function toMetadataInputs(inputs) {
23
+ return {
24
+ appId: inputs.appId,
25
+ name: inputs.name,
26
+ command: inputs.command,
27
+ kind: inputs.kind,
28
+ metadata: inputs.flatpak,
29
+ configKey: 'gjsify.flatpak',
30
+ };
31
+ }
26
32
  /**
27
33
  * Validate that the config has the minimum set of fields required for
28
34
  * MetaInfo XML rendering. Returns the list of missing fields with
29
35
  * actionable hints; empty list means OK.
30
36
  */
31
37
  export function validateScaffoldInputs(inputs) {
32
- const f = inputs.flatpak;
33
- const missing = [];
34
- if (!f.developer?.id || !f.developer?.name) {
35
- missing.push({
36
- field: 'gjsify.flatpak.developer',
37
- hint: 'Set `gjsify.flatpak.developer = { "id": "io.github.you", "name": "Your Name" }` in package.json. The id is reverse-DNS.',
38
- });
39
- }
40
- if (!f.summary) {
41
- missing.push({
42
- field: 'gjsify.flatpak.summary',
43
- hint: 'One-line app summary, ≤80 chars, no trailing period. Example: "Learn 6502 assembly language".',
44
- });
45
- }
46
- if (!f.description) {
47
- missing.push({
48
- field: 'gjsify.flatpak.description',
49
- hint: 'Plain text (split on blank lines) or DescriptionBlock[] for rich content with bullet lists + translator hints.',
50
- });
51
- }
52
- if (!f.license?.project) {
53
- missing.push({
54
- field: 'gjsify.flatpak.license.project',
55
- hint: 'SPDX identifier of the project license, e.g. "MIT", "GPL-3.0-or-later".',
56
- });
57
- }
58
- if (!f.homepageUrl) {
59
- missing.push({
60
- field: 'gjsify.flatpak.homepageUrl',
61
- hint: 'Required by Flathub. Example: "https://github.com/you/your-repo".',
62
- });
63
- }
64
- return missing;
38
+ return validateAppMetadata(toMetadataInputs(inputs));
65
39
  }
66
40
  /** Render the MetaInfo XML for a desktop application. */
67
41
  export function renderMetainfoApp(inputs) {
68
- return renderMetainfo(inputs, 'desktop-application');
42
+ return renderMetainfoAppShared(toMetadataInputs(inputs));
69
43
  }
70
44
  /** Render the MetaInfo XML for a console application. */
71
45
  export function renderMetainfoCli(inputs) {
72
- return renderMetainfo(inputs, 'console-application');
46
+ return renderMetainfoCliShared(toMetadataInputs(inputs));
73
47
  }
74
48
  /** Render the .desktop entry (app kind only). */
75
49
  export function renderDesktop(inputs) {
76
- const f = inputs.flatpak;
77
- const categoriesLine = (f.categories ?? ['Utility']).join(';') + ';';
78
- const keywordsLine = f.keywords?.length ? `Keywords=${f.keywords.join(';')};\n` : '';
79
- // `MimeType=` is sourced from `gjsify.flatpak.provides.mimetypes` —
80
- // the same field already populates `<mediatype>` entries in the
81
- // MetaInfo XML, so callers configure both with one knob. Typical
82
- // entries: `x-scheme-handler/<scheme>` for URL-scheme handlers
83
- // (Flatpak portal: chats / browsers can launch the app via a
84
- // custom URL), or `application/<mime>` for file-type handlers.
85
- const mimetypes = f.provides?.mimetypes ?? [];
86
- const mimetypesLine = mimetypes.length ? `MimeType=${mimetypes.join(';')};\n` : '';
87
- return substitute(loadDesktopTemplate(), {
88
- NAME: inputs.name,
89
- SUMMARY: f.summary ?? inputs.name,
90
- COMMAND: inputs.command,
91
- APP_ID: inputs.appId,
92
- CATEGORIES_LINE: categoriesLine,
93
- KEYWORDS_LINE: keywordsLine,
94
- MIMETYPES_LINE: mimetypesLine,
95
- });
50
+ return renderDesktopEntry(toMetadataInputs(inputs));
96
51
  }
97
52
  /** Render the flathub.json policy file. */
98
53
  export function renderFlathubJson(kind) {
99
54
  return kind === 'cli' ? loadFlathubCliTemplate() : loadFlathubAppTemplate();
100
55
  }
101
- // ─── MetaInfo XML builder ────────────────────────────────────────────────
102
- function renderMetainfo(inputs, kind) {
103
- const f = inputs.flatpak;
104
- const year = new Date().getFullYear();
105
- const developerName = f.developer?.name ?? '';
106
- const lines = [];
107
- lines.push('<?xml version="1.0" encoding="UTF-8"?>');
108
- lines.push(`<!-- Copyright ${year} ${escapeXml(developerName)} -->`);
109
- lines.push(`<component type="${kind}">`);
110
- lines.push(` <id>${escapeXml(inputs.appId)}</id>`);
111
- lines.push(` <metadata_license>${escapeXml(f.license?.metadata ?? 'CC0-1.0')}</metadata_license>`);
112
- lines.push(` <project_license>${escapeXml(f.license?.project ?? '')}</project_license>`);
113
- lines.push(` <name>${escapeXml(inputs.name)}</name>`);
114
- pushTranslatorHint(lines, f.summaryTranslatorHint, ' ');
115
- lines.push(` <summary>${escapeXml(f.summary ?? inputs.name)}</summary>`);
116
- if (f.iconRemote) {
117
- lines.push(` <icon type="remote">${escapeXml(f.iconRemote)}</icon>`);
118
- }
119
- // <description>
120
- lines.push(' <description>');
121
- for (const blockLine of renderDescriptionBlocks(f.description ?? '', ' ')) {
122
- lines.push(blockLine);
123
- }
124
- lines.push(' </description>');
125
- // <developer>
126
- if (f.developer?.id && f.developer?.name) {
127
- lines.push(` <developer id="${escapeXml(f.developer.id)}">`);
128
- const translateAttr = f.developer.nameTranslatable === true ? '' : ' translate="no"';
129
- lines.push(` <name${translateAttr}>${escapeXml(f.developer.name)}</name>`);
130
- if (f.developer.email) {
131
- lines.push(` <email>${escapeXml(f.developer.email)}</email>`);
132
- }
133
- lines.push(' </developer>');
134
- }
135
- if (kind === 'desktop-application') {
136
- lines.push(` <launchable type="desktop-id">${escapeXml(inputs.appId)}.desktop</launchable>`);
137
- }
138
- // <screenshots>
139
- if (f.screenshots?.length) {
140
- lines.push(' <screenshots>');
141
- f.screenshots.forEach((s, i) => {
142
- const type = s.type ?? (i === 0 ? 'default' : undefined);
143
- const typeAttr = type ? ` type="${escapeXml(type)}"` : '';
144
- const envAttr = s.environment ? ` environment="${escapeXml(s.environment)}"` : '';
145
- lines.push(` <screenshot${typeAttr}${envAttr}>`);
146
- lines.push(` <image>${escapeXml(s.url)}</image>`);
147
- if (s.caption) {
148
- pushTranslatorHint(lines, s.captionTranslatorHint, ' ');
149
- lines.push(` <caption>${escapeXml(s.caption)}</caption>`);
150
- }
151
- lines.push(' </screenshot>');
152
- });
153
- lines.push(' </screenshots>');
154
- }
155
- // <url> entries
156
- if (f.homepageUrl)
157
- lines.push(` <url type="homepage">${escapeXml(f.homepageUrl)}</url>`);
158
- if (f.bugtrackerUrl)
159
- lines.push(` <url type="bugtracker">${escapeXml(f.bugtrackerUrl)}</url>`);
160
- if (f.vcsBrowserUrl)
161
- lines.push(` <url type="vcs-browser">${escapeXml(f.vcsBrowserUrl)}</url>`);
162
- if (f.donationUrl)
163
- lines.push(` <url type="donation">${escapeXml(f.donationUrl)}</url>`);
164
- if (f.translateUrl)
165
- lines.push(` <url type="translate">${escapeXml(f.translateUrl)}</url>`);
166
- // <content_rating>
167
- const cr = normaliseContentRating(f.contentRating);
168
- if (cr.attributes && Object.keys(cr.attributes).length > 0) {
169
- lines.push(` <content_rating type="${escapeXml(cr.type)}">`);
170
- for (const [key, value] of Object.entries(cr.attributes)) {
171
- lines.push(` <content_attribute id="${escapeXml(key)}">${escapeXml(value)}</content_attribute>`);
172
- }
173
- lines.push(' </content_rating>');
174
- }
175
- else {
176
- lines.push(` <content_rating type="${escapeXml(cr.type)}" />`);
177
- }
178
- // <releases>
179
- if (f.releases?.length) {
180
- lines.push(' <releases>');
181
- for (const r of f.releases) {
182
- if (r.description === undefined) {
183
- lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}" />`);
184
- }
185
- else {
186
- lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}">`);
187
- lines.push(' <description>');
188
- for (const blockLine of renderDescriptionBlocks(r.description, ' ')) {
189
- lines.push(blockLine);
190
- }
191
- lines.push(' </description>');
192
- lines.push(' </release>');
193
- }
194
- }
195
- lines.push(' </releases>');
196
- }
197
- // <categories>
198
- if (f.categories?.length) {
199
- lines.push(' <categories>');
200
- for (const c of f.categories)
201
- lines.push(` <category>${escapeXml(c)}</category>`);
202
- lines.push(' </categories>');
203
- }
204
- // <keywords>
205
- if (f.keywords?.length) {
206
- lines.push(' <keywords>');
207
- for (const k of f.keywords)
208
- lines.push(` <keyword>${escapeXml(k)}</keyword>`);
209
- lines.push(' </keywords>');
210
- }
211
- // <branding> (apps only — Flathub ignores it on CLI)
212
- if (kind === 'desktop-application' && f.branding) {
213
- lines.push(' <branding>');
214
- lines.push(` <color type="primary" scheme_preference="light">${escapeXml(f.branding.accentLight)}</color>`);
215
- lines.push(` <color type="primary" scheme_preference="dark">${escapeXml(f.branding.accentDark)}</color>`);
216
- lines.push(' </branding>');
217
- }
218
- // <kudos>
219
- if (f.kudos?.length) {
220
- lines.push(' <kudos>');
221
- for (const k of f.kudos)
222
- lines.push(` <kudo>${escapeXml(k)}</kudo>`);
223
- lines.push(' </kudos>');
224
- }
225
- // <provides> — always emit <binary> for both kinds; <mediatype>/<dbus> only when configured
226
- const binaries = f.provides?.binaries ?? [inputs.command];
227
- const mimetypes = f.provides?.mimetypes ?? [];
228
- const dbus = f.provides?.dbus ?? [];
229
- if (binaries.length || mimetypes.length || dbus.length) {
230
- lines.push(' <provides>');
231
- for (const b of binaries)
232
- lines.push(` <binary>${escapeXml(b)}</binary>`);
233
- for (const m of mimetypes)
234
- lines.push(` <mediatype>${escapeXml(m)}</mediatype>`);
235
- for (const d of dbus)
236
- lines.push(` <dbus type="${escapeXml(d.type)}">${escapeXml(d.id)}</dbus>`);
237
- lines.push(' </provides>');
238
- }
239
- // <supports>
240
- if (f.supports?.controls?.length || f.supports?.internet) {
241
- lines.push(' <supports>');
242
- for (const c of f.supports.controls ?? [])
243
- lines.push(` <control>${escapeXml(c)}</control>`);
244
- if (f.supports.internet)
245
- lines.push(` <internet>${escapeXml(f.supports.internet)}</internet>`);
246
- lines.push(' </supports>');
247
- }
248
- // <requires>
249
- if (f.requires?.displayLengthMin || f.requires?.controls?.length || f.requires?.internet) {
250
- lines.push(' <requires>');
251
- if (f.requires.displayLengthMin) {
252
- lines.push(` <display_length compare="ge">${f.requires.displayLengthMin}</display_length>`);
253
- }
254
- for (const c of f.requires.controls ?? [])
255
- lines.push(` <control>${escapeXml(c)}</control>`);
256
- if (f.requires.internet)
257
- lines.push(` <internet>${escapeXml(f.requires.internet)}</internet>`);
258
- lines.push(' </requires>');
259
- }
260
- // <recommends>
261
- if (f.recommends?.displayLengthMin || f.recommends?.controls?.length) {
262
- lines.push(' <recommends>');
263
- if (f.recommends.displayLengthMin) {
264
- lines.push(` <display_length compare="ge">${f.recommends.displayLengthMin}</display_length>`);
265
- }
266
- for (const c of f.recommends.controls ?? [])
267
- lines.push(` <control>${escapeXml(c)}</control>`);
268
- lines.push(' </recommends>');
269
- }
270
- lines.push('</component>');
271
- return lines.join('\n') + '\n';
272
- }
273
- // ─── Description block renderer ──────────────────────────────────────────
274
- function renderDescriptionBlocks(description, indent) {
275
- const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
276
- const out = [];
277
- for (const block of blocks) {
278
- if ('p' in block) {
279
- pushTranslatorHint(out, block.translatorHint, indent);
280
- out.push(`${indent}<p>${escapeXml(block.p.trim().replace(/\s+/g, ' '))}</p>`);
281
- }
282
- else {
283
- pushTranslatorHint(out, block.translatorHint, indent);
284
- out.push(`${indent}<ul>`);
285
- for (const item of block.ul) {
286
- if (typeof item === 'string') {
287
- out.push(`${indent} <li>${escapeXml(item)}</li>`);
288
- }
289
- else {
290
- pushTranslatorHint(out, item.translatorHint, `${indent} `);
291
- out.push(`${indent} <li>${escapeXml(item.item)}</li>`);
292
- }
293
- }
294
- out.push(`${indent}</ul>`);
295
- }
296
- }
297
- return out;
298
- }
299
- /** Auto-convert blank-line-split string into paragraph blocks. */
300
- function stringToBlocks(s) {
301
- return s
302
- .trim()
303
- .split(/\n\n+/)
304
- .map((para) => ({ p: para.trim().replace(/\s+/g, ' ') }));
305
- }
306
- function pushTranslatorHint(out, hint, indent) {
307
- if (!hint)
308
- return;
309
- out.push(`${indent}<!-- TRANSLATORS: ${hint} -->`);
310
- }
311
- // ─── Helpers ─────────────────────────────────────────────────────────────
312
- function normaliseContentRating(cr) {
313
- if (cr === undefined)
314
- return { type: 'oars-1.1' };
315
- if (typeof cr === 'string')
316
- return { type: cr };
317
- return { type: cr.type ?? 'oars-1.1', attributes: cr.attributes };
318
- }
319
- function substitute(template, tokens) {
320
- let out = template;
321
- for (const [key, value] of Object.entries(tokens)) {
322
- out = out.split(`{{${key}}}`).join(value);
323
- }
324
- return out;
325
- }
326
- function escapeXml(value) {
327
- return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
328
- }
@@ -75,7 +75,7 @@ export const flatpakSyncFlathubCommand = {
75
75
  handler: async (args) => {
76
76
  const cwd = process.cwd();
77
77
  const cfg = new Config();
78
- const configData = await cfg.forBuild({}).catch(() => ({}));
78
+ const configData = await cfg.forCommand().catch(() => ({}));
79
79
  const flatpak = configData.flatpak ?? {};
80
80
  const appId = args.appId ?? flatpak.appId ?? readPackageJson(cwd).name;
81
81
  if (!appId) {
@@ -13,9 +13,15 @@ import { readFileSync, readdirSync } from 'node:fs';
13
13
  import { cpus } from 'node:os';
14
14
  import { affectedClosure, buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
15
15
  import { findWorkspaceRoot } from '../utils/workspace-root.js';
16
+ import { cliRuntimeClosure } from '../utils/cli-runtime-closure.js';
16
17
  import { prefixLines } from '../utils/prefixed-output.js';
17
18
  import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
18
- import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
19
+ // The ONE runner-selection rule, shared rather than re-derived. This file used to
20
+ // carry its own copy; `workspace.ts` grew the bootstrap-CLI branch and the copy
21
+ // here did not, so a `gjsify foreach` reached from a cold-tree bootstrap would
22
+ // have delegated to npm and lost the shim one level down — silently, and only on
23
+ // the OS legs that bootstrap cold. `onboard.ts` already imports it from here.
24
+ import { detectPackageManager } from './workspace.js';
19
25
  // Every child spawned by spawnPrefixed registers here so fail-fast can
20
26
  // terminate the whole in-flight set instead of waiting on it. On CI a
21
27
  // single failed package used to leave foreach awaiting its still-running
@@ -425,20 +431,44 @@ export const foreachCommand = {
425
431
  targets: selected.map((w) => w.name),
426
432
  })
427
433
  : undefined;
434
+ // The serial prefix: every child of this command BOOTS the CLI, so a
435
+ // package the CLI imports must not be written while siblings are starting
436
+ // (the torn `lib/esm` in `utils/cli-runtime-closure.ts`'s header). Building
437
+ // that closure first — alone, in the order the caller already sorted — is a
438
+ // REORDERING, not extra work: the prefix is removed from the parallel set,
439
+ // so the cost is only the parallelism given up over those few packages.
440
+ //
441
+ // Applied to every PARALLEL invocation, not only to `build`. Which scripts
442
+ // rewrite an output tree is not knowable from a script NAME — `build`,
443
+ // `build:types`, a package's own `dist` step and `clear` all do — and
444
+ // gating on the literal string "build" would be the hand-written list this
445
+ // closure exists to replace. A serial run needs no prefix at all: nothing
446
+ // else is in flight to read what it writes.
447
+ const prefixNames = args.parallel === true ? cliRuntimeClosure(allWorkspaces) : new Set();
448
+ const prefix = prefixNames.size > 0 ? selected.filter((ws) => prefixNames.has(ws.name)) : [];
449
+ const sweep = prefix.length > 0 ? selected.filter((ws) => !prefixNames.has(ws.name)) : selected;
428
450
  try {
451
+ if (prefix.length > 0) {
452
+ console.error(`[gjsify foreach] serial prefix: ${prefix.length} package(s) the running CLI imports — ` +
453
+ `${prefix.map((ws) => ws.name).join(', ')}`);
454
+ for (const ws of prefix) {
455
+ console.error(`[gjsify foreach] start ${ws.name} (serial prefix)`);
456
+ await runOne(ws, finalCmd, cmdArgs, /* prefixOutput */ true, verbose, exec, cache);
457
+ }
458
+ }
429
459
  if (args.parallel && !args.topological && !args['topological-dev']) {
430
460
  const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
431
- await runParallel(selected, finalCmd, cmdArgs, jobs, verbose, exec, cache);
461
+ await runParallel(sweep, finalCmd, cmdArgs, jobs, verbose, exec, cache);
432
462
  }
433
463
  else if (args.parallel) {
434
464
  // Topological + parallel: each workspace starts as soon as its
435
465
  // deps (in the selected set) have finished. Yarn calls this
436
466
  // "topological order with concurrency"; we cap at --jobs.
437
467
  const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
438
- await runTopologicalParallel(selected, finalCmd, cmdArgs, jobs, verbose, args['topological-dev'] === true, exec, cache);
468
+ await runTopologicalParallel(sweep, finalCmd, cmdArgs, jobs, verbose, args['topological-dev'] === true, exec, cache);
439
469
  }
440
470
  else {
441
- await runSequential(selected, finalCmd, cmdArgs, verbose, exec, cache);
471
+ await runSequential(sweep, finalCmd, cmdArgs, verbose, exec, cache);
442
472
  }
443
473
  }
444
474
  catch (err) {
@@ -703,22 +733,6 @@ async function runOne(ws, script, args, prefixOutput, verbose, exec, cache) {
703
733
  if (cache && before)
704
734
  cache.storeAfterSuccess(ws, before);
705
735
  }
706
- function detectPackageManager() {
707
- // Under GJS there is no Node — neither `npm` nor `yarn` can run scripts, so
708
- // the runner is `gjsify` itself via the GJS shim on PATH
709
- // (`ensureGjsifyShimOnPath`). Makes node-free `gjsify foreach` work.
710
- if (isGjs())
711
- return 'gjsify';
712
- // `npm_config_user_agent` is set by npm/yarn/pnpm — first token is
713
- // `<name>/<version>`. Reuse it so `gjsify foreach build` invoked
714
- // through `yarn run` keeps using yarn, etc.
715
- const ua = process.env.npm_config_user_agent ?? '';
716
- if (ua.startsWith('yarn/'))
717
- return 'yarn';
718
- if (ua.startsWith('gjsify/'))
719
- return 'gjsify';
720
- return 'npm';
721
- }
722
736
  async function spawnPrefixed(cmd, args, cwd, prefix) {
723
737
  // `completion: 'exit'` — every path out of this command's handler ends in
724
738
  // `process.exit(…)` (see the handler's tail), which is what quits the GLib
@@ -12,6 +12,7 @@ export * from './gresource.js';
12
12
  export * from './gettext.js';
13
13
  export * from './gsettings.js';
14
14
  export { flatpakCommand } from './flatpak/index.js';
15
+ export * from './ship.js';
15
16
  export * from './dlx.js';
16
17
  export * from './install.js';
17
18
  export * from './foreach.js';
@@ -12,6 +12,7 @@ export * from './gresource.js';
12
12
  export * from './gettext.js';
13
13
  export * from './gsettings.js';
14
14
  export { flatpakCommand } from './flatpak/index.js';
15
+ export * from './ship.js';
15
16
  export * from './dlx.js';
16
17
  export * from './install.js';
17
18
  export * from './foreach.js';
@@ -0,0 +1,11 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface ShipOptions {
3
+ target?: string[];
4
+ out?: string;
5
+ stage: boolean;
6
+ 'skip-build': boolean;
7
+ arch?: string;
8
+ verbose: boolean;
9
+ }
10
+ export declare const shipCommand: Command<unknown, ShipOptions>;
11
+ export {};