@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
@@ -0,0 +1,217 @@
1
+ // `gjsify ship` — turn a built application into something a stranger can
2
+ // install (ADR 0024).
3
+ //
4
+ // One payload, produced once, then wrapped per format. The command itself is
5
+ // thin on purpose: every decision worth testing lives in `utils/ship/*` as a
6
+ // pure function, and what is left here is filesystem work and printing.
7
+ //
8
+ // Scope today is Linux and `--app gjs`: no runtime is bundled, because GJS and
9
+ // GTK come from the distribution and shipping ~100 MiB of them would be cargo
10
+ // cult (ADR 0024 § 4). macOS and Windows layouts, and Flatpak as a target under
11
+ // this command, are the later stages of the same ADR.
12
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { basename, join, relative, resolve } from 'node:path';
14
+ import { Config } from '../config.js';
15
+ import { renderDesktopEntry, renderMetainfoApp, renderMetainfoCli, validateAppMetadata, } from '../utils/app-metadata.js';
16
+ import { readPackageJson } from '../utils/pkg-json.js';
17
+ import { describeExit, spawnToCompletion } from '../utils/spawn.js';
18
+ import { buildDeb } from '../utils/ship/deb.js';
19
+ import { deriveDepends, warnAboutGjsFloor } from '../utils/ship/depends.js';
20
+ import { discoverPayload } from '../utils/ship/discover.js';
21
+ import { resolveFormats, FORMAT_IDS } from '../utils/ship/formats.js';
22
+ import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
23
+ import { buildTimestamp, isArchIndependent } from '../utils/ship/payload.js';
24
+ import { planOverlay, planStage } from '../utils/ship/plan.js';
25
+ import { renderLauncher } from '../utils/ship/launcher.js';
26
+ import { buildRpm } from '../utils/ship/rpm.js';
27
+ import { declaredBundlePath, resolveShipSettings } from '../utils/ship/settings.js';
28
+ import { readStage, writeStage } from '../utils/ship/stage-writer.js';
29
+ const LOG = '[gjsify ship]';
30
+ export const shipCommand = {
31
+ command: 'ship',
32
+ description: `Build installable artifacts (${FORMAT_IDS.join(', ')}) from the current project.`,
33
+ builder: (yargs) => yargs
34
+ .option('target', {
35
+ type: 'string',
36
+ array: true,
37
+ description: `Formats to build, comma-separated or repeated. Default: ${FORMAT_IDS.join(',')}.`,
38
+ })
39
+ .option('out', {
40
+ type: 'string',
41
+ normalize: true,
42
+ description: 'Output root, relative to the project. Default: `gjsify.ship.outDir`, else `ship`.',
43
+ })
44
+ .option('stage', {
45
+ type: 'boolean',
46
+ default: false,
47
+ description: 'Produce the staged payload and stop, without packing anything.',
48
+ })
49
+ .option('skip-build', {
50
+ type: 'boolean',
51
+ default: false,
52
+ description: "Package what is already built instead of running the project's `build` script.",
53
+ })
54
+ .option('arch', {
55
+ type: 'string',
56
+ description: 'Target architecture in `process.arch` spelling. Default: this host.',
57
+ })
58
+ .option('verbose', {
59
+ type: 'boolean',
60
+ default: false,
61
+ description: 'Print each staged file and the GI namespaces the bundle imports.',
62
+ }),
63
+ handler: async (args) => {
64
+ const projectDir = process.cwd();
65
+ const config = new Config();
66
+ // No `.catch(() => ({}))` here, unlike the flatpak commands: a config
67
+ // file that fails to load would silently become "no config", and the
68
+ // result is a shipped artifact carrying default metadata the author
69
+ // never chose. An unreadable config is an error worth printing.
70
+ const configData = await config.forCommand(projectDir);
71
+ const ship = configData.ship ?? {};
72
+ const flatpak = configData.flatpak ?? {};
73
+ assertShippableTarget(configData.app);
74
+ const pkg = (readPackageJson(join(projectDir, 'package.json')) ?? {});
75
+ // Resolved before anything is built or written: a typo'd `--target`
76
+ // should not cost a build and leave a half-written `ship/stage/`.
77
+ const formats = resolveFormats(args.target ?? ship.targets ?? FORMAT_IDS);
78
+ if (!args['skip-build'])
79
+ await runProjectBuild(projectDir);
80
+ const discovered = discoverPayload({
81
+ projectDir,
82
+ pkg,
83
+ ship,
84
+ flatpakIcon: flatpak.icon,
85
+ declaredBundle: declaredBundlePath(pkg, ship),
86
+ });
87
+ const { settings, metadata, warnings } = resolveShipSettings({
88
+ projectDir,
89
+ pkg,
90
+ ship,
91
+ flatpak,
92
+ cli: { outDir: args.out, arch: args.arch },
93
+ discovered,
94
+ });
95
+ for (const warning of warnings)
96
+ console.warn(`${LOG} ${warning}`);
97
+ const mtime = buildTimestamp(settings.bundlePath);
98
+ const metadataInputs = {
99
+ appId: settings.appId,
100
+ name: settings.name,
101
+ command: settings.binaryName,
102
+ kind: settings.kind,
103
+ metadata,
104
+ configKey: 'gjsify.ship',
105
+ copyrightYear: new Date(mtime * 1000).getUTCFullYear(),
106
+ };
107
+ // Warn rather than fail: an incomplete AppStream component still
108
+ // installs and still runs. It is app STORES that will reject it, and
109
+ // that is a different day's problem than getting the package built.
110
+ for (const missing of validateAppMetadata(metadataInputs)) {
111
+ console.warn(`${LOG} ${missing.field} is not set — ${missing.hint}`);
112
+ }
113
+ const stageInputs = {
114
+ bundleFiles: discovered.bundleFiles,
115
+ launcher: renderLauncher(settings, basename(settings.bundlePath)),
116
+ metainfo: settings.kind === 'app' ? renderMetainfoApp(metadataInputs) : renderMetainfoCli(metadataInputs),
117
+ desktopEntry: settings.kind === 'app' ? renderDesktopEntry(metadataInputs) : undefined,
118
+ licenseText: settings.licenseFile === undefined ? undefined : readFileSync(settings.licenseFile, 'utf-8'),
119
+ };
120
+ const outRoot = resolve(projectDir, settings.outDir);
121
+ const stageDir = join(outRoot, 'stage');
122
+ const staged = planStage(settings, stageInputs);
123
+ writeStage(stageDir, staged);
124
+ console.log(`${LOG} staged ${staged.length} file(s) in ${relative(projectDir, stageDir)}/`);
125
+ if (args.verbose)
126
+ for (const file of staged)
127
+ console.log(`${LOG} ${file.path}`);
128
+ if (args.stage)
129
+ return;
130
+ const namespaces = scanGiNamespaces(readFileSync(settings.bundlePath, 'utf-8'));
131
+ if (args.verbose)
132
+ console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
133
+ const artifacts = [];
134
+ for (const format of formats) {
135
+ artifacts.push(await packOne({ format, settings, stageInputs, staged, stageDir, outRoot, namespaces, mtime }));
136
+ }
137
+ for (const artifact of artifacts) {
138
+ console.log(`${LOG} ${artifact.format}: ${relative(projectDir, artifact.path)} (${artifact.size} bytes)`);
139
+ }
140
+ },
141
+ };
142
+ async function packOne(input) {
143
+ const { format, settings, stageDir, outRoot, mtime } = input;
144
+ const overlay = planOverlay(settings, format, input.stageInputs);
145
+ const overlayDir = join(outRoot, 'overlay', format.id);
146
+ writeStage(overlayDir, overlay);
147
+ const payload = readStage([stageDir, overlayDir], [...input.staged, ...overlay]);
148
+ const depends = deriveDepends(format.id, {
149
+ namespaces: input.namespaces,
150
+ kind: settings.kind,
151
+ hasSchemas: settings.schemaFiles.length > 0,
152
+ extra: settings.extraDepends[format.id],
153
+ typelibPackages: settings.typelibPackages,
154
+ minGjsVersion: settings.minGjsVersion,
155
+ });
156
+ for (const warning of warnAboutGjsFloor(format.id, settings.minGjsVersion))
157
+ console.warn(`${LOG} ${warning}`);
158
+ const archLabel = format.archName(settings.arch, isArchIndependent(payload));
159
+ const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
160
+ const bytes = format.id === 'deb' ? await buildDeb(common) : await buildRpm(common);
161
+ const outDir = join(outRoot, 'out');
162
+ mkdirSync(outDir, { recursive: true });
163
+ const target = join(outDir, format.fileName(settings, archLabel));
164
+ writeFileSync(target, bytes);
165
+ return { format: format.id, path: target, size: bytes.byteLength };
166
+ }
167
+ /**
168
+ * Refuse a build target this command cannot package correctly.
169
+ *
170
+ * The launcher execs `gjs -m <bundle>`, so a `--app node` bundle would produce
171
+ * a package that installs and then fails at startup — and ADR 0024 § 4 says
172
+ * that case needs a BUNDLED Node, whose delivery (fetched at ship time versus a
173
+ * platform package) is still an open decision. Better to say so than to ship
174
+ * the broken half of it.
175
+ *
176
+ * An undeclared target is the common case for a GJS app and is allowed: `app`
177
+ * otherwise defaults to the HOST runtime, which would refuse a perfectly good
178
+ * GJS project merely for running this command under Node.
179
+ */
180
+ function assertShippableTarget(app) {
181
+ if (app === undefined || app === 'gjs')
182
+ return;
183
+ throw new Error(`gjsify ship: this project declares \`gjsify.app: "${app}"\`, and only \`gjs\` can be packaged today. ` +
184
+ 'The launcher execs `gjs -m <bundle>`, so any other target would install and then fail to start. ' +
185
+ 'ADR 0024 § 4 has the runtime-per-OS table and what is still open for the other targets.');
186
+ }
187
+ /**
188
+ * Run the project's own `build` script, as a CHILD process.
189
+ *
190
+ * NOT dispatched in-process, even though `cli-app.ts` exists to allow exactly
191
+ * that: `gjsify run`'s script path ends in `return process.exit(code)` on every
192
+ * branch — deliberately, because under GJS a bare `process.exit` falls through
193
+ * and it needs exactly one exit scheduled. Calling it from here would end the
194
+ * process after the build and never reach the packing, and neither the types
195
+ * nor a `--skip-build` test would have caught it.
196
+ *
197
+ * `completion: 'return'` because this handler returns afterwards; on GJS that
198
+ * selects the blocking path rather than arming a main loop only an exit quits.
199
+ */
200
+ async function runProjectBuild(projectDir) {
201
+ const pkg = readPackageJson(join(projectDir, 'package.json'));
202
+ if (typeof pkg?.scripts?.build !== 'string') {
203
+ throw new Error('gjsify ship: this project has no `build` script to run. Add one, or pass --skip-build to ' +
204
+ 'package what is already built.');
205
+ }
206
+ console.log(`${LOG} running the project's build script`);
207
+ const result = await spawnToCompletion('gjsify', ['run', 'build'], {
208
+ completion: 'return',
209
+ cwd: projectDir,
210
+ stdio: 'inherit',
211
+ notFound: () => new Error('gjsify ship: `gjsify` is not on PATH, so the project build cannot be run. Build the project ' +
212
+ 'yourself and pass --skip-build.'),
213
+ });
214
+ if (result.code !== 0) {
215
+ throw new Error(`gjsify ship: the project's build script failed${describeExit(result)}.`);
216
+ }
217
+ }
package/lib/config.d.ts CHANGED
@@ -24,5 +24,17 @@ export declare class Config {
24
24
  globals?: string;
25
25
  excludeGlobals?: string[];
26
26
  }>;
27
+ /**
28
+ * The config for a command that is NOT a build — `gjsify flatpak …`,
29
+ * `gjsify ship …`.
30
+ *
31
+ * They all want exactly what the loader already produces: the merged
32
+ * `package.json#gjsify` + config-file object, with none of `forBuild`'s
33
+ * build-specific defaulting. Reaching it through `forBuild({} as never)`
34
+ * — five copies of that line before this method existed — lies to the type
35
+ * system and drags a tsconfig read and bundler defaults into a command
36
+ * that has no bundle.
37
+ */
38
+ forCommand(searchFrom?: string): Promise<ConfigData>;
27
39
  forBuild(cliArgs: ArgumentsCamelCase<CliBuildOptions>): Promise<ConfigData>;
28
40
  }
package/lib/config.js CHANGED
@@ -233,6 +233,21 @@ export class Config {
233
233
  excludeGlobals: config.nodeScript?.excludeGlobals ?? config.excludeGlobals,
234
234
  };
235
235
  }
236
+ /**
237
+ * The config for a command that is NOT a build — `gjsify flatpak …`,
238
+ * `gjsify ship …`.
239
+ *
240
+ * They all want exactly what the loader already produces: the merged
241
+ * `package.json#gjsify` + config-file object, with none of `forBuild`'s
242
+ * build-specific defaulting. Reaching it through `forBuild({} as never)`
243
+ * — five copies of that line before this method existed — lies to the type
244
+ * system and drags a tsconfig read and bundler defaults into a command
245
+ * that has no bundle.
246
+ */
247
+ async forCommand(searchFrom = process.cwd()) {
248
+ const { config } = await this.load(searchFrom);
249
+ return config;
250
+ }
236
251
  async forBuild(cliArgs) {
237
252
  const configFile = await this.load(process.cwd());
238
253
  const configData = { ...configFile.config };
@@ -171,6 +171,13 @@ export interface ConfigData {
171
171
  * defaults declaratively. CLI flags override these.
172
172
  */
173
173
  flatpak?: ConfigDataFlatpak;
174
+ /**
175
+ * Config for `gjsify ship` — the installable artifacts (`.deb`, `.rpm`)
176
+ * built from one staged payload. Metadata left unset here falls back to
177
+ * `gjsify.flatpak`, because both blocks describe the SAME application
178
+ * (ADR 0024 § 8) and no project should have to write it twice.
179
+ */
180
+ ship?: ConfigDataShip;
174
181
  /**
175
182
  * Config for `gjsify format` / `lint` / `fix`. A thin shell: oxc's own
176
183
  * `.oxfmtrc.json` / `.oxlintrc.json` are the real configuration files.
@@ -197,52 +204,14 @@ export interface ConfigDataTest {
197
204
  /** Default runtimes when `--runtime` not specified. Default: `['gjs', 'node']`. */
198
205
  runtimes?: Array<'gjs' | 'node'>;
199
206
  }
200
- /** Flatpak-toolchain config for the `gjsify flatpak` subcommand group. */
201
- export interface ConfigDataFlatpak {
202
- /** Reverse-DNS app id, e.g. `eu.jumplink.Learn6502`. Defaults to `package.json#name` if it looks like a reverse-DNS id. */
203
- appId?: string;
204
- /**
205
- * Runtime family. Default `'gnome'` — needed at runtime by GJS bundles
206
- * for GLib/GObject/GIO. `'freedesktop'` is only suitable for non-gjsify
207
- * CLI tools (no GJS interpreter ships in the Freedesktop runtime).
208
- */
209
- runtime?: 'gnome' | 'freedesktop';
210
- /** Runtime/SDK version, e.g. `'50'` for GNOME or `'24.08'` for Freedesktop. */
211
- runtimeVersion?: string;
212
- /**
213
- * Extra SDK extensions for the manifest, e.g.
214
- * `['org.freedesktop.Sdk.Extension.llvm17']` for native code needing a specific
215
- * toolchain. Leave empty for pure gjsify projects: the GNOME runtime already
216
- * ships GJS + GLib + libsoup and `gjsify build` produces a self-contained bundle
217
- * that needs no build-time Node.
218
- */
219
- sdkExtensions?: string[];
220
- /** Path components prepended to PATH inside the build sandbox. */
221
- appendPath?: string[];
222
- /** The binary name to run (`/app/bin/<command>`). Defaults to `appId`. */
223
- command?: string;
224
- /** Finish-args (capabilities). Default depends on `runtime` + `--cli-only`. */
225
- finishArgs?: string[];
226
- /** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. `blueprint-compiler` build). */
227
- extraModules?: unknown[];
228
- /**
229
- * Full replacement for the manifest's `modules` array: used verbatim, with
230
- * neither `extraModules` nor the meson default added. For CLI tools that ship a
231
- * pre-built bundle and install via shell commands (`buildsystem: simple`).
232
- */
233
- modules?: unknown[];
234
- /** Cleanup glob patterns applied to the final manifest, e.g. `['/include', '/lib/pkgconfig']`. */
235
- cleanup?: string[];
236
- /** Source-of-truth lockfile for `gjsify flatpak deps` — `yarn.lock` or `package-lock.json`. */
237
- lockfile?: string;
238
- /**
239
- * GitHub-Actions container image override for `gjsify flatpak ci`. Default is
240
- * derived from runtime + runtimeVersion, e.g. gnome+50 →
241
- * `ghcr.io/flathub-infra/flatpak-github-actions:gnome-50`.
242
- */
243
- ciContainer?: string;
244
- /** Branches the generated workflow triggers on. Default `['main']`. */
245
- ciBranches?: string[];
207
+ /**
208
+ * The app metadata every install format needs — AppStream MetaInfo fields plus
209
+ * what a freedesktop `.desktop` entry is built from. Shared on purpose: a
210
+ * `.deb`, an `.rpm`, a Flatpak and a macOS bundle all describe the SAME
211
+ * application, so this block belongs to the app rather than to whichever packer
212
+ * happens to read it (ADR 0024 § 8).
213
+ */
214
+ export interface AppMetadata {
246
215
  /**
247
216
  * `'app'` (default) → desktop-application MetaInfo, GUI finish-args, .desktop +
248
217
  * icon required. `'cli'` → console-application MetaInfo with
@@ -394,6 +363,127 @@ export interface ConfigDataFlatpak {
394
363
  displayLengthMin?: number;
395
364
  controls?: Array<'keyboard' | 'pointing' | 'touch' | 'gamepad' | 'tablet' | 'console'>;
396
365
  };
366
+ }
367
+ /**
368
+ * `gjsify ship` configuration — the packaging half of {@link AppMetadata}.
369
+ *
370
+ * Everything here has a derived default; a project that already has a
371
+ * `gjsify.flatpak` block usually needs no `gjsify.ship` block at all.
372
+ */
373
+ export interface ConfigDataShip extends AppMetadata {
374
+ /** Reverse-DNS app id. Falls back to `gjsify.flatpak.appId`, then `package.json#name`. */
375
+ appId?: string;
376
+ /**
377
+ * Package name and `bin/` entry. Default: `package.json#name` with the npm
378
+ * scope stripped, lowercased, non-alphanumerics folded to `-`.
379
+ */
380
+ binaryName?: string;
381
+ /** Override the upstream version. Default: `package.json#version`, normalised. */
382
+ version?: string;
383
+ /** Package revision within one upstream version (deb revision / rpm release). Default `'1'`. */
384
+ release?: string;
385
+ /** `Maintainer:` / `Packager:` as `Name <email>`. Default: `package.json#author`. */
386
+ maintainer?: string;
387
+ /** Formats to build when `--target` is not given. Default `['deb', 'rpm']`. */
388
+ targets?: string[];
389
+ /** Output root, relative to the project. Default `'ship'`. */
390
+ outDir?: string;
391
+ /**
392
+ * The built bundle `bin/<name>` executes. Default: `gjsify.main`, then
393
+ * `package.json#main`. Its whole directory is staged into `lib/<name>/`.
394
+ */
395
+ bundle?: string;
396
+ /** Icon file, or a directory of them. Sizes are read from the path or the filename. */
397
+ icon?: string;
398
+ /** A `*.gschema.xml` file, or a directory of them. */
399
+ schemas?: string;
400
+ /** Licence file. Default: the first of LICENSE, LICENSE.md, LICENSE.txt, COPYING. */
401
+ licenseFile?: string;
402
+ /** deb `Section:`. Default: derived from `categories`. */
403
+ section?: string;
404
+ /** rpm `Group:`. Default: derived from `categories`. */
405
+ group?: string;
406
+ /**
407
+ * Minimum GJS the emitted dependency asks for. Default `'1.86'` — what the
408
+ * bundler targets. Lower it only if the bundle genuinely runs on an older
409
+ * GJS: no released Debian ships 1.86, so the default makes a `.deb` that
410
+ * apt refuses on trixie rather than one that installs and then fails to
411
+ * start.
412
+ */
413
+ minGjsVersion?: string;
414
+ /**
415
+ * Runtime dependencies appended to the set derived from the bundle's
416
+ * `gi://` imports — the escape hatch for a namespace the table does not
417
+ * know, and for anything that is not a typelib at all.
418
+ */
419
+ depends?: {
420
+ deb?: string[];
421
+ rpm?: string[];
422
+ };
423
+ /**
424
+ * GI namespace → the package that ships its typelib, filling gaps in the
425
+ * built-in table (`Nautilus-3.0`, a namespace from a private library, …).
426
+ * A row here wins over the built-in one.
427
+ *
428
+ * This is what unblocks a namespace `gjsify ship` does not know; `depends`
429
+ * is for dependencies that are not typelibs at all and deliberately does
430
+ * not silence the unmapped-namespace failure.
431
+ */
432
+ typelibPackages?: Record<string, {
433
+ deb: string;
434
+ rpm: string;
435
+ }>;
436
+ /** Extra payload entries: prefix-relative destination → project-relative source. */
437
+ extraFiles?: Record<string, string>;
438
+ /** Arguments the launcher appends before the user's own. */
439
+ execArgs?: string[];
440
+ }
441
+ /** Flatpak-toolchain config for the `gjsify flatpak` subcommand group. */
442
+ export interface ConfigDataFlatpak extends AppMetadata {
443
+ /** Reverse-DNS app id, e.g. `eu.jumplink.Learn6502`. Defaults to `package.json#name` if it looks like a reverse-DNS id. */
444
+ appId?: string;
445
+ /**
446
+ * Runtime family. Default `'gnome'` — needed at runtime by GJS bundles
447
+ * for GLib/GObject/GIO. `'freedesktop'` is only suitable for non-gjsify
448
+ * CLI tools (no GJS interpreter ships in the Freedesktop runtime).
449
+ */
450
+ runtime?: 'gnome' | 'freedesktop';
451
+ /** Runtime/SDK version, e.g. `'50'` for GNOME or `'24.08'` for Freedesktop. */
452
+ runtimeVersion?: string;
453
+ /**
454
+ * Extra SDK extensions for the manifest, e.g.
455
+ * `['org.freedesktop.Sdk.Extension.llvm17']` for native code needing a specific
456
+ * toolchain. Leave empty for pure gjsify projects: the GNOME runtime already
457
+ * ships GJS + GLib + libsoup and `gjsify build` produces a self-contained bundle
458
+ * that needs no build-time Node.
459
+ */
460
+ sdkExtensions?: string[];
461
+ /** Path components prepended to PATH inside the build sandbox. */
462
+ appendPath?: string[];
463
+ /** The binary name to run (`/app/bin/<command>`). Defaults to `appId`. */
464
+ command?: string;
465
+ /** Finish-args (capabilities). Default depends on `runtime` + `--cli-only`. */
466
+ finishArgs?: string[];
467
+ /** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. `blueprint-compiler` build). */
468
+ extraModules?: unknown[];
469
+ /**
470
+ * Full replacement for the manifest's `modules` array: used verbatim, with
471
+ * neither `extraModules` nor the meson default added. For CLI tools that ship a
472
+ * pre-built bundle and install via shell commands (`buildsystem: simple`).
473
+ */
474
+ modules?: unknown[];
475
+ /** Cleanup glob patterns applied to the final manifest, e.g. `['/include', '/lib/pkgconfig']`. */
476
+ cleanup?: string[];
477
+ /** Source-of-truth lockfile for `gjsify flatpak deps` — `yarn.lock` or `package-lock.json`. */
478
+ lockfile?: string;
479
+ /**
480
+ * GitHub-Actions container image override for `gjsify flatpak ci`. Default is
481
+ * derived from runtime + runtimeVersion, e.g. gnome+50 →
482
+ * `ghcr.io/flathub-infra/flatpak-github-actions:gnome-50`.
483
+ */
484
+ ciContainer?: string;
485
+ /** Branches the generated workflow triggers on. Default `['main']`. */
486
+ ciBranches?: string[];
397
487
  /**
398
488
  * Flathub tracking-repo override for `gjsify flatpak sync-flathub` / `diff`.
399
489
  * Default `flathub/<appId>`; set when the upstream repo deviates.
@@ -0,0 +1,51 @@
1
+ import type { AppMetadata } from '../types/config-data.js';
2
+ /** Everything a renderer needs that is not part of {@link AppMetadata} itself. */
3
+ export interface AppMetadataInputs {
4
+ /** Reverse-DNS application id — the MetaInfo `<id>` and the icon/desktop basename. */
5
+ appId: string;
6
+ /** Human-readable display name. */
7
+ name: string;
8
+ /** The binary to launch (`Exec=`, and the default `<provides><binary>`). */
9
+ command: string;
10
+ /** `'app'` renders a desktop-application component, `'cli'` a console one. */
11
+ kind: 'app' | 'cli';
12
+ metadata: AppMetadata;
13
+ /**
14
+ * Config path the validator names in its hints, e.g. `gjsify.flatpak` or
15
+ * `gjsify.ship`. Both read the same fields, so the hint has to say which
16
+ * block the caller was actually reading.
17
+ */
18
+ configKey: string;
19
+ /**
20
+ * Year for the copyright comment. Passed in rather than read from the
21
+ * clock: this XML is PAYLOAD, so `new Date()` here makes the artifact's
22
+ * bytes depend on when it was packed — a reproducibility hole no test
23
+ * running inside one second can see. Defaults to the current year for
24
+ * callers that write the file to a source tree rather than into a package.
25
+ */
26
+ copyrightYear?: number;
27
+ }
28
+ export interface MissingFieldError {
29
+ field: string;
30
+ hint: string;
31
+ }
32
+ /**
33
+ * Validate that the metadata has the minimum set of fields AppStream needs.
34
+ * Returns the missing fields with actionable hints; an empty list means OK.
35
+ */
36
+ export declare function validateAppMetadata(inputs: AppMetadataInputs): MissingFieldError[];
37
+ /** Render the MetaInfo XML for a desktop application. */
38
+ export declare function renderMetainfoApp(inputs: AppMetadataInputs): string;
39
+ /** Render the MetaInfo XML for a console application. */
40
+ export declare function renderMetainfoCli(inputs: AppMetadataInputs): string;
41
+ /**
42
+ * Render the `.desktop` entry (app kind only).
43
+ *
44
+ * `Exec=` carries a field code when the app declares MIME types, because a
45
+ * desktop entry with `MimeType=` and no `%f`/`%u` is registered as a handler
46
+ * that is then launched with no argument — the file the user double-clicked is
47
+ * dropped on the floor, and `desktop-file-validate` says so. `x-scheme-handler/*`
48
+ * wins over file types: a URL handler must take `%u`.
49
+ */
50
+ export declare function renderDesktopEntry(inputs: AppMetadataInputs): string;
51
+ export declare function escapeXml(value: string): string;