@gjsify/cli 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/cli.gjs.mjs +453 -425
  2. package/lib/actions/build.js +2 -1
  3. package/lib/commands/affected-classify.js +7 -5
  4. package/lib/commands/build.js +6 -0
  5. package/lib/commands/flatpak/ci.js +10 -2
  6. package/lib/commands/flatpak/init.js +15 -16
  7. package/lib/commands/flatpak/utils.d.ts +0 -26
  8. package/lib/commands/flatpak/utils.js +4 -29
  9. package/lib/commands/gettext.d.ts +2 -1
  10. package/lib/commands/gettext.js +105 -47
  11. package/lib/commands/onboard.d.ts +7 -0
  12. package/lib/commands/onboard.js +349 -96
  13. package/lib/commands/ship.d.ts +3 -0
  14. package/lib/commands/ship.js +911 -70
  15. package/lib/commands/trust.d.ts +6 -0
  16. package/lib/commands/trust.js +1 -1
  17. package/lib/config.js +25 -0
  18. package/lib/types/cli-build-options.d.ts +11 -0
  19. package/lib/types/config-data.d.ts +137 -3
  20. package/lib/utils/flatpak-runtime.d.ts +41 -0
  21. package/lib/utils/flatpak-runtime.js +54 -0
  22. package/lib/utils/msgfmt-merge.d.ts +35 -0
  23. package/lib/utils/msgfmt-merge.js +102 -0
  24. package/lib/utils/npm-otp.d.ts +27 -2
  25. package/lib/utils/npm-otp.js +82 -7
  26. package/lib/utils/onboard-discovery.d.ts +68 -0
  27. package/lib/utils/onboard-discovery.js +178 -0
  28. package/lib/utils/onboard-probe.d.ts +118 -1
  29. package/lib/utils/onboard-probe.js +153 -4
  30. package/lib/utils/prompt-output.d.ts +12 -0
  31. package/lib/utils/prompt-output.js +54 -0
  32. package/lib/utils/prompt.js +22 -2
  33. package/lib/utils/publishable-packages.d.ts +1 -1
  34. package/lib/utils/publishable-packages.js +1 -0
  35. package/lib/utils/resolve-npm-package.d.ts +56 -0
  36. package/lib/utils/resolve-npm-package.js +69 -0
  37. package/lib/utils/ship/app-runtime.d.ts +214 -0
  38. package/lib/utils/ship/app-runtime.js +429 -0
  39. package/lib/utils/ship/depends.d.ts +54 -7
  40. package/lib/utils/ship/depends.js +120 -6
  41. package/lib/utils/ship/discover.js +18 -1
  42. package/lib/utils/ship/dmg.d.ts +71 -0
  43. package/lib/utils/ship/dmg.js +181 -0
  44. package/lib/utils/ship/flatpak-config.d.ts +66 -0
  45. package/lib/utils/ship/flatpak-config.js +132 -0
  46. package/lib/utils/ship/flatpak.d.ts +45 -0
  47. package/lib/utils/ship/flatpak.js +148 -0
  48. package/lib/utils/ship/formats.d.ts +116 -3
  49. package/lib/utils/ship/formats.js +869 -11
  50. package/lib/utils/ship/launcher.d.ts +13 -3
  51. package/lib/utils/ship/launcher.js +362 -17
  52. package/lib/utils/ship/layout.d.ts +268 -0
  53. package/lib/utils/ship/layout.js +351 -0
  54. package/lib/utils/ship/localize-metadata.d.ts +22 -0
  55. package/lib/utils/ship/localize-metadata.js +126 -0
  56. package/lib/utils/ship/mime.js +1 -9
  57. package/lib/utils/ship/msi.d.ts +126 -0
  58. package/lib/utils/ship/msi.js +417 -0
  59. package/lib/utils/ship/node-runtime.d.ts +71 -0
  60. package/lib/utils/ship/node-runtime.js +122 -0
  61. package/lib/utils/ship/payload.d.ts +156 -5
  62. package/lib/utils/ship/payload.js +580 -14
  63. package/lib/utils/ship/plan.d.ts +21 -0
  64. package/lib/utils/ship/plan.js +20 -7
  65. package/lib/utils/ship/plist.d.ts +58 -0
  66. package/lib/utils/ship/plist.js +198 -0
  67. package/lib/utils/ship/rpm.js +9 -3
  68. package/lib/utils/ship/schemas.d.ts +61 -0
  69. package/lib/utils/ship/schemas.js +118 -0
  70. package/lib/utils/ship/scripts.js +7 -3
  71. package/lib/utils/ship/settings.d.ts +19 -1
  72. package/lib/utils/ship/settings.js +19 -1
  73. package/lib/utils/ship/share-dirs.d.ts +23 -0
  74. package/lib/utils/ship/share-dirs.js +46 -0
  75. package/lib/utils/ship/signing.d.ts +195 -0
  76. package/lib/utils/ship/signing.js +317 -0
  77. package/lib/utils/ship/stage-manifest.d.ts +50 -13
  78. package/lib/utils/ship/stage-manifest.js +137 -14
  79. package/lib/utils/ship/stage-writer.d.ts +48 -0
  80. package/lib/utils/ship/stage-writer.js +71 -1
  81. package/lib/utils/ship/types.d.ts +264 -16
  82. package/lib/utils/ship/xml.d.ts +7 -0
  83. package/lib/utils/ship/xml.js +25 -0
  84. package/lib/utils/ship/zip.d.ts +69 -0
  85. package/lib/utils/ship/zip.js +245 -0
  86. package/lib/utils/trust-registry.js +5 -0
  87. package/package.json +19 -19
@@ -42,3 +42,51 @@ export declare function writeStage(root: string, files: readonly StagedFile[]):
42
42
  * nothing.
43
43
  */
44
44
  export declare function readStage(roots: readonly string[], plan: readonly StagePlanEntry[]): PayloadEntry[];
45
+ /**
46
+ * Materialise a PAYLOAD — bytes already in hand — under `root`, replacing it.
47
+ *
48
+ * The twin of {@link writeStage}, and the difference is what each takes.
49
+ * `writeStage` writes a PLAN, whose sources are paths on the assembling host or
50
+ * strings; this writes what {@link readStage} handed back, which is the only shape
51
+ * available on a host that has the stage and nothing else.
52
+ *
53
+ * It exists for the one artifact in `FORMATS` that is a DIRECTORY: a `<App>.app`
54
+ * is not a container around the payload, it IS the payload plus the format's own
55
+ * overlay, laid out on disk. Writing it through this function rather than by
56
+ * copying the stage directory keeps the same two properties every other packer
57
+ * has — the modes come from the plan (`readStage` has already applied them, and
58
+ * the artifact upload that flattens them cannot reach in between), and the
59
+ * stage's own `.gjsify-ship-stage.json` stays out, because it was never in the
60
+ * payload.
61
+ *
62
+ * `root` is WIPED first, exactly as `writeStage` wipes: a `.app` left over from a
63
+ * previous run with a file the current payload no longer names would otherwise be
64
+ * signed, zipped and shipped.
65
+ *
66
+ * `stripPrefix` is what makes it the ARTIFACT rather than a directory holding
67
+ * one, and leaving it out produced exactly that: every staged darwin path already
68
+ * begins with `<App>.app/` — that is what `Layout.root` means — so writing them
69
+ * verbatim under an artifact called `<App>.app` gave
70
+ * `out/Ship Demo.app/Ship Demo.app/Contents/…`. The Finder shows the outer
71
+ * directory as a plain folder, so the failure is a bundle that is not one, and it
72
+ * is invisible to every reader that walks the tree looking for `Contents/`.
73
+ * Measured on the WIP this replaces; the discriminator is the depth assertion in
74
+ * `tests/e2e/ship-macos`.
75
+ *
76
+ * A path OUTSIDE `stripPrefix` throws rather than being written somewhere near
77
+ * the artifact. The alternative — pass it through — would put a file beside the
78
+ * bundle instead of inside it, which is the shape ADR 0024 § A4 records
79
+ * `codesign` refusing, and it would be silent.
80
+ */
81
+ export declare function writePayload(root: string, payload: readonly PayloadEntry[], stripPrefix: string): void;
82
+ /**
83
+ * Total bytes of the regular files under `root`.
84
+ *
85
+ * What `packOne` reports for a `FormatDescriptor.artifactKind: 'directory'`.
86
+ * `statSync` on a directory answers the directory ENTRY's size — 4096 on ext4 —
87
+ * so a `.app` carrying a 20 MiB bundle would be printed as "4096 bytes", which is
88
+ * not a rounding error but a different number. Directory entries themselves are
89
+ * deliberately not counted: what a user wants to know is how much payload they
90
+ * are about to move.
91
+ */
92
+ export declare function directorySize(root: string): number;
@@ -12,7 +12,7 @@
12
12
  // A stage now arrives from another machine, so "the tree and the plan disagree"
13
13
  // stopped being impossible and became a transfer failure — and every way of
14
14
  // papering over it produces an artifact that installs.
15
- import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
15
+ import { chmodSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
16
16
  import { dirname, join, sep } from 'node:path';
17
17
  import { listFilesRecursive } from './discover.js';
18
18
  import { STAGE_MANIFEST_FILE } from './stage-manifest.js';
@@ -114,3 +114,73 @@ export function readStage(roots, plan) {
114
114
  }
115
115
  return [...byPath.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
116
116
  }
117
+ /**
118
+ * Materialise a PAYLOAD — bytes already in hand — under `root`, replacing it.
119
+ *
120
+ * The twin of {@link writeStage}, and the difference is what each takes.
121
+ * `writeStage` writes a PLAN, whose sources are paths on the assembling host or
122
+ * strings; this writes what {@link readStage} handed back, which is the only shape
123
+ * available on a host that has the stage and nothing else.
124
+ *
125
+ * It exists for the one artifact in `FORMATS` that is a DIRECTORY: a `<App>.app`
126
+ * is not a container around the payload, it IS the payload plus the format's own
127
+ * overlay, laid out on disk. Writing it through this function rather than by
128
+ * copying the stage directory keeps the same two properties every other packer
129
+ * has — the modes come from the plan (`readStage` has already applied them, and
130
+ * the artifact upload that flattens them cannot reach in between), and the
131
+ * stage's own `.gjsify-ship-stage.json` stays out, because it was never in the
132
+ * payload.
133
+ *
134
+ * `root` is WIPED first, exactly as `writeStage` wipes: a `.app` left over from a
135
+ * previous run with a file the current payload no longer names would otherwise be
136
+ * signed, zipped and shipped.
137
+ *
138
+ * `stripPrefix` is what makes it the ARTIFACT rather than a directory holding
139
+ * one, and leaving it out produced exactly that: every staged darwin path already
140
+ * begins with `<App>.app/` — that is what `Layout.root` means — so writing them
141
+ * verbatim under an artifact called `<App>.app` gave
142
+ * `out/Ship Demo.app/Ship Demo.app/Contents/…`. The Finder shows the outer
143
+ * directory as a plain folder, so the failure is a bundle that is not one, and it
144
+ * is invisible to every reader that walks the tree looking for `Contents/`.
145
+ * Measured on the WIP this replaces; the discriminator is the depth assertion in
146
+ * `tests/e2e/ship-macos`.
147
+ *
148
+ * A path OUTSIDE `stripPrefix` throws rather than being written somewhere near
149
+ * the artifact. The alternative — pass it through — would put a file beside the
150
+ * bundle instead of inside it, which is the shape ADR 0024 § A4 records
151
+ * `codesign` refusing, and it would be silent.
152
+ */
153
+ export function writePayload(root, payload, stripPrefix) {
154
+ // `''` means "the payload is already artifact-relative"; anything else is a
155
+ // directory name and gets its separator here rather than at three call sites.
156
+ const strip = stripPrefix === '' ? '' : `${stripPrefix}/`;
157
+ rmSync(root, { recursive: true, force: true, maxRetries: 5 });
158
+ mkdirSync(root, { recursive: true });
159
+ for (const entry of payload) {
160
+ if (!entry.path.startsWith(strip)) {
161
+ throw new Error(`gjsify ship: ${entry.path} is outside ${stripPrefix || 'the artifact'}, which this artifact ` +
162
+ 'IS — so there is nowhere inside it for the file to go. A payload file that escaped the ' +
163
+ 'bundle would ship beside it, unsigned and unfound.');
164
+ }
165
+ const target = join(root, entry.path.slice(strip.length).split('/').join(sep));
166
+ mkdirSync(dirname(target), { recursive: true });
167
+ writeFileSync(target, entry.data);
168
+ chmodSync(target, entry.mode);
169
+ }
170
+ }
171
+ /**
172
+ * Total bytes of the regular files under `root`.
173
+ *
174
+ * What `packOne` reports for a `FormatDescriptor.artifactKind: 'directory'`.
175
+ * `statSync` on a directory answers the directory ENTRY's size — 4096 on ext4 —
176
+ * so a `.app` carrying a 20 MiB bundle would be printed as "4096 bytes", which is
177
+ * not a rounding error but a different number. Directory entries themselves are
178
+ * deliberately not counted: what a user wants to know is how much payload they
179
+ * are about to move.
180
+ */
181
+ export function directorySize(root) {
182
+ let total = 0;
183
+ for (const rel of listFilesRecursive(root))
184
+ total += statSync(join(root, rel.split('/').join(sep))).size;
185
+ return total;
186
+ }
@@ -36,7 +36,108 @@ export interface ShipArtifact {
36
36
  /** Size in bytes. */
37
37
  size: number;
38
38
  }
39
- export type FormatId = 'deb' | 'rpm';
39
+ /**
40
+ * The closed vocabulary of formats.
41
+ *
42
+ * The two macOS members are spelled `macos-…` rather than `app` / `app-zip`, and
43
+ * the reason is collision rather than length: the bare word `app` is already
44
+ * taken three times over in this subsystem — the `gjsify.app` config key,
45
+ * `ShipSettings.app`, `PackSettings.app` — and every one of them means the
46
+ * INTERPRETER. A member of this union is also a directory name (`overlay/<id>/`)
47
+ * and a `--target` value a user types, so the ambiguity would have reached both a
48
+ * path and a command line.
49
+ *
50
+ * `scripts/check-ship-format-vocabulary.mjs` reads the declaration below
51
+ * TEXTUALLY and fails if a comment gives it a second match, which is why this one
52
+ * describes the union without quoting its head.
53
+ */
54
+ export type FormatId = 'deb' | 'rpm' | 'flatpak' | 'macos-app' | 'macos-app-zip' | 'macos-app-dmg' | 'windows-dir' | 'windows-dir-zip' | 'msi';
55
+ /**
56
+ * The formats whose ARTIFACT carries a distro dependency list.
57
+ *
58
+ * Not every `FormatId`, and this is where the difference is written down once
59
+ * instead of being rediscovered per call site. A `.deb` says
60
+ * `Depends: gir1.2-gtk-4.0`, an `.rpm` says `Requires: gtk4`, and both are
61
+ * package names in a distribution's namespace. A Flatpak has no such field: it
62
+ * declares ONE dependency, its runtime (`org.gnome.Platform//50`), in the
63
+ * manifest — and inside that runtime a typelib is either present or nothing on
64
+ * the system provides it, so there is no package to name and no gap a name
65
+ * could close.
66
+ *
67
+ * `Extract<…>` rather than a fresh literal union, so this cannot outlive
68
+ * `FormatId`: dropping a member there shrinks this and reds every table keyed on
69
+ * it. The other direction is covered by {@link FormatDescriptor.depends} being
70
+ * REQUIRED — a new format cannot be added without saying which side it is on.
71
+ */
72
+ export type DistroFormatId = Extract<FormatId, 'deb' | 'rpm'>;
73
+ /** An operating system in the repo-wide `process.platform` spelling (root AGENTS.md § Runtime & platform model). */
74
+ export type HostOs = 'linux' | 'darwin' | 'win32';
75
+ /**
76
+ * Commands a packer EXECS — a flat LIST when every host runs the same ones, and
77
+ * a per-OS MAP when the backend is host-SELECTED.
78
+ *
79
+ * The map arrived with the `.msi` and nothing else needs it, which is exactly why
80
+ * it is a union rather than a map everywhere: five of the eight rows exec one tool
81
+ * or none, and spelling `{ linux: [], darwin: [], win32: [] }` on each of them
82
+ * would make five rows say three times over what one row has to say once.
83
+ *
84
+ * The `.msi` cannot use the flat form and be honest. It is produced by `wixl` on
85
+ * Linux and by WiX v3 on Windows out of ONE authored `.wxs` (ADR 0024 § A6), so a
86
+ * flat list is wrong in both directions: the union of the two demands `wixl` of a
87
+ * Windows host that will never run it, and either half alone silently claims the
88
+ * other OS needs nothing. {@link requiredToolsOn} is the one resolver, and
89
+ * `assertToolsInstalled` takes the host it resolves for.
90
+ */
91
+ export type RequiredTools = readonly string[] | Readonly<Partial<Record<HostOs, readonly string[]>>>;
92
+ /**
93
+ * Where a format can be FINISHED, and who can read the result back (ADR 0024 § A3).
94
+ *
95
+ * Three fields rather than one `hostOs`, because the three questions have
96
+ * different answers. `finishOn` is not the LAYOUT — a `<App>.app` tree is
97
+ * assembled anywhere while the `.dmg` around it needs macOS. `requiredTools` is
98
+ * not implied by `finishOn` — `.deb` and `.rpm` are written by this tree and
99
+ * exec nothing, which is the whole reason `rpm` could become an independent
100
+ * oracle and catch the dpkg-`$1` defect in the first artifact. And `oracle` is
101
+ * derivable from neither: a format built by the platform's own tool forfeits
102
+ * that independence unless a reader from a DIFFERENT implementation family
103
+ * exists, which is exactly what `selfReading` is the honest confession of.
104
+ */
105
+ export interface HostRequirement {
106
+ /** OSes whose tooling can finish this format. `'any'` = pure JS, under GJS, offline. */
107
+ finishOn: 'any' | readonly HostOs[];
108
+ /** Commands the packer EXECS. Empty iff this tree writes the format itself. */
109
+ requiredTools: RequiredTools;
110
+ /**
111
+ * How to install {@link requiredTools}, in this format's own words.
112
+ *
113
+ * DATA, because the refusal that names it is one generic function. The
114
+ * first cut hardcoded `sudo dnf install flatpak flatpak-builder` inside
115
+ * `assertToolsInstalled`, which is correct for exactly one format and would
116
+ * have told the first `.dmg` or `.msi` user to install flatpak — from the
117
+ * same branchless dispatch this whole table exists to keep branchless.
118
+ * Required whenever `requiredTools` is non-empty on ANY host; `flatpak.spec.ts`
119
+ * holds that, the same way it holds `selfReading`.
120
+ *
121
+ * ONE string even for a host-selected backend, and that is not a shortcut: the
122
+ * refusal already names the tool that is missing, so a hint covering both routes
123
+ * ("Fedora: … / Windows: …") answers the reader's question without a second
124
+ * per-OS field to keep in step with the first.
125
+ */
126
+ installHint?: string;
127
+ oracle: {
128
+ /** Readers from a DIFFERENT implementation family than the packer. */
129
+ readWith: readonly string[];
130
+ /** Where each reader runs. A Linux-runnable reader is worth more — every CI leg has one. */
131
+ readOn: readonly HostOs[];
132
+ /**
133
+ * No independent discriminator yet. Legal to DECLARE while a format is
134
+ * being built; illegal to release — `flatpak.spec.ts` is what turns that
135
+ * sentence into a red test, so flipping it is a decision somebody makes
136
+ * rather than a field nobody reads.
137
+ */
138
+ selfReading: boolean;
139
+ };
140
+ }
40
141
  /**
41
142
  * Everything that is per-FORMAT, as data.
42
143
  *
@@ -46,8 +147,71 @@ export type FormatId = 'deb' | 'rpm';
46
147
  */
47
148
  export interface FormatDescriptor {
48
149
  id: FormatId;
150
+ /**
151
+ * The OS whose LAYOUT this format wraps.
152
+ *
153
+ * Not `host.finishOn`, and the two must not be collapsed: `finishOn` says
154
+ * where the container can be PRODUCED, this says which staged tree it is a
155
+ * container FOR. A `.dmg` is `finishOn: ['darwin']` and `layoutOs: 'darwin'`,
156
+ * which makes them look like one field — but the `.app` zip beside it is
157
+ * `finishOn: 'any'` with the same `layoutOs`, and that is the pair that
158
+ * proves they are different questions.
159
+ *
160
+ * This is what gives `defaultFormatIds` its second criterion. Without it, a
161
+ * `.app` and a Windows program directory — both `finishOn: 'any'` — would
162
+ * make a bare `gjsify ship` on Linux emit five artifacts, three of which are
163
+ * for operating systems the caller did not ask about.
164
+ */
165
+ layoutOs: HostOs;
49
166
  /** Install prefix the format's contents hang under. */
50
167
  prefix: string;
168
+ /** Where this format can be packed, what it execs, and who reads it back. */
169
+ host: HostRequirement;
170
+ /**
171
+ * The key this format's dependency list is spelled under, or `null` when its
172
+ * dependencies are not a package list at all — see {@link DistroFormatId}.
173
+ */
174
+ depends: DistroFormatId | null;
175
+ /**
176
+ * The interpreters this format's runtime can actually provide at RUN time.
177
+ *
178
+ * A list and not a boolean, and on the descriptor rather than in a branch,
179
+ * because the answer is a property of the format's runtime and nothing else
180
+ * can be asked for it. `deb`/`rpm` carry both: a distribution ships `gjs` and
181
+ * `nodejs`, and the package declares whichever it execs.
182
+ *
183
+ * ⚠️ `flatpak` carries `gjs` ALONE, and that is a measured limit rather than
184
+ * a conservative default. `org.gnome.Platform` ships `gjs` and no `node`;
185
+ * Node exists only as `org.freedesktop.Sdk.Extension.node2x`, which puts
186
+ * `/usr/lib/sdk/node24/bin` on the **build** PATH — this repo's own
187
+ * `guides/flatpak-cli-tool.md` says so in those words. So a `--app node`
188
+ * Flatpak would install and die at `exec node`.
189
+ *
190
+ * That case was reachable: `assertLauncherMatchesInterpreter` is about the
191
+ * launcher agreeing with the DEPENDENCY, and a Flatpak has no dependency
192
+ * list, so the check is vacuous here — it compares `settings.app` against a
193
+ * launcher rendered from `settings.app`. Only the RUNTIME knows, and only
194
+ * this row can say.
195
+ */
196
+ interpreters: readonly ('gjs' | 'node')[];
197
+ /**
198
+ * Why this format's runtime provides {@link interpreters} and no more — the
199
+ * body of the refusal a project on the wrong interpreter gets.
200
+ *
201
+ * On the row rather than inside the refusal, and the incident is one line
202
+ * old: the refusal carried Flatpak's paragraph about `org.gnome.Platform`
203
+ * hardcoded, and the first macOS row printed it — telling somebody shipping a
204
+ * `.app` that `org.freedesktop.Sdk.Extension.node2x` puts node on the build
205
+ * path. Exactly the shape `installHint` moved out of `assertToolsInstalled`
206
+ * for, one field over.
207
+ *
208
+ * REQUIRED, including on the two rows that accept both interpreters and can
209
+ * therefore never print it. A row that answers "nothing here is restricted,
210
+ * and this is why" has ANSWERED; an optional field is one a sixth format can
211
+ * leave undefined while restricting `interpreters`, and the refusal would go
212
+ * back to having nothing to say.
213
+ */
214
+ interpreterGap: string;
51
215
  /** Where the project's licence text goes in this format's overlay. */
52
216
  licenseDest: (binaryName: string) => string;
53
217
  /**
@@ -66,6 +230,19 @@ export interface FormatDescriptor {
66
230
  archName: (arch: string, archIndependent: boolean) => string;
67
231
  /** Artifact filename, given the pack-time settings and the resolved arch label. */
68
232
  fileName: (settings: PackSettings, archLabel: string) => string;
233
+ /**
234
+ * Whether {@link fileName} names a FILE or a DIRECTORY.
235
+ *
236
+ * A `.deb`, an `.rpm` and a `.flatpak` are single files; a `<App>.app` is a
237
+ * directory that macOS treats as one object, and it is the first artifact in
238
+ * this table that is not a file. Recorded rather than inferred from the
239
+ * suffix, because `packOne` reports a SIZE and `statSync` on a directory
240
+ * answers 4096 on ext4 — a `.app` carrying a 20 MiB bundle would be printed
241
+ * as "4096 bytes", which is not a rounding error but a different number
242
+ * entirely. The `.dmg` over that same tree is `'file'` again, which is what
243
+ * makes this a property of the CONTAINER rather than "macOS is special".
244
+ */
245
+ artifactKind: 'file' | 'directory';
69
246
  }
70
247
  /** One shared-mime-info type definition (`gjsify.ship.mimeTypes`). */
71
248
  export interface ShipMimeType {
@@ -109,8 +286,16 @@ export interface ShipMimeType {
109
286
  * `share/locale/<rel>`, and no packer reads the field. So translations
110
287
  * cross in the payload, where they belong, and the `abs` path does not
111
288
  * cross at all. This list being explicit is what caught that.
112
- * - `appId`, `name`, `kind`, `execArgs` — read by the planner and the metadata
113
- * renderers, all of which have run by the time a stage exists.
289
+ * - `kind`, `execArgs` — read by the planner and the metadata renderers, all of
290
+ * which have run by the time a stage exists. `appId` was on this line and
291
+ * MOVED: the Flatpak packer needs it at pack time twice over — it is the
292
+ * manifest's `id` and the ref `flatpak build-bundle` exports — and neither is
293
+ * derivable from the payload, because the desktop entry that carries the id
294
+ * is only staged for `kind: 'app'`. `name` was on it too and moved for the
295
+ * same kind of reason one milestone later: the macOS artifact IS
296
+ * `<name>.app`, so the packing host has to be able to NAME it, and no other
297
+ * field in this object can spell it (`binaryName` is the executable inside
298
+ * the bundle, not the bundle).
114
299
  * - `arch` — carried by the manifest's `target`, which is also the value a
115
300
  * mismatched stage is refused on, so it lives in exactly one place.
116
301
  * - `mimeTypes` — phase 1 renders the shared-mime-info document into the
@@ -121,6 +306,19 @@ export interface ShipMimeType {
121
306
  export interface PackSettings {
122
307
  /** Package name: the `bin/` entry, the deb `Package:`, the rpm `Name:`. */
123
308
  binaryName: string;
309
+ /** Reverse-DNS application id — the Flatpak manifest's `id` and the exported ref. */
310
+ appId: string;
311
+ /**
312
+ * Human-readable display name — and, on macOS, the `<App>.app` directory itself.
313
+ *
314
+ * On this side of the host boundary because the darwin packers cannot work
315
+ * without it: `macos-app`'s artifact is a directory called `${name}.app` and
316
+ * `macos-app-zip` wraps that directory, so a finishing host with the stage and
317
+ * nothing else still has to be able to spell it. Also what `Info.plist`'s
318
+ * `CFBundleName`/`CFBundleDisplayName` carry — see `layout.ts`'s
319
+ * `LayoutMetadataInput`.
320
+ */
321
+ name: string;
124
322
  /** Upstream version, normalised for package managers. */
125
323
  version: string;
126
324
  /** Package revision within one upstream version (deb revision / rpm release). */
@@ -139,26 +337,65 @@ export interface PackSettings {
139
337
  /** rpm `Group:`. */
140
338
  group: string;
141
339
  /** Extra runtime dependencies per format, appended to the derived set. */
142
- extraDepends: {
143
- deb: string[];
144
- rpm: string[];
145
- };
340
+ extraDepends: Record<DistroFormatId, string[]>;
146
341
  /** Project-supplied GI-namespace → package rows, filling gaps in the built-in table. */
147
- typelibPackages: Record<string, {
148
- deb: string;
149
- rpm: string;
150
- }>;
151
- /** Minimum GJS the emitted dependency asks for. */
342
+ typelibPackages: Record<string, Record<DistroFormatId, string>>;
343
+ /**
344
+ * The interpreter the launcher execs, and therefore the one the package
345
+ * depends on. From `gjsify.app`; default `'gjs'`.
346
+ *
347
+ * ON `PackSettings`, not on `ShipSettings` alone, because BOTH halves need it
348
+ * and they must not answer it differently: `renderLauncher` writes
349
+ * `exec gjs -m` or `exec node` from this field, and `deriveDepends` seeds
350
+ * `gjs >= …` or `nodejs >= …` from the same one. Two sources here is the
351
+ * defect this field replaces — a filename heuristic once produced
352
+ * `Depends: gjs (>= 1.86), nodejs (>= 24)` on a package whose launcher execed
353
+ * gjs.
354
+ */
355
+ app: 'gjs' | 'node';
356
+ /** Minimum GJS the emitted dependency asks for. Used only for `app: 'gjs'`. */
152
357
  minGjsVersion: string;
358
+ /** Minimum Node major. Used only for `app: 'node'`. */
359
+ minNodeVersion: string;
360
+ /** The Flatpak manifest's non-payload half, fully defaulted at stage time. */
361
+ flatpak: ShipFlatpakSettings;
362
+ }
363
+ /**
364
+ * What the generated Flatpak manifest says about everything that is not the payload.
365
+ *
366
+ * Resolved — defaults applied — while the project is still in reach, because
367
+ * the defaults depend on `kind` and on `gjsify.flatpak`/`gjsify.ship.flatpak`,
368
+ * and the finishing host has neither.
369
+ *
370
+ * There is deliberately no `modules` / `extraModules` here. Under `ship` the
371
+ * module list IS the staged payload (ADR 0024 § 2: "a target that needs an
372
+ * extra file gets an overlay, never a branch in the staging code"), and an
373
+ * escape hatch that injects arbitrary build modules would put a second staging
374
+ * model back in the tree — which is the one thing § 8 gates the whole migration
375
+ * on. A project that genuinely needs to BUILD something inside the sandbox
376
+ * still has `gjsify flatpak init` + `gjsify flatpak build`, unchanged.
377
+ */
378
+ export interface ShipFlatpakSettings {
379
+ /** Runtime id, e.g. `org.gnome.Platform`. */
380
+ runtime: string;
381
+ /** Runtime/SDK version, e.g. `50`. */
382
+ runtimeVersion: string;
383
+ /** SDK id, e.g. `org.gnome.Sdk`. */
384
+ sdk: string;
385
+ /** The branch the app is exported under — the last segment of `app/<id>/<arch>/<branch>`. */
386
+ branch: string;
387
+ sdkExtensions: string[];
388
+ /** Path components prepended to `PATH` inside the build sandbox. */
389
+ appendPath: string[];
390
+ /** Capabilities the finished app is granted. */
391
+ finishArgs: string[];
392
+ /** Cleanup globs applied to `/app` after the module has run. */
393
+ cleanup: string[];
153
394
  }
154
395
  /** The resolved, fully defaulted configuration for one `gjsify ship` run. */
155
396
  export interface ShipSettings extends PackSettings {
156
397
  /** Absolute path to the project being shipped. */
157
398
  projectDir: string;
158
- /** Reverse-DNS application id — desktop entry, metainfo and icon basename. */
159
- appId: string;
160
- /** Human-readable display name. */
161
- name: string;
162
399
  /** Absolute path to the licence file, when one was found. */
163
400
  licenseFile?: string;
164
401
  /** `'app'` stages a desktop entry and an icon; `'cli'` stages neither. */
@@ -203,4 +440,15 @@ export interface ShipSettings extends PackSettings {
203
440
  outDir: string;
204
441
  /** Target architecture in `process.arch` spelling. */
205
442
  arch: string;
443
+ /**
444
+ * The OS whose LAYOUT this run assembles, from the `gjsify ship <os>`
445
+ * positional (ADR 0024 § A2).
446
+ *
447
+ * Beside `arch` and for the same reason: together they are the stage's
448
+ * `target`, the string `--expect-target <os>-<arch>` is compared against, and
449
+ * the pair a matrix leg is identified by. Deliberately NOT in
450
+ * {@link PackSettings} — the manifest's `target` carries it, so it lives in
451
+ * exactly one place on the far side of the host boundary.
452
+ */
453
+ layoutOs: HostOs;
206
454
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Escape a value for XML element content or an attribute value.
3
+ *
4
+ * `&` FIRST, or the ampersands the four later replacements introduce are escaped
5
+ * again and `Ship & Co` comes out as `Ship &amp;amp; Co`.
6
+ */
7
+ export declare function xmlEscape(value: string): string;
@@ -0,0 +1,25 @@
1
+ // The XML escape `gjsify ship`'s document renderers share.
2
+ //
3
+ // TWO COPIES BECAME ONE. `mime.ts` and `msi.ts` each grew their own — five
4
+ // replacements, same entities, same order, byte-identical bodies — one milestone
5
+ // apart, by people who never saw the other file. The second copy is where the
6
+ // helper goes.
7
+ //
8
+ // `plist.ts` keeps its own and that divergence CARRIES something: a plist value
9
+ // lands only in element CONTENT, so `"` and `'` are left as themselves rather
10
+ // than becoming entities a human reading `Info.plist` has to decode. This one is
11
+ // used in ATTRIBUTES too — a `.wxs` is almost nothing else — so it escapes both.
12
+ /**
13
+ * Escape a value for XML element content or an attribute value.
14
+ *
15
+ * `&` FIRST, or the ampersands the four later replacements introduce are escaped
16
+ * again and `Ship & Co` comes out as `Ship &amp;amp; Co`.
17
+ */
18
+ export function xmlEscape(value) {
19
+ return value
20
+ .replace(/&/g, '&amp;')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;')
24
+ .replace(/'/g, '&apos;');
25
+ }
@@ -0,0 +1,69 @@
1
+ import type { PayloadEntry } from './payload.js';
2
+ /** CRC-32 of a byte range, in ZIP's spelling (pre- and post-inverted). */
3
+ export declare function crc32(data: Uint8Array): number;
4
+ /**
5
+ * A Unix epoch second as the MS-DOS date and time pair ZIP stores.
6
+ *
7
+ * The format is from 1980 and cannot say anything earlier, and its seconds field
8
+ * holds HALF-seconds, so an odd second is not representable. Neither is refused:
9
+ * the stamp is metadata no reader in this repository asserts on, and failing a
10
+ * build because `SOURCE_DATE_EPOCH` named 1979 would reject an artifact over a
11
+ * display field. Earlier than 1980 is CLAMPED to it; the odd second is floored.
12
+ *
13
+ * UTC deliberately, not local time. A DOS stamp carries no zone, so reading it
14
+ * back as local time is the only convention available — and using the packing
15
+ * host's zone would mean two runners in different regions produce different bytes
16
+ * from one stage, which is exactly the property this module exists to hold.
17
+ */
18
+ export declare function dosDateTime(epochSeconds: number): {
19
+ date: number;
20
+ time: number;
21
+ };
22
+ /** One entry to store, addressed by its path INSIDE the archive. */
23
+ export interface ZipEntry {
24
+ /** Archive-relative, POSIX-separated. ZIP stores `/` on every platform. */
25
+ path: string;
26
+ /** POSIX mode bits, written into the external attributes' high 16 bits. */
27
+ mode: number;
28
+ data: Uint8Array;
29
+ }
30
+ /**
31
+ * Build a ZIP archive: local headers, then the central directory, then the EOCD.
32
+ *
33
+ * `mtime` is one stamp for every entry, taken from the caller. Per-file mtimes
34
+ * are deliberately not read off disk: an artifact upload does not carry them
35
+ * (`stage-manifest.ts`'s header), so re-stat'ing the stage on the packing host
36
+ * would stamp the archive with "whenever the download finished" and two hosts
37
+ * packing one stage would disagree.
38
+ *
39
+ * ENTRIES ARE SORTED BY PATH, and the caller's order is not trusted for the same
40
+ * reason `plan.ts` sorts: two runs over one payload must produce one byte
41
+ * sequence, and a `readdir` order is a filesystem's opinion.
42
+ *
43
+ * NO DIRECTORY ENTRIES. A ZIP may carry them and `unzip` recreates the tree from
44
+ * the file paths alone, so emitting them would add entries no reader here
45
+ * inspects and one more thing to keep deterministic. The consequence is stated
46
+ * rather than left to be discovered: an EMPTY directory inside a `.app` would not
47
+ * survive the archive — nothing in the payload can produce one, because
48
+ * `writeStage` only ever writes files.
49
+ *
50
+ * ZIP64 is NOT implemented, and the refusal below is what keeps that honest:
51
+ * past 4 GiB or 65535 entries the 32-bit fields silently wrap and the archive
52
+ * that comes out is well-formed and wrong. A loud limit is the better failure —
53
+ * the same argument `assertPayloadMatchesArch` makes one field over, where a
54
+ * label nothing compared to the payload produced an artifact `rpm` confirmed.
55
+ */
56
+ export declare function buildZip(entries: readonly ZipEntry[], mtime: number): Uint8Array;
57
+ /**
58
+ * The payload as zip entries, keeping each file's planned mode.
59
+ *
60
+ * `topLevel` names a directory every entry goes UNDER, and `''` means the payload
61
+ * paths already carry one. The two callers differ because their layouts do:
62
+ * a `<App>.app`'s staged paths already begin with the bundle directory
63
+ * (`Layout.root`), while a Windows program directory's do not — the stage IS its
64
+ * contents, because an installer chooses the parent. An archive without a top
65
+ * level expands into whatever directory the user happened to be in, scattering
66
+ * `app\`, `share\` and the launcher across it, and no listing of NAMES reads as
67
+ * wrong: every entry is individually correct.
68
+ */
69
+ export declare function zipEntriesFromPayload(payload: readonly PayloadEntry[], topLevel?: string): ZipEntry[];