@gjsify/cli 0.42.0 → 0.44.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 (51) hide show
  1. package/dist/cli.gjs.mjs +444 -415
  2. package/lib/actions/build.d.ts +6 -0
  3. package/lib/actions/build.js +66 -9
  4. package/lib/commands/affected-classify.js +7 -5
  5. package/lib/commands/build.js +6 -0
  6. package/lib/commands/flatpak/ci.js +10 -2
  7. package/lib/commands/flatpak/init.js +15 -16
  8. package/lib/commands/flatpak/utils.d.ts +0 -26
  9. package/lib/commands/flatpak/utils.js +4 -29
  10. package/lib/commands/ship.d.ts +2 -0
  11. package/lib/commands/ship.js +358 -104
  12. package/lib/config.js +25 -0
  13. package/lib/types/cli-build-options.d.ts +11 -0
  14. package/lib/types/config-data.d.ts +106 -2
  15. package/lib/utils/flatpak-runtime.d.ts +41 -0
  16. package/lib/utils/flatpak-runtime.js +54 -0
  17. package/lib/utils/gjs-bundle-guard.d.ts +57 -3
  18. package/lib/utils/gjs-bundle-guard.js +122 -13
  19. package/lib/utils/jsx-config.d.ts +45 -0
  20. package/lib/utils/jsx-config.js +98 -0
  21. package/lib/utils/onboard-probe.d.ts +23 -0
  22. package/lib/utils/onboard-probe.js +65 -0
  23. package/lib/utils/ship/deb.d.ts +3 -3
  24. package/lib/utils/ship/deb.js +6 -2
  25. package/lib/utils/ship/depends.d.ts +13 -13
  26. package/lib/utils/ship/depends.js +25 -3
  27. package/lib/utils/ship/discover.js +72 -6
  28. package/lib/utils/ship/flatpak-config.d.ts +66 -0
  29. package/lib/utils/ship/flatpak-config.js +132 -0
  30. package/lib/utils/ship/flatpak.d.ts +45 -0
  31. package/lib/utils/ship/flatpak.js +148 -0
  32. package/lib/utils/ship/formats.d.ts +35 -0
  33. package/lib/utils/ship/formats.js +152 -5
  34. package/lib/utils/ship/mime.d.ts +19 -0
  35. package/lib/utils/ship/mime.js +89 -0
  36. package/lib/utils/ship/payload.d.ts +76 -0
  37. package/lib/utils/ship/payload.js +91 -1
  38. package/lib/utils/ship/plan.d.ts +30 -1
  39. package/lib/utils/ship/plan.js +54 -2
  40. package/lib/utils/ship/rpm.d.ts +3 -3
  41. package/lib/utils/ship/rpm.js +20 -3
  42. package/lib/utils/ship/scripts.d.ts +14 -4
  43. package/lib/utils/ship/scripts.js +26 -8
  44. package/lib/utils/ship/settings.js +20 -0
  45. package/lib/utils/ship/stage-manifest.d.ts +163 -0
  46. package/lib/utils/ship/stage-manifest.js +445 -0
  47. package/lib/utils/ship/stage-writer.d.ts +30 -3
  48. package/lib/utils/ship/stage-writer.js +76 -9
  49. package/lib/utils/ship/types.d.ts +203 -27
  50. package/lib/utils/ship/types.js +5 -0
  51. package/package.json +19 -19
@@ -11,6 +11,43 @@
11
11
  import { DEFAULT_REGISTRY, registryFor } from '@gjsify/npm-registry';
12
12
  import { classifyTrustList, trustUrl } from './trust-registry.js';
13
13
  export const PROBE_RETRY_DELAY_MS = 400;
14
+ /**
15
+ * Does the registry serve a packument for this name?
16
+ *
17
+ * THE ORACLE, and the reason it exists: npm's trust endpoint answers `2xx` with an
18
+ * EMPTY LIST for a name that was never published. `classifyTrustList` reads that as
19
+ * `untrusted`, so `gjsify onboard` planned "already on npm, just needs trust" for a
20
+ * package that did not exist — it trusted the name, reported `0 to publish+trust`
21
+ * and published NOTHING. A bootstrap command reporting its own gap as closed, and
22
+ * it is why the v0.36.0 repair failed twice before anyone looked at the registry.
23
+ *
24
+ * `GET <registry>/<name>` is unauthenticated and needs no OTP, so it answers in the
25
+ * one situation that matters: a `--dry-run` before the maintainer has an OTP to
26
+ * hand, where every trust read is `401` and the plan is otherwise empty.
27
+ *
28
+ * `null` means UNDECIDED (network or an unexpected status) — never "absent". A
29
+ * bootstrap that publishes on a failed probe is worse than one that reports blocked.
30
+ */
31
+ export async function packumentExists(registry, name) {
32
+ const base = registry.endsWith('/') ? registry.slice(0, -1) : registry;
33
+ try {
34
+ // `Accept` narrows the response to the abbreviated packument: the full one
35
+ // for a package like `@gjsify/cli` is megabytes, and existence is all we ask.
36
+ const res = await fetch(`${base}/${name.replace('/', '%2f')}`, {
37
+ method: 'GET',
38
+ headers: { accept: 'application/vnd.npm.install-v1+json' },
39
+ });
40
+ if (res.status === 404)
41
+ return false;
42
+ if (res.ok)
43
+ return true;
44
+ return null;
45
+ }
46
+ catch {
47
+ // A DNS or TLS failure is not evidence of absence.
48
+ return null;
49
+ }
50
+ }
14
51
  /** Default probe concurrency — kept SMALL so a single token never bursts npm. */
15
52
  export const DEFAULT_PROBE_CONCURRENCY = 4;
16
53
  /** Map a classified trust state to the action the onboard sweep should take. */
@@ -48,6 +85,34 @@ export async function probeTrustState(request, ws, ctx, opts = {}) {
48
85
  res = await request('GET', url);
49
86
  state = classifyTrustList(res.status, res.json, classifyOpts);
50
87
  }
88
+ // `untrusted` is the ONE ambiguous answer: the trust endpoint returns `2xx` with
89
+ // an empty list both for "published, nobody trusted it" and for "no such name".
90
+ // Only that case pays for the extra read — `trusted` cannot be a missing package
91
+ // and `unpublished` already has its answer.
92
+ // Two questions, two endpoints. The trust list answers "who may publish this",
93
+ // and onboard also needs "does this name exist at all" — which it can only
94
+ // INFER from the trust list, and infers wrongly in both ambiguous states:
95
+ //
96
+ // untrusted npm serves 2xx and an EMPTY list for a name that was never
97
+ // published — byte-identical to a real package that simply has
98
+ // no trusted publisher configured.
99
+ // auth-required a 401 says nothing whatsoever about existence. It is also
100
+ // where EVERY package lands in a `--dry-run` run before the
101
+ // maintainer has an OTP to hand, which is exactly the moment
102
+ // the plan is being read to decide whether to proceed.
103
+ //
104
+ // The packument answers the question directly and unauthenticated, and a 404
105
+ // there is proof no OTP could change. Only these two states pay for the extra
106
+ // read: `trusted` cannot be a missing package, and `unpublished` already has
107
+ // its answer from the endpoint it just asked.
108
+ if (state === 'untrusted' || state === 'auth-required') {
109
+ const exists = await (opts.exists ?? packumentExists)(registry, ws.name);
110
+ // ONLY a definitive 404 reclassifies. `null` — network failure, unexpected
111
+ // status — leaves the state exactly as it was: a bootstrap that publishes
112
+ // on a failed read is worse than one that reports itself blocked.
113
+ if (exists === false)
114
+ state = 'unpublished';
115
+ }
51
116
  return { ws, registry, url, state, httpStatus: res.status, action: actionForState(state) };
52
117
  }
53
118
  /**
@@ -1,7 +1,7 @@
1
- import type { PayloadEntry } from './payload.js';
2
- import type { ShipSettings } from './types.js';
1
+ import { type PayloadEntry } from './payload.js';
2
+ import type { PackSettings } from './types.js';
3
3
  export interface DebInputs {
4
- settings: ShipSettings;
4
+ settings: PackSettings;
5
5
  /** The staged payload, prefix-relative. */
6
6
  payload: readonly PayloadEntry[];
7
7
  /** Install prefix, e.g. `/usr`. */
@@ -20,8 +20,9 @@ import { createArArchive } from './ar.js';
20
20
  import { formatDebDepend } from './depends.js';
21
21
  import { gzipDeterministic } from './gzip.js';
22
22
  import { renderDebScripts } from './scripts.js';
23
+ import { readPayloadFacts } from './payload.js';
23
24
  export async function buildDeb(inputs) {
24
- const { settings, payload, mtime } = inputs;
25
+ const { payload, mtime } = inputs;
25
26
  const prefix = inputs.prefix.replace(/^\/+/, '');
26
27
  const named = payload.map((entry) => ({ ...entry, tarPath: `./${posix.join(prefix, entry.path)}` }));
27
28
  const directories = ancestorDirectories(named.map((entry) => entry.tarPath));
@@ -44,7 +45,10 @@ export async function buildDeb(inputs) {
44
45
  { name: './control', body: control, mode: 0o644, mtime },
45
46
  { name: './md5sums', body: md5sums, mode: 0o644, mtime },
46
47
  ];
47
- const maintainerScripts = renderDebScripts(settings, inputs.prefix);
48
+ // From the PAYLOAD, not the settings: the maintainer scripts refresh exactly the
49
+ // directories this package wrote into, and since ADR 0024 § A2 this packer also runs on a
50
+ // host that has the staged tree and no project to ask.
51
+ const maintainerScripts = renderDebScripts(readPayloadFacts(payload), inputs.prefix);
48
52
  for (const [name, body] of Object.entries(maintainerScripts)) {
49
53
  // dpkg refuses a maintainer script outside 0555–0775 with the mode in
50
54
  // the error, so this is 0755 rather than "whatever the source had".
@@ -1,4 +1,4 @@
1
- import type { FormatId } from './types.js';
1
+ import type { DistroFormatId } from './types.js';
2
2
  /**
3
3
  * The GJS the bundles target (root AGENTS.md § Constraints: GJS 1.86 /
4
4
  * SpiderMonkey 140, Rolldown `firefox140`). A `--app gjs` bundle can carry
@@ -14,25 +14,25 @@ import type { FormatId } from './types.js';
14
14
  * is how a project whose bundle really does run on older GJS lowers it.
15
15
  */
16
16
  export declare const DEFAULT_GJS_FLOOR = "1.86";
17
- /**
18
- * Warn when the derived floor cannot be satisfied by a released Debian.
19
- * Returns the warning lines; empty when the floor is fine.
20
- */
21
- export declare function warnAboutGjsFloor(format: FormatId, floor: string): string[];
17
+ export declare function warnAboutGjsFloor(format: DistroFormatId, floor: string): string[];
22
18
  export interface DependsInputs {
23
19
  /** GI namespaces the bundle imports, in `Ns-Version` spelling. */
24
20
  namespaces: readonly string[];
25
- /** `'app'` installs a desktop entry and icons; `'cli'` does not. */
26
- kind: 'app' | 'cli';
21
+ /**
22
+ * Whether the payload installs into `share/icons/hicolor/`.
23
+ *
24
+ * Was `kind: 'app' | 'cli'`, which is a proxy for the question and not the question:
25
+ * `hicolor-icon-theme` is depended on because it OWNS that directory, so an app that
26
+ * installs no icon used to declare a dependency it never touched. Derived from the payload
27
+ * (`readPayloadFacts`), which is also the only thing the packing host has (ADR 0024 § A2).
28
+ */
29
+ hasIcons: boolean;
27
30
  /** Whether the payload installs GSettings schemas. */
28
31
  hasSchemas: boolean;
29
32
  /** User-supplied additions for this format. */
30
33
  extra: readonly string[];
31
34
  /** Project-supplied table rows, filling gaps in {@link TYPELIB_PACKAGES}. */
32
- typelibPackages?: Record<string, {
33
- deb: string;
34
- rpm: string;
35
- }>;
35
+ typelibPackages?: Record<string, Record<DistroFormatId, string>>;
36
36
  /**
37
37
  * Paths of typelib files the payload carries itself. The namespaces they cover need no distro
38
38
  * dependency — and are derived from these filenames, never declared separately.
@@ -46,7 +46,7 @@ export interface DependsInputs {
46
46
  *
47
47
  * @throws when a namespace has no entry in the table — see the module header.
48
48
  */
49
- export declare function deriveDepends(format: FormatId, inputs: DependsInputs): string[];
49
+ export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs): string[];
50
50
  /** One parsed dependency. `relation`/`version` are absent for an unversioned one. */
51
51
  export interface ParsedDepend {
52
52
  name: string;
@@ -35,14 +35,34 @@
35
35
  * is how a project whose bundle really does run on older GJS lowers it.
36
36
  */
37
37
  export const DEFAULT_GJS_FLOOR = '1.86';
38
+ /**
39
+ * The package providing `glib-compile-schemas`, per format.
40
+ *
41
+ * A record, not `format === 'deb' ? … : …`. The ternary reads as a two-way
42
+ * choice and is really "deb, or ELSE rpm's package name" — measured on a third
43
+ * format, `dmg` got `glib2`, at exit 0, in the `Depends:` of a package that has
44
+ * no such thing. Adding a `DistroFormatId` now fails to build here instead.
45
+ */
46
+ const SCHEMA_COMPILER_PACKAGE = { deb: 'libglib2.0-bin', rpm: 'glib2' };
38
47
  /** Debian's `gjs` versions per suite, as measured 2026-08-15 — see {@link warnAboutGjsFloor}. */
39
48
  const DEBIAN_GJS = 'trixie ships 1.82.3 and forky 1.88.1; 1.84 and 1.86 were skipped';
40
49
  /**
41
50
  * Warn when the derived floor cannot be satisfied by a released Debian.
42
51
  * Returns the warning lines; empty when the floor is fine.
43
52
  */
53
+ /**
54
+ * Which formats this Debian-suite warning is ABOUT.
55
+ *
56
+ * A `Record<DistroFormatId, …>` and not `format !== 'deb'`: the negative form answers
57
+ * for every format that will ever exist, and answers "stay quiet" — so a third
58
+ * format would inherit silence about a floor nobody has checked for its distro,
59
+ * with no compile error. As a record, adding a `DistroFormatId` fails to build until
60
+ * someone decides. `rpm` is false because Fedora ships a current GJS, which is a
61
+ * measured fact about Fedora, not a default.
62
+ */
63
+ const GJS_FLOOR_IS_DEBIAN_NEWS = { deb: true, rpm: false };
44
64
  export function warnAboutGjsFloor(format, floor) {
45
- if (format !== 'deb')
65
+ if (!GJS_FLOOR_IS_DEBIAN_NEWS[format])
46
66
  return [];
47
67
  // Only a floor forky ACTUALLY satisfies is quiet. The first cut tested
48
68
  // `>= 1.88.1` and therefore went silent for 1.90 and 2.0 as well — the
@@ -171,7 +191,9 @@ export function deriveDepends(format, inputs) {
171
191
  'have to. (`gjsify.ship.depends.<format>` is for dependencies that are not typelibs at all — it ' +
172
192
  'appends, and deliberately does not silence this.)');
173
193
  }
174
- if (inputs.kind === 'app')
194
+ // The package that owns `/usr/share/icons/hicolor` — so this follows the icons, not the
195
+ // app/cli distinction that used to stand in for them.
196
+ if (inputs.hasIcons)
175
197
  out.push('hicolor-icon-theme');
176
198
  // The package that ships `glib-compile-schemas`, NOT `gsettings-desktop-schemas`
177
199
  // (which ships GNOME's own `org.gnome.desktop.*` schemas and has nothing to
@@ -182,7 +204,7 @@ export function deriveDepends(format, inputs) {
182
204
  // `Gio.Settings.new()` aborts the app — an install that succeeds and an app
183
205
  // that does not start.
184
206
  if (inputs.hasSchemas)
185
- out.push(format === 'deb' ? 'libglib2.0-bin' : 'glib2');
207
+ out.push(SCHEMA_COMPILER_PACKAGE[format]);
186
208
  out.push(...inputs.extra);
187
209
  // Set-dedupe keeps first-insertion order, so `gjs` stays first and the
188
210
  // list is stable across runs.
@@ -4,8 +4,8 @@
4
4
  // so `settings.ts` and `plan.ts` can stay pure. Every probe has a configured
5
5
  // override; the probes exist so a project that follows the GNOME convention
6
6
  // (`data/icons/hicolor/…`, `data/*.gschema.xml`) needs no configuration at all.
7
- import { existsSync, readdirSync, statSync } from 'node:fs';
8
- import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
7
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
8
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
9
9
  import { LICENSE_FILE_NAMES } from './settings.js';
10
10
  const ICON_EXTENSIONS = ['.svg', '.png'];
11
11
  export function discoverPayload(input) {
@@ -189,6 +189,34 @@ function discoverSchemas(projectDir, configured) {
189
189
  }
190
190
  return [];
191
191
  }
192
+ /**
193
+ * The licence text this package ships — searched UP the tree, not just beside
194
+ * `package.json`.
195
+ *
196
+ * A monorepo package does not carry its own copy. `packages/infra/cli`
197
+ * declares `"license": "MIT"` and the text lives one `LICENSE` up at the
198
+ * repository root, shared by sixty-odd packages; searching `projectDir` alone
199
+ * found nothing, `planOverlay` turned "nothing" into an empty overlay, and the
200
+ * `.deb` shipped without the `/usr/share/doc/<pkg>/copyright` that Debian
201
+ * Policy § 12.5 requires. Nothing downstream noticed: `dpkg-deb --info` prints
202
+ * a clean control file and `dpkg -i` installs it. It took the first leg that
203
+ * ran a real `lintian` to say so.
204
+ *
205
+ * WHERE THE SEARCH STOPS is the whole design. It climbs to the first ancestor
206
+ * that is a project root — one holding `.git`, or a `package.json` declaring
207
+ * `workspaces` — and searches that directory too, then stops. Above that line
208
+ * a `LICENSE` belongs to some unrelated tree that happens to be a parent
209
+ * directory (`~/Projekte/LICENSE` is not yours), and packaging its text would
210
+ * make a specific false legal claim. When NO marker is found the search is the
211
+ * project directory alone: without a marker there is no way to tell where the
212
+ * project ends, and guessing wrong is the same false claim.
213
+ *
214
+ * What this cannot check is whether the inherited text is the RIGHT licence
215
+ * for this package — an Apache-2.0 package inside an MIT monorepo would
216
+ * inherit the wrong one. Reading an SPDX identifier out of licence prose is
217
+ * not something to guess at, so the caller PRINTS the inherited path instead
218
+ * (see `commands/ship.ts`), and `gjsify.ship.licenseFile` overrides it.
219
+ */
192
220
  function discoverLicense(projectDir, configured) {
193
221
  if (configured !== undefined) {
194
222
  const target = resolve(projectDir, configured);
@@ -197,13 +225,51 @@ function discoverLicense(projectDir, configured) {
197
225
  }
198
226
  return target;
199
227
  }
200
- for (const name of LICENSE_FILE_NAMES) {
201
- const target = join(projectDir, name);
202
- if (existsSync(target))
203
- return target;
228
+ for (const dir of licenseSearchDirs(projectDir)) {
229
+ for (const name of LICENSE_FILE_NAMES) {
230
+ const target = join(dir, name);
231
+ if (existsSync(target))
232
+ return target;
233
+ }
204
234
  }
205
235
  return undefined;
206
236
  }
237
+ /** `projectDir` first, then each ancestor up to and including the project root. */
238
+ function licenseSearchDirs(projectDir) {
239
+ const dirs = [];
240
+ for (let current = projectDir;;) {
241
+ dirs.push(current);
242
+ if (isProjectRoot(current))
243
+ return dirs;
244
+ const parent = dirname(current);
245
+ if (parent === current)
246
+ return [projectDir];
247
+ current = parent;
248
+ }
249
+ }
250
+ /**
251
+ * Is this the top of the tree the project belongs to?
252
+ *
253
+ * `.git` is tested with `existsSync` rather than `statSync().isDirectory()`
254
+ * because in a git WORKTREE it is a file, and this repository is developed in
255
+ * worktrees.
256
+ */
257
+ function isProjectRoot(dir) {
258
+ if (existsSync(join(dir, '.git')))
259
+ return true;
260
+ const manifest = join(dir, 'package.json');
261
+ if (!existsSync(manifest))
262
+ return false;
263
+ try {
264
+ return JSON.parse(readFileSync(manifest, 'utf-8')).workspaces !== undefined;
265
+ }
266
+ catch {
267
+ // A malformed `package.json` in some ancestor is not this command's
268
+ // business, and it must not turn a licence search into a ship failure.
269
+ // Not a root, then: the climb continues and `.git` still stops it.
270
+ return false;
271
+ }
272
+ }
207
273
  /**
208
274
  * Every regular file under `dir`, as POSIX-separated relative paths.
209
275
  *
@@ -0,0 +1,66 @@
1
+ import type { ConfigDataFlatpak, ConfigDataShip, ShipFlatpakOptions } from '../../types/config-data.js';
2
+ import type { ShipFlatpakSettings } from './types.js';
3
+ /**
4
+ * The release that stops reading `gjsify.flatpak` for the keys below.
5
+ *
6
+ * A major, not the next minor: the block is in published releases and in
7
+ * downstream `package.json`s that this repository does not control, and a
8
+ * window measured in weeks is a window nobody notices before it closes.
9
+ */
10
+ export declare const LEGACY_FLATPAK_KEYS_REMOVED_IN = "1.0.0";
11
+ /** The keys whose new home is `gjsify.ship.flatpak`, in the order the warning lists them. */
12
+ export declare const MIGRATED_FLATPAK_KEYS: readonly ["runtime", "runtimeVersion", "sdkExtensions", "appendPath", "finishArgs", "cleanup"];
13
+ export type MigratedFlatpakKey = (typeof MIGRATED_FLATPAK_KEYS)[number];
14
+ /** The six keys as a reader sees them, before any default is applied. */
15
+ export type MigratedFlatpakValues = {
16
+ [K in MigratedFlatpakKey]: ConfigDataFlatpak[K];
17
+ };
18
+ export interface PickedFlatpakBuildKeys {
19
+ values: MigratedFlatpakValues;
20
+ /** Keys that came from `gjsify.flatpak`, in {@link MIGRATED_FLATPAK_KEYS} order. */
21
+ fromLegacy: MigratedFlatpakKey[];
22
+ }
23
+ /**
24
+ * `gjsify.ship.flatpak.<key>` when set, else `gjsify.flatpak.<key>` — per KEY,
25
+ * noting which side answered.
26
+ *
27
+ * Per-key and not per-block: a project migrating one key at a time must not
28
+ * lose the five it has not moved yet. That is also what makes the warning
29
+ * actionable — it names the keys still coming from the old block, so the reader
30
+ * knows what to edit rather than being told a block is deprecated.
31
+ *
32
+ * A LOOP over {@link MIGRATED_FLATPAK_KEYS}, not six hand-written picks. Written
33
+ * out, the list was the one unbound copy of this vocabulary: a key added to it
34
+ * with no `pick` call behind it compiled fine, went missing from the window, and
35
+ * the only test that mentioned the constant compared it to a literal copy of
36
+ * itself. Reading the keys off the constant is what makes "this list IS the
37
+ * window" true rather than asserted.
38
+ *
39
+ * NO default is applied here. The defaults differ per reader — `ship` derives
40
+ * `finishArgs` from `kind`, `flatpak init` also merges `--sdk-extension` flags —
41
+ * and a default applied here would be a third answer neither of them chose.
42
+ */
43
+ export declare function pickFlatpakBuildKeys(preferred: ShipFlatpakOptions | undefined, legacy: ConfigDataFlatpak): PickedFlatpakBuildKeys;
44
+ /** The branch a `ship` Flatpak is exported under when the project names none. */
45
+ export declare const DEFAULT_SHIP_FLATPAK_BRANCH = "stable";
46
+ export interface ShipFlatpakInput {
47
+ ship: ConfigDataShip;
48
+ flatpak: ConfigDataFlatpak;
49
+ /** Decides the finish-args default: a GUI app needs a display, a CLI does not. */
50
+ kind: 'app' | 'cli';
51
+ }
52
+ export interface ResolvedShipFlatpak {
53
+ settings: ShipFlatpakSettings;
54
+ warnings: string[];
55
+ }
56
+ /**
57
+ * Resolve the Flatpak half of `gjsify.ship`, reading the legacy block where the
58
+ * new one is silent, and applying `ship`'s own defaults.
59
+ *
60
+ * The per-key fallback itself is {@link pickFlatpakBuildKeys}, shared with
61
+ * `gjsify flatpak init`/`ci`; what is `ship`-specific and stays here is the
62
+ * defaulting — `branch`, the `kind`-dependent finish-args, the derived
63
+ * `appendPath` — plus the warning, which only `ship` prints because only `ship`
64
+ * has somewhere else to point at.
65
+ */
66
+ export declare function resolveShipFlatpakSettings(input: ShipFlatpakInput): ResolvedShipFlatpak;
@@ -0,0 +1,132 @@
1
+ // The deprecation window on the `gjsify.flatpak` build keys (ADR 0024 § 8).
2
+ //
3
+ // § 8 names the cost of the migration and does not let it be skipped:
4
+ // "`gjsify.flatpak` is a published config contract, so the keys move with a
5
+ // deprecation window in which both spellings resolve and the old one warns."
6
+ // This file is that window.
7
+ //
8
+ // WHAT MOVED and what did not — the table is `docs/ship-formats.md`, and the
9
+ // short form is: the six BUILD keys moved and warn from the old spelling; the
10
+ // `AppMetadata` half is a designed ALIAS and is NOT deprecated (both blocks
11
+ // describe the same app, and warning on it would print for every project that
12
+ // has a `gjsify.flatpak` block at all); and the TOOLCHAIN keys are untouched
13
+ // because `gjsify flatpak <sub>` has not moved, so deprecating them would warn
14
+ // on commands with nowhere else to read from.
15
+ //
16
+ // A WINDOW HAS TWO SIDES, and the first cut only built one. The six build keys
17
+ // are read by `gjsify flatpak init` and `flatpak ci` as well, and those
18
+ // commands have NOT moved — so a project that did what this file's warning told
19
+ // it to do, and moved them, silently lost them there: `flatpak init` fell back
20
+ // to its own defaults and wrote a manifest against a different
21
+ // `org.gnome.Platform` version with different finish-args, committed, at exit 0.
22
+ // That is why {@link pickFlatpakBuildKeys} is exported and BOTH command groups
23
+ // go through it. "Both spellings resolve" is now true of every reader of these
24
+ // keys, which is what makes the advice safe to follow.
25
+ //
26
+ // WHY A NAMED VERSION, IN ONE PLACE. The precedent is
27
+ // `utils/normalize-bundler-options.ts`, the `esbuild` → `bundler` shim, and it
28
+ // also carries the precedent's DEFECT: its warning text names 0.5.0, the tree is
29
+ // far past it, and its header now has to say "removing the shim means fixing
30
+ // that string too". So the version here is a constant the message reads, and the
31
+ // spec asserts the message names it.
32
+ import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, deriveAppendPath, resolveRuntime, } from '../flatpak-runtime.js';
33
+ /**
34
+ * The release that stops reading `gjsify.flatpak` for the keys below.
35
+ *
36
+ * A major, not the next minor: the block is in published releases and in
37
+ * downstream `package.json`s that this repository does not control, and a
38
+ * window measured in weeks is a window nobody notices before it closes.
39
+ */
40
+ export const LEGACY_FLATPAK_KEYS_REMOVED_IN = '1.0.0';
41
+ /** The keys whose new home is `gjsify.ship.flatpak`, in the order the warning lists them. */
42
+ export const MIGRATED_FLATPAK_KEYS = [
43
+ 'runtime',
44
+ 'runtimeVersion',
45
+ 'sdkExtensions',
46
+ 'appendPath',
47
+ 'finishArgs',
48
+ 'cleanup',
49
+ ];
50
+ /**
51
+ * `gjsify.ship.flatpak.<key>` when set, else `gjsify.flatpak.<key>` — per KEY,
52
+ * noting which side answered.
53
+ *
54
+ * Per-key and not per-block: a project migrating one key at a time must not
55
+ * lose the five it has not moved yet. That is also what makes the warning
56
+ * actionable — it names the keys still coming from the old block, so the reader
57
+ * knows what to edit rather than being told a block is deprecated.
58
+ *
59
+ * A LOOP over {@link MIGRATED_FLATPAK_KEYS}, not six hand-written picks. Written
60
+ * out, the list was the one unbound copy of this vocabulary: a key added to it
61
+ * with no `pick` call behind it compiled fine, went missing from the window, and
62
+ * the only test that mentioned the constant compared it to a literal copy of
63
+ * itself. Reading the keys off the constant is what makes "this list IS the
64
+ * window" true rather than asserted.
65
+ *
66
+ * NO default is applied here. The defaults differ per reader — `ship` derives
67
+ * `finishArgs` from `kind`, `flatpak init` also merges `--sdk-extension` flags —
68
+ * and a default applied here would be a third answer neither of them chose.
69
+ */
70
+ export function pickFlatpakBuildKeys(preferred, legacy) {
71
+ const ship = preferred ?? {};
72
+ // Assembled through an index signature because the value type varies per
73
+ // key; the cast is on the finished object, where every key is present.
74
+ const values = {};
75
+ const fromLegacy = [];
76
+ for (const key of MIGRATED_FLATPAK_KEYS) {
77
+ const own = ship[key];
78
+ if (own !== undefined) {
79
+ values[key] = own;
80
+ continue;
81
+ }
82
+ const inherited = legacy[key];
83
+ if (inherited !== undefined)
84
+ fromLegacy.push(key);
85
+ values[key] = inherited;
86
+ }
87
+ return { values: values, fromLegacy };
88
+ }
89
+ /** The branch a `ship` Flatpak is exported under when the project names none. */
90
+ export const DEFAULT_SHIP_FLATPAK_BRANCH = 'stable';
91
+ /**
92
+ * Resolve the Flatpak half of `gjsify.ship`, reading the legacy block where the
93
+ * new one is silent, and applying `ship`'s own defaults.
94
+ *
95
+ * The per-key fallback itself is {@link pickFlatpakBuildKeys}, shared with
96
+ * `gjsify flatpak init`/`ci`; what is `ship`-specific and stays here is the
97
+ * defaulting — `branch`, the `kind`-dependent finish-args, the derived
98
+ * `appendPath` — plus the warning, which only `ship` prints because only `ship`
99
+ * has somewhere else to point at.
100
+ */
101
+ export function resolveShipFlatpakSettings(input) {
102
+ const shipFlatpak = input.ship.flatpak ?? {};
103
+ const { values, fromLegacy } = pickFlatpakBuildKeys(shipFlatpak, input.flatpak);
104
+ const resolved = resolveRuntime(values, {});
105
+ const extensions = values.sdkExtensions ?? [];
106
+ const { appendPath, finishArgs, cleanup } = values;
107
+ const settings = {
108
+ runtime: resolved.runtimeId,
109
+ runtimeVersion: resolved.runtimeVersion,
110
+ sdk: resolved.sdk,
111
+ branch: shipFlatpak.branch ?? DEFAULT_SHIP_FLATPAK_BRANCH,
112
+ sdkExtensions: [...extensions],
113
+ // Derived from the extensions when nothing was declared, and only then:
114
+ // an extension whose `/usr/lib/sdk/<x>/bin` is not on PATH inside the
115
+ // sandbox is an extension the build cannot use, which looks like the
116
+ // extension not being installed.
117
+ appendPath: appendPath ?? (extensions.length > 0 ? deriveAppendPath(extensions) : []),
118
+ finishArgs: finishArgs ?? (input.kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS),
119
+ cleanup: cleanup ?? [],
120
+ };
121
+ return { settings, warnings: fromLegacy.length === 0 ? [] : [deprecationWarning(fromLegacy)] };
122
+ }
123
+ /** One line, naming every key still read from the old block and where each goes. */
124
+ function deprecationWarning(keys) {
125
+ const list = keys.map((key) => `gjsify.flatpak.${key}`).join(', ');
126
+ const moved = keys.map((key) => `gjsify.ship.flatpak.${key}`).join(', ');
127
+ return (`DEPRECATION: ${list} ${keys.length > 1 ? 'are' : 'is'} read by \`gjsify ship\` for compatibility and ` +
128
+ `will be removed in ${LEGACY_FLATPAK_KEYS_REMOVED_IN}. Move ${keys.length > 1 ? 'them' : 'it'} to ` +
129
+ `${moved} — both spellings resolve until then, and \`gjsify.ship.flatpak\` wins where both are set. ` +
130
+ 'The app METADATA in `gjsify.flatpak` (name, summary, developer, categories, licence) is NOT ' +
131
+ 'deprecated: both blocks describe the same application and either may carry it (ADR 0024 § 8).');
132
+ }
@@ -0,0 +1,45 @@
1
+ import type { PackSettings } from './types.js';
2
+ export interface FlatpakManifestInput {
3
+ settings: PackSettings;
4
+ /** Absolute path of the staged payload — the tree `readStage` validated. */
5
+ stageDir: string;
6
+ /** Absolute path of this format's rendered overlay (the licence file). */
7
+ overlayDir: string;
8
+ /** The stage's own closure file, which is not payload and must not be copied into `/app`. */
9
+ stageManifestFile: string;
10
+ }
11
+ /**
12
+ * The generated Flatpak manifest, as a plain object.
13
+ *
14
+ * Pure, so every field is unit-testable without flatpak tooling — and the
15
+ * suite that needs that is the one running in CI, where there is none.
16
+ *
17
+ * The app id is what makes this legal Flatpak: every file flatpak EXPORTS out
18
+ * of `/app/share` has to be named after it, and `planStage` already enforces
19
+ * exactly that for its own reasons — the desktop entry, the metainfo, the icon,
20
+ * the GSettings schema and the shared-mime-info document are all
21
+ * `<appId>[.…].<ext>`, because `share/glib-2.0/schemas/` and
22
+ * `share/mime/packages/` are directories shared between packages. Two
23
+ * independent constraints, one naming rule.
24
+ */
25
+ export declare function renderShipFlatpakManifest(input: FlatpakManifestInput): Record<string, unknown>;
26
+ export interface FlatpakPackInput extends FlatpakManifestInput {
27
+ /** Working root for the manifest, the build dir, the repo and the builder's state. */
28
+ workDir: string;
29
+ /** Absolute path the finished `.flatpak` is written to. */
30
+ target: string;
31
+ /** Flatpak's spelling of the architecture, e.g. `x86_64`. */
32
+ archLabel: string;
33
+ verbose: boolean;
34
+ }
35
+ /**
36
+ * Build the app and export a single-file bundle at `input.target`.
37
+ *
38
+ * The only packer in this tree that execs anything, and the descriptor says so
39
+ * (`FORMATS.flatpak.host.requiredTools`). ADR 0024 § A3 is explicit that this
40
+ * costs the independent oracle the hand-written deb/rpm writers bought: what
41
+ * pays it back here is that a `.flatpak` is an OSTree static delta, so
42
+ * `flatpak build-import-bundle` + `ostree ls -R` read the artifact back with
43
+ * code this tree did not write.
44
+ */
45
+ export declare function buildFlatpakBundle(input: FlatpakPackInput): Promise<void>;