@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
@@ -98,19 +98,45 @@ export function cliVersion() {
98
98
  }
99
99
  /** Nearest ancestor `package.json` whose `name` is `@gjsify/cli`; '' when none. */
100
100
  function walkForCliVersion() {
101
+ const dir = cliPackageDir();
102
+ if (!dir)
103
+ return '';
104
+ try {
105
+ const pkg = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8'));
106
+ if (typeof pkg.version === 'string')
107
+ return pkg.version;
108
+ }
109
+ catch {
110
+ /* cliPackageDir() already parsed it once; a race or a permission flip lands here */
111
+ }
112
+ return '';
113
+ }
114
+ /**
115
+ * Directory of the RUNNING `@gjsify/cli` package — the nearest ancestor of this
116
+ * module holding a `package.json` named `@gjsify/cli`; `null` when none.
117
+ *
118
+ * The climb, not a fixed `'..'` hop, for the reason {@link cliVersion} states: the
119
+ * package is dual-entry and the two entries sit at different depths.
120
+ * `GJSIFY_CLI_PACKAGE_JSON` overrides the starting point, same as for the version.
121
+ *
122
+ * Exported because the answer to "where am I installed" is the same question as
123
+ * "which version am I", and this file is where that climb lives — a second copy
124
+ * elsewhere is the drift `check-cli-own-version-read.mjs` exists to prevent.
125
+ */
126
+ export function cliPackageDir() {
101
127
  const override = globalThis.process?.env?.['GJSIFY_CLI_PACKAGE_JSON'];
102
128
  let dir;
103
129
  try {
104
130
  dir = override ? dirname(override) : dirname(fileURLToPath(import.meta.url));
105
131
  }
106
132
  catch {
107
- return '';
133
+ return null;
108
134
  }
109
135
  for (let i = 0; i < 12; i++) {
110
136
  try {
111
137
  const pkg = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8'));
112
- if (pkg.name === '@gjsify/cli' && typeof pkg.version === 'string')
113
- return pkg.version;
138
+ if (pkg.name === '@gjsify/cli')
139
+ return dir;
114
140
  }
115
141
  catch {
116
142
  /* no manifest here (or unreadable) — keep climbing */
@@ -120,7 +146,7 @@ function walkForCliVersion() {
120
146
  break;
121
147
  dir = parent;
122
148
  }
123
- return '';
149
+ return null;
124
150
  }
125
151
  /**
126
152
  * npm-CLI-shaped user-agent, mirroring npm-registry-fetch's
@@ -0,0 +1,21 @@
1
+ export interface ArMember {
2
+ /** Member name, at most 16 bytes. */
3
+ name: string;
4
+ /** Member contents. */
5
+ data: Uint8Array;
6
+ /** Unix mtime in seconds. Default 0 — deterministic archives. */
7
+ mtime?: number;
8
+ /** POSIX mode. Default 0o100644. */
9
+ mode?: number;
10
+ }
11
+ /**
12
+ * Build an ar archive.
13
+ *
14
+ * Names are written space-padded with NO trailing `/`: that is what
15
+ * `dpkg-deb -b` emits and what dpkg's own reader expects for a `.deb`. The
16
+ * GNU `<name>/` convention and the BSD `#1/<len>` long-name extension are both
17
+ * for archives with names longer than 15 bytes — a `.deb`'s three members are
18
+ * `debian-binary`, `control.tar.<ext>` and `data.tar.<ext>`, all short, so
19
+ * neither extension can arise and refusing a long name is honest.
20
+ */
21
+ export declare function createArArchive(members: readonly ArMember[]): Uint8Array;
@@ -0,0 +1,57 @@
1
+ // The `ar` container a `.deb` is.
2
+ //
3
+ // Two hundred lines of dependency avoided: a Debian package is an ar archive of
4
+ // exactly three members, and the format is a text header per member with no
5
+ // index, no compression and no symbol table. Writing it here keeps `gjsify
6
+ // ship` free of `dpkg-deb` — which matters beyond convenience, because this
7
+ // project's CI runs on Fedora and `dpkg-deb` is not installable there without
8
+ // dragging in a foreign package manager. The same reasoning produced
9
+ // `gjsify flatpak sources` (a Node-free reader replacing
10
+ // `flatpak-node-generator`), and it has the same payoff: the packer runs
11
+ // wherever the CLI runs, including under GJS.
12
+ import { concatBytes, encodeUtf8 } from './bytes.js';
13
+ const GLOBAL_HEADER = '!<arch>\n';
14
+ const HEADER_SIZE = 60;
15
+ /**
16
+ * Build an ar archive.
17
+ *
18
+ * Names are written space-padded with NO trailing `/`: that is what
19
+ * `dpkg-deb -b` emits and what dpkg's own reader expects for a `.deb`. The
20
+ * GNU `<name>/` convention and the BSD `#1/<len>` long-name extension are both
21
+ * for archives with names longer than 15 bytes — a `.deb`'s three members are
22
+ * `debian-binary`, `control.tar.<ext>` and `data.tar.<ext>`, all short, so
23
+ * neither extension can arise and refusing a long name is honest.
24
+ */
25
+ export function createArArchive(members) {
26
+ const chunks = [encodeUtf8(GLOBAL_HEADER)];
27
+ for (const member of members) {
28
+ chunks.push(buildMemberHeader(member));
29
+ chunks.push(member.data);
30
+ // Members are aligned to an even offset; the pad byte is `\n`.
31
+ if (member.data.byteLength % 2 === 1)
32
+ chunks.push(encodeUtf8('\n'));
33
+ }
34
+ return concatBytes(chunks);
35
+ }
36
+ function buildMemberHeader(member) {
37
+ // `pad()` below is what refuses an over-long name, with the width in the
38
+ // message — a second length check here would differ only for a non-ASCII
39
+ // name, and all three member names are literals.
40
+ const header = pad(member.name, 16) +
41
+ pad(String(member.mtime ?? 0), 12) +
42
+ pad('0', 6) + // uid
43
+ pad('0', 6) + // gid
44
+ pad((member.mode ?? 0o100644).toString(8), 8) +
45
+ pad(String(member.data.byteLength), 10) +
46
+ '`\n';
47
+ if (header.length !== HEADER_SIZE) {
48
+ throw new Error(`gjsify ship: internal error — ar header is ${header.length} bytes, expected ${HEADER_SIZE}.`);
49
+ }
50
+ return encodeUtf8(header);
51
+ }
52
+ function pad(value, width) {
53
+ if (value.length > width) {
54
+ throw new Error(`gjsify ship: ar header field "${value}" does not fit in ${width} bytes.`);
55
+ }
56
+ return value.padEnd(width, ' ');
57
+ }
@@ -0,0 +1,4 @@
1
+ /** Join byte chunks into one buffer. */
2
+ export declare function concatBytes(chunks: readonly Uint8Array[]): Uint8Array;
3
+ /** UTF-8 encode. The archive formats are byte formats; every length is a BYTE length. */
4
+ export declare function encodeUtf8(value: string): Uint8Array;
@@ -0,0 +1,23 @@
1
+ // The byte-level helpers all three writers need.
2
+ //
3
+ // `concatBytes` existed once per writer before this file did — the same nine
4
+ // lines in `ar.ts`, `cpio.ts` and `rpm.ts`. The second copy is where a helper
5
+ // belongs (root AGENTS.md § Code anti-patterns): a drifted copy of THIS
6
+ // function would not throw, it would silently produce a truncated archive.
7
+ /** Join byte chunks into one buffer. */
8
+ export function concatBytes(chunks) {
9
+ let total = 0;
10
+ for (const chunk of chunks)
11
+ total += chunk.byteLength;
12
+ const out = new Uint8Array(total);
13
+ let offset = 0;
14
+ for (const chunk of chunks) {
15
+ out.set(chunk, offset);
16
+ offset += chunk.byteLength;
17
+ }
18
+ return out;
19
+ }
20
+ /** UTF-8 encode. The archive formats are byte formats; every length is a BYTE length. */
21
+ export function encodeUtf8(value) {
22
+ return new TextEncoder().encode(value);
23
+ }
@@ -0,0 +1,22 @@
1
+ export interface CpioEntry {
2
+ /** Archive path. RPM spells these `./usr/bin/foo`. */
3
+ name: string;
4
+ /** Full st_mode, i.e. file type bits OR permission bits. */
5
+ mode: number;
6
+ /** File contents; empty for directories and symlinks. */
7
+ data?: Uint8Array;
8
+ /** Symlink target — stored as the entry's data, per the cpio format. */
9
+ linkTarget?: string;
10
+ /** Unix mtime in seconds. Default 0. */
11
+ mtime?: number;
12
+ /** Inode number. RPM numbers entries from 1. */
13
+ ino?: number;
14
+ /** Hardlink count. Default 1. */
15
+ nlink?: number;
16
+ }
17
+ /** POSIX `st_mode` file-type bits, spelled out so call sites read as intent. */
18
+ export declare const S_IFREG = 32768;
19
+ export declare const S_IFDIR = 16384;
20
+ export declare const S_IFLNK = 40960;
21
+ /** Build a cpio archive, terminated by the mandatory `TRAILER!!!` entry. */
22
+ export declare function createCpioArchive(entries: readonly CpioEntry[]): Uint8Array;
@@ -0,0 +1,93 @@
1
+ // The "new ASCII" cpio archive (magic `070701`) an RPM carries as its payload.
2
+ //
3
+ // Every field is 8 hex digits, which makes the format trivially writable and —
4
+ // more usefully — trivially INSPECTABLE: `rpm2cpio x.rpm | cpio -it` reads what
5
+ // this writes, so the e2e suite has an oracle that shares no code with the
6
+ // writer. (rpm itself formats them with `%8.8lx`, i.e. LOWERCASE; both parse,
7
+ // and this writer emits uppercase.)
8
+ //
9
+ // Two padding rules, both easy to get subtly wrong and both silent when wrong:
10
+ // the filename (including its NUL) is padded so that the DATA starts on a
11
+ // 4-byte boundary counted from the start of the header, and the data itself is
12
+ // padded so the NEXT header does too.
13
+ import { concatBytes, encodeUtf8 } from './bytes.js';
14
+ const MAGIC = '070701';
15
+ const HEADER_FIELDS = 13;
16
+ const HEADER_SIZE = MAGIC.length + HEADER_FIELDS * 8; // 110
17
+ /** POSIX `st_mode` file-type bits, spelled out so call sites read as intent. */
18
+ export const S_IFREG = 0o100000;
19
+ export const S_IFDIR = 0o040000;
20
+ export const S_IFLNK = 0o120000;
21
+ /** Build a cpio archive, terminated by the mandatory `TRAILER!!!` entry. */
22
+ export function createCpioArchive(entries) {
23
+ const chunks = [];
24
+ let offset = 0;
25
+ entries.forEach((entry, index) => {
26
+ const body = entry.linkTarget !== undefined ? encodeUtf8(entry.linkTarget) : (entry.data ?? new Uint8Array(0));
27
+ const nameBytes = encodeUtf8(`${entry.name}\0`);
28
+ const header = buildHeader({
29
+ ino: entry.ino ?? index + 1,
30
+ mode: entry.mode,
31
+ nlink: entry.nlink ?? 1,
32
+ mtime: entry.mtime ?? 0,
33
+ filesize: body.byteLength,
34
+ namesize: nameBytes.byteLength,
35
+ });
36
+ offset = push(chunks, header, offset);
37
+ offset = push(chunks, nameBytes, offset);
38
+ offset = padTo4(chunks, offset);
39
+ offset = push(chunks, body, offset);
40
+ offset = padTo4(chunks, offset);
41
+ });
42
+ // The archive ends at the entry NAMED `TRAILER!!!` — that name is the
43
+ // signal, not any of the numeric fields. rpm writes `nlink = 1` there and
44
+ // zeroes the rest, so this does too.
45
+ const trailerName = encodeUtf8('TRAILER!!!\0');
46
+ offset = push(chunks, buildHeader({ ino: 0, mode: 0, nlink: 1, mtime: 0, filesize: 0, namesize: trailerName.byteLength }), offset);
47
+ offset = push(chunks, trailerName, offset);
48
+ padTo4(chunks, offset);
49
+ return concatBytes(chunks);
50
+ }
51
+ function buildHeader(fields) {
52
+ const header = MAGIC +
53
+ hex8(fields.ino) +
54
+ hex8(fields.mode) +
55
+ hex8(0) + // uid — packages install as root
56
+ hex8(0) + // gid
57
+ hex8(fields.nlink) +
58
+ hex8(fields.mtime) +
59
+ hex8(fields.filesize) +
60
+ hex8(0) + // devmajor
61
+ hex8(0) + // devminor
62
+ hex8(0) + // rdevmajor
63
+ hex8(0) + // rdevminor
64
+ hex8(fields.namesize) +
65
+ hex8(0); // check — unused for 070701, but the field is not optional
66
+ if (header.length !== HEADER_SIZE) {
67
+ throw new Error(`gjsify ship: internal error — cpio header is ${header.length} bytes, expected ${HEADER_SIZE}.`);
68
+ }
69
+ return encodeUtf8(header);
70
+ }
71
+ function hex8(value) {
72
+ if (value < 0 || !Number.isInteger(value)) {
73
+ throw new Error(`gjsify ship: cpio header field must be a non-negative integer, got ${value}.`);
74
+ }
75
+ const text = value.toString(16).toUpperCase();
76
+ if (text.length > 8) {
77
+ throw new Error(`gjsify ship: cpio header field ${value} does not fit in 8 hex digits.`);
78
+ }
79
+ return text.padStart(8, '0');
80
+ }
81
+ function push(chunks, bytes, offset) {
82
+ if (bytes.byteLength > 0)
83
+ chunks.push(bytes);
84
+ return offset + bytes.byteLength;
85
+ }
86
+ function padTo4(chunks, offset) {
87
+ const remainder = offset % 4;
88
+ if (remainder === 0)
89
+ return offset;
90
+ const padding = 4 - remainder;
91
+ chunks.push(new Uint8Array(padding));
92
+ return offset + padding;
93
+ }
@@ -0,0 +1,24 @@
1
+ import type { PayloadEntry } from './payload.js';
2
+ import type { ShipSettings } from './types.js';
3
+ export interface DebInputs {
4
+ settings: ShipSettings;
5
+ /** The staged payload, prefix-relative. */
6
+ payload: readonly PayloadEntry[];
7
+ /** Install prefix, e.g. `/usr`. */
8
+ prefix: string;
9
+ /** Already-derived dependency list, in `name`/`name >= version` form. */
10
+ depends: readonly string[];
11
+ /** Debian architecture (`amd64`, `all`, …). */
12
+ archLabel: string;
13
+ /** Unix seconds stamped into every header. */
14
+ mtime: number;
15
+ }
16
+ export declare function buildDeb(inputs: DebInputs): Promise<Uint8Array>;
17
+ /**
18
+ * `./`, `./usr/`, `./usr/bin/`, … for every path in the archive.
19
+ *
20
+ * dpkg opens each file with `O_CREAT | O_EXCL` and never creates a parent, so
21
+ * a missing directory entry is `cannot create '<path>'` mid-unpack — with the
22
+ * package half-installed.
23
+ */
24
+ export declare function ancestorDirectories(paths: readonly string[]): string[];
@@ -0,0 +1,118 @@
1
+ // The `.deb` packer.
2
+ //
3
+ // A Debian package is `debian-binary` + `control.tar.gz` + `data.tar.gz` in an
4
+ // ar container, and every one of those three parts already had a writer in
5
+ // this tree (`utils/ship/ar.ts`, `@gjsify/tar`) or is four bytes long. So no
6
+ // `dpkg-deb`, no vendored Go binary, no download at ship time — which is what
7
+ // lets the packer run under GJS, offline, on a Fedora CI image where dpkg does
8
+ // not exist.
9
+ //
10
+ // Everything below that looks like a detail is a documented dpkg failure mode:
11
+ // the ancestor-directory expansion (dpkg never calls `mkdir -p` and an ENOENT
12
+ // aborts the unpack), the `md5sums` path spelling (no `./`, exactly two
13
+ // spaces), the final newline in `control` (its absence is a parse error), and
14
+ // the gzip header's mtime (not a dpkg failure at all — it silently destroys
15
+ // reproducibility instead).
16
+ import { createHash } from 'node:crypto';
17
+ import { posix } from 'node:path';
18
+ import { createTarball } from '@gjsify/tar';
19
+ import { createArArchive } from './ar.js';
20
+ import { formatDebDepend } from './depends.js';
21
+ import { gzipDeterministic } from './gzip.js';
22
+ import { renderDebScripts } from './scripts.js';
23
+ export async function buildDeb(inputs) {
24
+ const { settings, payload, mtime } = inputs;
25
+ const prefix = inputs.prefix.replace(/^\/+/, '');
26
+ const named = payload.map((entry) => ({ ...entry, tarPath: `./${posix.join(prefix, entry.path)}` }));
27
+ const directories = ancestorDirectories(named.map((entry) => entry.tarPath));
28
+ const dataEntries = [
29
+ ...directories.map((name) => ({ name, mode: 0o755, mtime, directory: true })),
30
+ ...named.map((entry) => ({ name: entry.tarPath, body: entry.data, mode: entry.mode, mtime })),
31
+ ];
32
+ const dataTar = await gzipDeterministic(createTarball(dataEntries));
33
+ // Policy 5.6.20: regular files and symlinks round UP to whole KiB, every
34
+ // other filesystem object counts as one. Getting the unit wrong (bytes
35
+ // instead of KiB) is invisible — dpkg never validates this field, apt just
36
+ // shows the user a wrong number.
37
+ const installedSize = named.reduce((total, entry) => total + Math.ceil(entry.data.byteLength / 1024), 0) + directories.length;
38
+ const control = renderControl(inputs, installedSize);
39
+ const md5sums = named
40
+ .map((entry) => `${createHash('md5').update(entry.data).digest('hex')} ${entry.tarPath.slice(2)}\n`)
41
+ .join('');
42
+ const controlEntries = [
43
+ { name: './', mode: 0o755, mtime, directory: true },
44
+ { name: './control', body: control, mode: 0o644, mtime },
45
+ { name: './md5sums', body: md5sums, mode: 0o644, mtime },
46
+ ];
47
+ const maintainerScripts = renderDebScripts(settings, inputs.prefix);
48
+ for (const [name, body] of Object.entries(maintainerScripts)) {
49
+ // dpkg refuses a maintainer script outside 0555–0775 with the mode in
50
+ // the error, so this is 0755 rather than "whatever the source had".
51
+ controlEntries.push({ name: `./${name}`, body, mode: 0o755, mtime });
52
+ }
53
+ const controlTar = await gzipDeterministic(createTarball(controlEntries));
54
+ return createArArchive([
55
+ { name: 'debian-binary', data: new TextEncoder().encode('2.0\n'), mtime },
56
+ { name: 'control.tar.gz', data: controlTar, mtime },
57
+ { name: 'data.tar.gz', data: dataTar, mtime },
58
+ ]);
59
+ }
60
+ /**
61
+ * Collapse a value that has to occupy exactly one control line.
62
+ *
63
+ * A newline here does not corrupt the file — it FORGES a field: a summary
64
+ * pasted with a hard wrap turns its second line into whatever that line happens
65
+ * to start with, and `Pre-Depends: …` is a real thing to end up with by
66
+ * accident. The extended description already collapses whitespace per
67
+ * paragraph; these fields did not.
68
+ */
69
+ function singleLine(value) {
70
+ return value.replace(/[\r\n]+/g, ' ').trim();
71
+ }
72
+ /**
73
+ * `./`, `./usr/`, `./usr/bin/`, … for every path in the archive.
74
+ *
75
+ * dpkg opens each file with `O_CREAT | O_EXCL` and never creates a parent, so
76
+ * a missing directory entry is `cannot create '<path>'` mid-unpack — with the
77
+ * package half-installed.
78
+ */
79
+ export function ancestorDirectories(paths) {
80
+ const directories = new Set(['./']);
81
+ for (const path of paths) {
82
+ const parts = path.replace(/^\.\//, '').split('/');
83
+ parts.pop();
84
+ let current = '.';
85
+ for (const part of parts) {
86
+ current = `${current}/${part}`;
87
+ directories.add(`${current}/`);
88
+ }
89
+ }
90
+ return [...directories].sort();
91
+ }
92
+ function renderControl(inputs, installedSize) {
93
+ const { settings } = inputs;
94
+ const fields = [
95
+ ['Package', settings.binaryName],
96
+ ['Version', `${settings.version}-${settings.release}`],
97
+ ['Architecture', inputs.archLabel],
98
+ ['Maintainer', settings.maintainer],
99
+ ['Installed-Size', String(installedSize)],
100
+ ];
101
+ if (inputs.depends.length > 0)
102
+ fields.push(['Depends', inputs.depends.map(formatDebDepend).join(', ')]);
103
+ fields.push(['Section', settings.section], ['Priority', 'optional']);
104
+ if (settings.homepage)
105
+ fields.push(['Homepage', settings.homepage]);
106
+ const lines = fields.map(([key, value]) => `${key}: ${singleLine(value)}`);
107
+ // The extended description is continuation lines: one leading space per
108
+ // line, and a lone ` .` for a paragraph break — a truly blank line ends
109
+ // the field and silently truncates everything after it.
110
+ lines.push(`Description: ${singleLine(settings.summary)}`);
111
+ const body = settings.description.filter((paragraph) => paragraph !== settings.summary);
112
+ body.forEach((paragraph, index) => {
113
+ if (index > 0)
114
+ lines.push(' .');
115
+ lines.push(` ${paragraph}`);
116
+ });
117
+ return `${lines.join('\n')}\n`;
118
+ }
@@ -0,0 +1,62 @@
1
+ import type { FormatId } from './types.js';
2
+ /**
3
+ * The GJS the bundles target (root AGENTS.md § Constraints: GJS 1.86 /
4
+ * SpiderMonkey 140, Rolldown `firefox140`). A `--app gjs` bundle can carry
5
+ * ES2024 syntax an older SpiderMonkey does not parse, so this is a real floor
6
+ * and not a formality.
7
+ *
8
+ * It is also, today, UNSATISFIABLE ON DEBIAN — measured, not assumed: Debian
9
+ * went 1.82.3 (trixie) straight to 1.88.1 (forky), skipping 1.84 and 1.86, so
10
+ * no Debian suite ships a `gjs` that satisfies `>= 1.86`. Emitting the honest
11
+ * floor means a `.deb` apt REFUSES on trixie; lowering it silently would mean
12
+ * one apt installs and the app then dies on a syntax error. `warnAboutGjsFloor`
13
+ * exists so the choice is visible at ship time, and `gjsify.ship.minGjsVersion`
14
+ * is how a project whose bundle really does run on older GJS lowers it.
15
+ */
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[];
22
+ export interface DependsInputs {
23
+ /** GI namespaces the bundle imports, in `Ns-Version` spelling. */
24
+ namespaces: readonly string[];
25
+ /** `'app'` installs a desktop entry and icons; `'cli'` does not. */
26
+ kind: 'app' | 'cli';
27
+ /** Whether the payload installs GSettings schemas. */
28
+ hasSchemas: boolean;
29
+ /** User-supplied additions for this format. */
30
+ extra: readonly string[];
31
+ /** Project-supplied table rows, filling gaps in {@link TYPELIB_PACKAGES}. */
32
+ typelibPackages?: Record<string, {
33
+ deb: string;
34
+ rpm: string;
35
+ }>;
36
+ /** Minimum GJS. Default {@link DEFAULT_GJS_FLOOR}. */
37
+ minGjsVersion?: string;
38
+ }
39
+ /**
40
+ * The `Depends:` / `Requires:` list for one format.
41
+ *
42
+ * @throws when a namespace has no entry in the table — see the module header.
43
+ */
44
+ export declare function deriveDepends(format: FormatId, inputs: DependsInputs): string[];
45
+ /** One parsed dependency. `relation`/`version` are absent for an unversioned one. */
46
+ export interface ParsedDepend {
47
+ name: string;
48
+ relation?: '>=' | '>' | '<=' | '<' | '=';
49
+ version?: string;
50
+ }
51
+ /** `gjs >= 1.86` → its parts. Both packers need this; only the spelling differs. */
52
+ export declare function parseDepend(depend: string): ParsedDepend;
53
+ /**
54
+ * Format a dependency for `Depends:`.
55
+ *
56
+ * dpkg puts the bound in parentheses and spells strict inequality `<<`/`>>` —
57
+ * bare `<`/`>` are deprecated aliases for `<=`/`>=`, so emitting them means the
58
+ * opposite of what it reads like.
59
+ */
60
+ export declare function formatDebDepend(depend: string): string;
61
+ /** The namespaces the table knows — exported so the unit tests can assert on the set. */
62
+ export declare function knownNamespaces(): string[];