@gjsify/cli 0.44.0 → 0.46.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.
- package/dist/cli.gjs.mjs +202 -167
- package/lib/commands/gettext.d.ts +2 -1
- package/lib/commands/gettext.js +105 -47
- package/lib/commands/onboard.d.ts +7 -0
- package/lib/commands/onboard.js +349 -96
- package/lib/commands/ship.d.ts +3 -0
- package/lib/commands/ship.js +850 -58
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/commands/upgrade.d.ts +23 -0
- package/lib/commands/upgrade.js +95 -12
- package/lib/templates/install.mjs.tmpl +2 -2
- package/lib/types/config-data.d.ts +127 -1
- package/lib/utils/gi-search-path.js +11 -3
- package/lib/utils/msgfmt-merge.d.ts +35 -0
- package/lib/utils/msgfmt-merge.js +102 -0
- package/lib/utils/npm-otp.d.ts +27 -2
- package/lib/utils/npm-otp.js +82 -7
- package/lib/utils/onboard-discovery.d.ts +68 -0
- package/lib/utils/onboard-discovery.js +178 -0
- package/lib/utils/onboard-probe.d.ts +118 -1
- package/lib/utils/onboard-probe.js +153 -4
- package/lib/utils/prompt-output.d.ts +12 -0
- package/lib/utils/prompt-output.js +54 -0
- package/lib/utils/prompt.js +22 -2
- package/lib/utils/publishable-packages.d.ts +1 -1
- package/lib/utils/publishable-packages.js +1 -0
- package/lib/utils/resolve-npm-package.d.ts +56 -0
- package/lib/utils/resolve-npm-package.js +69 -0
- package/lib/utils/ship/app-runtime.d.ts +214 -0
- package/lib/utils/ship/app-runtime.js +429 -0
- package/lib/utils/ship/depends.d.ts +50 -0
- package/lib/utils/ship/depends.js +117 -3
- package/lib/utils/ship/discover.js +103 -1
- package/lib/utils/ship/dmg.d.ts +71 -0
- package/lib/utils/ship/dmg.js +181 -0
- package/lib/utils/ship/flatpak.js +1 -1
- package/lib/utils/ship/formats.d.ts +108 -7
- package/lib/utils/ship/formats.js +778 -15
- package/lib/utils/ship/launcher.d.ts +13 -3
- package/lib/utils/ship/launcher.js +419 -17
- package/lib/utils/ship/layout.d.ts +330 -0
- package/lib/utils/ship/layout.js +504 -0
- package/lib/utils/ship/localize-metadata.d.ts +22 -0
- package/lib/utils/ship/localize-metadata.js +126 -0
- package/lib/utils/ship/mime.js +1 -9
- package/lib/utils/ship/msi.d.ts +126 -0
- package/lib/utils/ship/msi.js +434 -0
- package/lib/utils/ship/node-runtime.d.ts +71 -0
- package/lib/utils/ship/node-runtime.js +122 -0
- package/lib/utils/ship/payload.d.ts +156 -5
- package/lib/utils/ship/payload.js +599 -14
- package/lib/utils/ship/pe-launcher.d.ts +29 -0
- package/lib/utils/ship/pe-launcher.js +663 -0
- package/lib/utils/ship/plan.d.ts +21 -0
- package/lib/utils/ship/plan.js +55 -7
- package/lib/utils/ship/plist.d.ts +98 -0
- package/lib/utils/ship/plist.js +256 -0
- package/lib/utils/ship/rpm.js +16 -3
- package/lib/utils/ship/schemas.d.ts +61 -0
- package/lib/utils/ship/schemas.js +118 -0
- package/lib/utils/ship/scripts.js +7 -3
- package/lib/utils/ship/settings.d.ts +91 -2
- package/lib/utils/ship/settings.js +93 -1
- package/lib/utils/ship/share-dirs.d.ts +53 -0
- package/lib/utils/ship/share-dirs.js +76 -0
- package/lib/utils/ship/signing.d.ts +311 -0
- package/lib/utils/ship/signing.js +506 -0
- package/lib/utils/ship/stage-manifest.d.ts +40 -12
- package/lib/utils/ship/stage-manifest.js +99 -13
- package/lib/utils/ship/stage-writer.d.ts +48 -0
- package/lib/utils/ship/stage-writer.js +91 -2
- package/lib/utils/ship/types.d.ts +191 -14
- package/lib/utils/ship/xml.d.ts +7 -0
- package/lib/utils/ship/xml.js +25 -0
- package/lib/utils/ship/zip.d.ts +69 -0
- package/lib/utils/ship/zip.js +245 -0
- package/lib/utils/system-gi.d.ts +20 -5
- package/lib/utils/system-gi.js +36 -13
- package/lib/utils/trust-registry.js +5 -0
- package/package.json +19 -19
package/lib/commands/trust.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface TrustRequestResult {
|
|
|
20
20
|
status: number;
|
|
21
21
|
json: unknown;
|
|
22
22
|
text: string;
|
|
23
|
+
/**
|
|
24
|
+
* Response headers, so a caller can honour npm's own `Retry-After` on a 429
|
|
25
|
+
* instead of guessing a backoff. Optional because test doubles construct this
|
|
26
|
+
* shape by hand and a missing header is a well-defined "it said nothing".
|
|
27
|
+
*/
|
|
28
|
+
headers?: Headers;
|
|
23
29
|
}
|
|
24
30
|
/** A `(method, url, body?) => TrustRequestResult` function bound to a registry auth + OTP context. */
|
|
25
31
|
export type TrustRequester = (method: string, url: string, body?: unknown) => Promise<TrustRequestResult>;
|
package/lib/commands/trust.js
CHANGED
|
@@ -279,7 +279,7 @@ export function createTrustRequester(ctx) {
|
|
|
279
279
|
catch {
|
|
280
280
|
/* non-JSON body — leave json undefined */
|
|
281
281
|
}
|
|
282
|
-
return { status: res.status, json, text };
|
|
282
|
+
return { status: res.status, json, text, headers: res.headers };
|
|
283
283
|
};
|
|
284
284
|
}
|
|
285
285
|
function firstLine(s) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DependencyGroup } from '../utils/dep-aggregation.js';
|
|
1
2
|
import type { Command } from '../types/index.js';
|
|
2
3
|
interface UpgradeOptions {
|
|
3
4
|
latest?: boolean;
|
|
@@ -8,10 +9,32 @@ interface UpgradeOptions {
|
|
|
8
9
|
excludeWorkspace?: string;
|
|
9
10
|
align?: boolean;
|
|
10
11
|
check?: boolean;
|
|
12
|
+
exact?: boolean;
|
|
11
13
|
dryRun?: boolean;
|
|
12
14
|
cwd?: string;
|
|
13
15
|
verbose?: boolean;
|
|
14
16
|
yes?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare const upgradeCommand: Command<unknown, UpgradeOptions>;
|
|
19
|
+
/**
|
|
20
|
+
* `--exact` half of the CI gate: does every matched dep carry a bare version?
|
|
21
|
+
*
|
|
22
|
+
* Consistency and exactness are different questions, and the first cannot answer the
|
|
23
|
+
* second: a repository where every manifest agrees on `^4.1.0` is perfectly consistent
|
|
24
|
+
* and still lets a minor release move a subpath under a lockfile-less install. That is
|
|
25
|
+
* the hazard ADR 0029 § Risks 1 names for `@girs/*`, whose `./vocabulary` export is what
|
|
26
|
+
* `@gjsify/gtk-host` consumes — so the pin has to be the whole version, and something
|
|
27
|
+
* has to say so on every PR.
|
|
28
|
+
*
|
|
29
|
+
* Returns the number of offending declarations so the caller can exit once for both arms.
|
|
30
|
+
*/
|
|
31
|
+
export declare function reportInexactRanges(groups: readonly DependencyGroup[]): number;
|
|
32
|
+
/**
|
|
33
|
+
* The range a dependency will be WRITTEN at.
|
|
34
|
+
*
|
|
35
|
+
* Preview and write share this deliberately. They were two expressions for one decision
|
|
36
|
+
* for exactly one revision, and `--dry-run --exact` announced `^4.3.0` while the writer
|
|
37
|
+
* would have produced `4.3.0` — a tool describing a change it was not about to make.
|
|
38
|
+
*/
|
|
39
|
+
export declare function targetRange(prefix: string, version: string, exact: boolean): string;
|
|
17
40
|
export {};
|
package/lib/commands/upgrade.js
CHANGED
|
@@ -79,6 +79,11 @@ export const upgradeCommand = {
|
|
|
79
79
|
description: 'CI gate: exit non-zero when any dep is declared inconsistently across workspaces. Implies offline (no registry calls). Pairs with `--align` as the fix.',
|
|
80
80
|
type: 'boolean',
|
|
81
81
|
default: false,
|
|
82
|
+
})
|
|
83
|
+
.option('exact', {
|
|
84
|
+
description: 'Pin without a range operator. When writing, emits `1.2.3` instead of `^1.2.3`; with `--check`, fails on any matched dep that carries one. Pair with `--filter` — a repository-wide exactness check fails on every ordinary caret dep by design.',
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
default: false,
|
|
82
87
|
})
|
|
83
88
|
.option('dry-run', {
|
|
84
89
|
description: 'Print the upgrade plan without writing package.json files.',
|
|
@@ -118,7 +123,7 @@ export const upgradeCommand = {
|
|
|
118
123
|
const groups = groupByDependency(decls);
|
|
119
124
|
// --check: exit non-zero if any inconsistency.
|
|
120
125
|
if (args.check) {
|
|
121
|
-
return runCheckMode(groups);
|
|
126
|
+
return runCheckMode(groups, args.exact);
|
|
122
127
|
}
|
|
123
128
|
// --align: offline consistency-only, no registry calls.
|
|
124
129
|
if (args.align) {
|
|
@@ -163,10 +168,10 @@ export const upgradeCommand = {
|
|
|
163
168
|
if (args.dryRun) {
|
|
164
169
|
const fileCount = uniqueLocations(selected).length;
|
|
165
170
|
console.log(`[gjsify upgrade] --dry-run: would update ${selected.length} deps across ${fileCount} package.json file(s).`);
|
|
166
|
-
printChangePlan(selected);
|
|
171
|
+
printChangePlan(selected, args.exact);
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
169
|
-
const files = applyToFiles(selected);
|
|
174
|
+
const files = applyToFiles(selected, args.exact);
|
|
170
175
|
console.log(`✏️ updated ${selected.length} dep(s) across ${files} package.json file(s). Run \`gjsify install\` to apply.`);
|
|
171
176
|
},
|
|
172
177
|
};
|
|
@@ -281,10 +286,53 @@ function splitRange(range) {
|
|
|
281
286
|
return { prefix, version: parsed?.version ?? null };
|
|
282
287
|
}
|
|
283
288
|
// ─── Modes: --check / --align ──────────────────────────────────────────
|
|
284
|
-
|
|
289
|
+
/**
|
|
290
|
+
* `--exact` half of the CI gate: does every matched dep carry a bare version?
|
|
291
|
+
*
|
|
292
|
+
* Consistency and exactness are different questions, and the first cannot answer the
|
|
293
|
+
* second: a repository where every manifest agrees on `^4.1.0` is perfectly consistent
|
|
294
|
+
* and still lets a minor release move a subpath under a lockfile-less install. That is
|
|
295
|
+
* the hazard ADR 0029 § Risks 1 names for `@girs/*`, whose `./vocabulary` export is what
|
|
296
|
+
* `@gjsify/gtk-host` consumes — so the pin has to be the whole version, and something
|
|
297
|
+
* has to say so on every PR.
|
|
298
|
+
*
|
|
299
|
+
* Returns the number of offending declarations so the caller can exit once for both arms.
|
|
300
|
+
*/
|
|
301
|
+
export function reportInexactRanges(groups) {
|
|
302
|
+
const offenders = groups
|
|
303
|
+
.map((g) => ({ name: g.name, loose: g.occurrences.filter((o) => o.prefix !== '') }))
|
|
304
|
+
.filter((entry) => entry.loose.length > 0);
|
|
305
|
+
if (offenders.length === 0)
|
|
306
|
+
return 0;
|
|
307
|
+
const total = offenders.reduce((n, entry) => n + entry.loose.length, 0);
|
|
308
|
+
console.error(`gjsify upgrade --check --exact: FAIL. ${total} declaration(s) across ${offenders.length} dep(s) carry a range operator:\n`);
|
|
309
|
+
for (const entry of offenders) {
|
|
310
|
+
const byRange = new Map();
|
|
311
|
+
for (const occ of entry.loose) {
|
|
312
|
+
const list = byRange.get(occ.currentRange) ?? [];
|
|
313
|
+
list.push(occ.workspace);
|
|
314
|
+
byRange.set(occ.currentRange, list);
|
|
315
|
+
}
|
|
316
|
+
console.error(` ${ANSI.bold}${entry.name}${ANSI.reset}`);
|
|
317
|
+
for (const [range, holders] of byRange.entries()) {
|
|
318
|
+
const shown = holders.length > 6 ? `${holders.slice(0, 6).join(', ')} … +${holders.length - 6}` : holders.join(', ');
|
|
319
|
+
console.error(` ${range.padEnd(16)} — ${shown}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return total;
|
|
323
|
+
}
|
|
324
|
+
function runCheckMode(groups, exact = false) {
|
|
325
|
+
const inexact = exact ? reportInexactRanges(groups) : 0;
|
|
285
326
|
const inconsistencies = findInconsistencies(groups);
|
|
327
|
+
if (inconsistencies.length === 0 && inexact > 0) {
|
|
328
|
+
console.error(`\nFix: run \`gjsify upgrade --align --exact\` (offline; drops the operator and keeps the version).`);
|
|
329
|
+
// `return`, not a bare call: under GJS the process does not halt here, and the
|
|
330
|
+
// consistency report below would run on top of the exactness one.
|
|
331
|
+
return process.exit(1);
|
|
332
|
+
}
|
|
286
333
|
if (inconsistencies.length === 0) {
|
|
287
|
-
console.log(`gjsify upgrade --check: OK. ${groups.length} dep(s) consistently declared across workspaces
|
|
334
|
+
console.log(`gjsify upgrade --check: OK. ${groups.length} dep(s) consistently declared across workspaces` +
|
|
335
|
+
(exact ? `, every declaration pinned exactly.` : `.`));
|
|
288
336
|
return;
|
|
289
337
|
}
|
|
290
338
|
console.error(`gjsify upgrade --check: FAIL. ${inconsistencies.length} dep(s) declared at inconsistent ranges:\n`);
|
|
@@ -334,13 +382,13 @@ function runAlignMode(groups, args) {
|
|
|
334
382
|
console.log(`gjsify upgrade --align: aligning ${updates.length} inconsistent dep(s) to their highest declared version:\n`);
|
|
335
383
|
for (const u of updates) {
|
|
336
384
|
const newPrefix = u.occurrences[0]?.prefix ?? '^';
|
|
337
|
-
console.log(` ${u.name.padEnd(28)} ranges: ${[...u.declaredRanges].join(', ')} → ${newPrefix
|
|
385
|
+
console.log(` ${u.name.padEnd(28)} ranges: ${[...u.declaredRanges].join(', ')} → ${targetRange(newPrefix, u.latestVersion, args.exact ?? false)}`);
|
|
338
386
|
}
|
|
339
387
|
if (args.dryRun) {
|
|
340
388
|
console.log('\n[gjsify upgrade --align] --dry-run: no files changed.');
|
|
341
389
|
return;
|
|
342
390
|
}
|
|
343
|
-
const files = applyToFiles(updates);
|
|
391
|
+
const files = applyToFiles(updates, args.exact);
|
|
344
392
|
console.log(`\n✏️ updated ${updates.length} dep(s) across ${files} package.json file(s).`);
|
|
345
393
|
}
|
|
346
394
|
// ─── Registry resolution (group-aware) ─────────────────────────────────
|
|
@@ -491,10 +539,10 @@ function renderDeclaredCell(c) {
|
|
|
491
539
|
return joined;
|
|
492
540
|
return joined.slice(0, 29) + '…';
|
|
493
541
|
}
|
|
494
|
-
function printChangePlan(selected) {
|
|
542
|
+
function printChangePlan(selected, exact = false) {
|
|
495
543
|
for (const u of selected) {
|
|
496
544
|
for (const occ of u.occurrences) {
|
|
497
|
-
console.log(` ${occ.workspace.padEnd(38)} ${u.name.padEnd(28)} ${occ.currentRange.padEnd(12)} → ${occ.prefix
|
|
545
|
+
console.log(` ${occ.workspace.padEnd(38)} ${u.name.padEnd(28)} ${occ.currentRange.padEnd(12)} → ${targetRange(occ.prefix, u.latestVersion, exact)}`);
|
|
498
546
|
}
|
|
499
547
|
}
|
|
500
548
|
}
|
|
@@ -543,7 +591,17 @@ async function promptSelection(candidates) {
|
|
|
543
591
|
}
|
|
544
592
|
}
|
|
545
593
|
// ─── Write-back across all affected workspaces ─────────────────────────
|
|
546
|
-
|
|
594
|
+
/**
|
|
595
|
+
* The range a dependency will be WRITTEN at.
|
|
596
|
+
*
|
|
597
|
+
* Preview and write share this deliberately. They were two expressions for one decision
|
|
598
|
+
* for exactly one revision, and `--dry-run --exact` announced `^4.3.0` while the writer
|
|
599
|
+
* would have produced `4.3.0` — a tool describing a change it was not about to make.
|
|
600
|
+
*/
|
|
601
|
+
export function targetRange(prefix, version, exact) {
|
|
602
|
+
return exact ? version : `${prefix}${version}`;
|
|
603
|
+
}
|
|
604
|
+
function applyToFiles(updates, exact = false) {
|
|
547
605
|
// Group by workspace.location so each package.json is read + written once.
|
|
548
606
|
const byLocation = new Map();
|
|
549
607
|
for (const u of updates) {
|
|
@@ -559,25 +617,50 @@ function applyToFiles(updates) {
|
|
|
559
617
|
const raw = readFileSync(pkgJsonPath, 'utf-8');
|
|
560
618
|
const pkg = JSON.parse(raw);
|
|
561
619
|
let changed = false;
|
|
620
|
+
const changedNames = [];
|
|
562
621
|
for (const g of groups) {
|
|
563
622
|
const occ = g.occurrences[0];
|
|
564
623
|
const map = pkg[occ.field];
|
|
565
624
|
if (!map)
|
|
566
625
|
continue;
|
|
567
|
-
const next =
|
|
626
|
+
const next = targetRange(occ.prefix, g.latestVersion, exact);
|
|
568
627
|
if (map[g.name] !== next) {
|
|
569
628
|
map[g.name] = next;
|
|
570
629
|
changed = true;
|
|
630
|
+
changedNames.push(g.name);
|
|
571
631
|
}
|
|
572
632
|
}
|
|
573
633
|
if (changed) {
|
|
574
634
|
const indent = detectIndent(raw);
|
|
575
|
-
|
|
635
|
+
const rewritten = JSON.stringify(pkg, null, indent) + (raw.endsWith('\n') ? '\n' : '');
|
|
636
|
+
writeFileSync(pkgJsonPath, keepUntouchedLines(raw, rewritten, changedNames), 'utf-8');
|
|
576
637
|
touched++;
|
|
577
638
|
}
|
|
578
639
|
}
|
|
579
640
|
return touched;
|
|
580
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Take the re-serialised file, but keep every line that is not about a changed dep.
|
|
644
|
+
*
|
|
645
|
+
* A `JSON.parse` → `JSON.stringify` round trip rewrites the WHOLE file, and re-encoding
|
|
646
|
+
* is not identity: measured on this repository, pinning `@girs/*` decoded `\u2014` into
|
|
647
|
+
* an em dash inside three `description` fields nobody asked to touch, so a dependency
|
|
648
|
+
* bump arrived as a diff over unrelated prose. Semantically equal, and still noise a
|
|
649
|
+
* reviewer has to read past.
|
|
650
|
+
*
|
|
651
|
+
* Line-for-line only. Adding or removing a key changes the line count, and then the
|
|
652
|
+
* mapping is not sound — so that case falls back to the plain re-serialisation rather
|
|
653
|
+
* than guessing an alignment.
|
|
654
|
+
*/
|
|
655
|
+
function keepUntouchedLines(raw, rewritten, changedNames) {
|
|
656
|
+
const before = raw.split('\n');
|
|
657
|
+
const after = rewritten.split('\n');
|
|
658
|
+
if (before.length !== after.length)
|
|
659
|
+
return rewritten;
|
|
660
|
+
return after
|
|
661
|
+
.map((line, i) => (changedNames.some((name) => line.includes(`"${name}"`)) ? line : before[i]))
|
|
662
|
+
.join('\n');
|
|
663
|
+
}
|
|
581
664
|
function uniqueLocations(updates) {
|
|
582
665
|
const set = new Set();
|
|
583
666
|
for (const u of updates) {
|
|
@@ -430,7 +430,34 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
430
430
|
release?: string;
|
|
431
431
|
/** `Maintainer:` / `Packager:` as `Name <email>`. Default: `package.json#author`. */
|
|
432
432
|
maintainer?: string;
|
|
433
|
-
/**
|
|
433
|
+
/**
|
|
434
|
+
* The runtime a shipped artifact runs, PER TARGET — `gjsify.app` is the
|
|
435
|
+
* default and a key here overrides it for one layout.
|
|
436
|
+
*
|
|
437
|
+
* KEYED IN THE `process.platform` SPELLING, like {@link ShipSignOptions} and
|
|
438
|
+
* for the same reason: the value that resolves is chosen by the LAYOUT being
|
|
439
|
+
* assembled — `FormatDescriptor.layoutOs` and the stage manifest's `target.os`
|
|
440
|
+
* — never by the host doing the assembling. `macos` and `windows` are the
|
|
441
|
+
* `gjsify ship <os>` POSITIONAL's spelling, and both `gjsify ship` and
|
|
442
|
+
* `manifest-conformance`'s `ship` rule refuse them BY NAME, because a runtime
|
|
443
|
+
* under a key nothing reads is a target that silently keeps the project-wide
|
|
444
|
+
* answer.
|
|
445
|
+
*
|
|
446
|
+
* `resolveShipApp` (`utils/ship/settings.ts`) is the only reader of this key
|
|
447
|
+
* or of `gjsify.app`, and holds the rest: what one field could not answer,
|
|
448
|
+
* the measurement behind it, and why an absent key means `gjsify.app` and
|
|
449
|
+
* never `Layout.shippedRuntime`.
|
|
450
|
+
*/
|
|
451
|
+
app?: ShipAppOptions;
|
|
452
|
+
/**
|
|
453
|
+
* Formats to build when `--target` is not given. Default `['deb', 'rpm']`.
|
|
454
|
+
*
|
|
455
|
+
* A project-level DEFAULT, so it is filtered to the layout `gjsify ship <os>`
|
|
456
|
+
* assembles rather than refused against it — a name here that wraps another
|
|
457
|
+
* OS's layout is dropped, and `gjsify ship` prints which. A `--target` on the
|
|
458
|
+
* command line is a claim about ONE run and is refused instead. Without that
|
|
459
|
+
* split, declaring this key made `gjsify ship darwin --stage` impossible.
|
|
460
|
+
*/
|
|
434
461
|
targets?: string[];
|
|
435
462
|
/** Output root, relative to the project. Default `'ship'`. */
|
|
436
463
|
outDir?: string;
|
|
@@ -457,6 +484,18 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
457
484
|
* start.
|
|
458
485
|
*/
|
|
459
486
|
minGjsVersion?: string;
|
|
487
|
+
/**
|
|
488
|
+
* Minimum Node major the emitted dependency asks for, when the payload is a
|
|
489
|
+
* `--app node` bundle. Default `'24'` — the LTS line the
|
|
490
|
+
* `@gjsify/node-runtime-*` packages bundle for macOS and Windows.
|
|
491
|
+
*
|
|
492
|
+
* Lower it only if the bundle genuinely runs on an older Node. The default
|
|
493
|
+
* excludes every current DEB stable/LTS (trixie 20, Ubuntu 24.04 18, Ubuntu
|
|
494
|
+
* 26.04 22), so it makes a `.deb` that apt refuses there rather than one that
|
|
495
|
+
* installs and then dies on the first unsupported syntax — the same trade
|
|
496
|
+
* `minGjsVersion` makes, and `gjsify ship` warns about it either way.
|
|
497
|
+
*/
|
|
498
|
+
minNodeVersion?: string;
|
|
460
499
|
/**
|
|
461
500
|
* Runtime dependencies appended to the set derived from the bundle's
|
|
462
501
|
* `gi://` imports — the escape hatch for a namespace the table does not
|
|
@@ -506,6 +545,30 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
506
545
|
* produces a package that installs its translations and shows none of them.
|
|
507
546
|
*/
|
|
508
547
|
localeDir?: string;
|
|
548
|
+
/**
|
|
549
|
+
* A font file, or a directory of them, the application SHIPS (ADR 0038).
|
|
550
|
+
* Default: `data/fonts` when it exists.
|
|
551
|
+
*
|
|
552
|
+
* Staged into `share/fonts/<appId>/`, which is where fontconfig looks: under a
|
|
553
|
+
* `/usr` prefix through the stock `fonts.conf`'s unconditional
|
|
554
|
+
* `<dir>/usr/share/fonts</dir>`, and under every other prefix through
|
|
555
|
+
* `<dir prefix="xdg">fonts</dir>` plus the `XDG_DATA_DIRS` the launcher already
|
|
556
|
+
* exports. The app names the FAMILY in its styles and needs no install-layout
|
|
557
|
+
* knowledge, exactly as it needs none for icons or schemas.
|
|
558
|
+
*
|
|
559
|
+
* This is what a branded application needs: on a GNOME host the family is
|
|
560
|
+
* usually installed anyway, and on macOS, on Windows and on any Linux that is
|
|
561
|
+
* not GNOME it is not — so without shipping it Pango silently substitutes a
|
|
562
|
+
* fallback and the app merely looks wrong, at exit 0.
|
|
563
|
+
*
|
|
564
|
+
* `.woff` / `.woff2` / `.eot` are REFUSED. Whether FreeType opens a web-font
|
|
565
|
+
* wrapper is a BUILD option of the FreeType the app runs against (brotli for
|
|
566
|
+
* woff2, zlib for woff), so it is a property of the host's or the bundle's
|
|
567
|
+
* closure rather than of the file — and a font that resolves on the developer's
|
|
568
|
+
* box and not in the shipped bundle is the exact silent substitution this key
|
|
569
|
+
* exists to prevent. Ship the desktop face.
|
|
570
|
+
*/
|
|
571
|
+
fonts?: string;
|
|
509
572
|
/** Extra payload entries: prefix-relative destination → project-relative source. */
|
|
510
573
|
extraFiles?: Record<string, string>;
|
|
511
574
|
/** Arguments the launcher appends before the user's own. */
|
|
@@ -519,6 +582,69 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
519
582
|
* NOT move, is `utils/ship/flatpak-config.ts`.
|
|
520
583
|
*/
|
|
521
584
|
flatpak?: ShipFlatpakOptions;
|
|
585
|
+
/**
|
|
586
|
+
* The signing identity this project uses by default, per OS.
|
|
587
|
+
*
|
|
588
|
+
* `--sign <identity>` on the command line wins. Absent means unsigned, which
|
|
589
|
+
* is the default path and a legitimate output (ADR 0024 § A13).
|
|
590
|
+
*/
|
|
591
|
+
sign?: ShipSignOptions;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* `gjsify.ship.app` — the shipped runtime per OS, overriding `gjsify.app`.
|
|
595
|
+
*
|
|
596
|
+
* All three keys are present, which is where this differs from
|
|
597
|
+
* {@link ShipSignOptions}: `linux` is absent there because a `.deb` carries no
|
|
598
|
+
* per-file signature, while a Linux artifact very much has a runtime — it is the
|
|
599
|
+
* one that emits `Depends: gjs (>= 1.86)` or `Depends: nodejs (>= 24)`, and the
|
|
600
|
+
* whole point of this table is that stating darwin's answer must not move it.
|
|
601
|
+
*
|
|
602
|
+
* Only `gjs` and `node` are shippable. `browser` has no process to launch and
|
|
603
|
+
* `nativescript` ships as an APK/IPA through another pipeline (ADR 0024 § 4), so
|
|
604
|
+
* either one is refused here exactly as it is on `gjsify.app`.
|
|
605
|
+
*/
|
|
606
|
+
export interface ShipAppOptions {
|
|
607
|
+
/** What a `.deb`/`.rpm`/Flatpak depends on and what its launcher execs. */
|
|
608
|
+
linux?: 'gjs' | 'node';
|
|
609
|
+
/** What a `<App>.app` carries. `node` is the only value the macOS formats accept. */
|
|
610
|
+
darwin?: 'gjs' | 'node';
|
|
611
|
+
/** What a Windows program directory carries. `node` is the only value its formats accept. */
|
|
612
|
+
win32?: 'gjs' | 'node';
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* `gjsify.ship.sign` — an IDENTITY per OS, and never a certificate.
|
|
616
|
+
*
|
|
617
|
+
* KEYED PER OS, and the two keys must not be one field: a Developer ID string
|
|
618
|
+
* and an Authenticode subject name live in different namespaces, and the value
|
|
619
|
+
* that resolves is chosen by the LAYOUT being packed (`FormatDescriptor.layoutOs`
|
|
620
|
+
* and the stage manifest's `target.os`), not by the host doing the packing
|
|
621
|
+
* (ADR 0024 § A12).
|
|
622
|
+
*
|
|
623
|
+
* `linux` is deliberately absent: a `.deb` and an `.rpm` carry no per-file
|
|
624
|
+
* signature — the artifact is signed as a whole by the repository that serves
|
|
625
|
+
* it, with that repository's key. A key here would look like it did something.
|
|
626
|
+
*
|
|
627
|
+
* WHAT IS NOT HERE, and each is a decision rather than an omission:
|
|
628
|
+
* - a certificate, a `.p12` path or a password. `codesign` and `signtool` are
|
|
629
|
+
* handed a NAME and resolve the private key themselves, so `gjsify ship`
|
|
630
|
+
* never sees a secret and nothing on this surface has to be redacted from a
|
|
631
|
+
* log line (§ A14).
|
|
632
|
+
* - an `entitlements` key. § A16 leaves library validation vs. hardened runtime
|
|
633
|
+
* explicitly unmeasured, and a config key for an unmeasured mechanism is a
|
|
634
|
+
* promise the implementation cannot keep.
|
|
635
|
+
* - a notarisation credential. That is a per-RUN account credential, not a
|
|
636
|
+
* machine-held identity, and a config key for one is an invitation to commit
|
|
637
|
+
* it — `--notarize <keychain-profile>` is the only way to pass it.
|
|
638
|
+
*/
|
|
639
|
+
export interface ShipSignOptions {
|
|
640
|
+
/** `codesign --sign <identity>`. `'-'` is ad-hoc and needs no developer identity. */
|
|
641
|
+
darwin?: {
|
|
642
|
+
identity?: string;
|
|
643
|
+
};
|
|
644
|
+
/** `signtool sign /n <identity>` — the certificate's SUBJECT NAME in a Windows store. */
|
|
645
|
+
win32?: {
|
|
646
|
+
identity?: string;
|
|
647
|
+
};
|
|
522
648
|
}
|
|
523
649
|
/**
|
|
524
650
|
* The Flatpak knobs `gjsify ship` reads. Every one has a derived default.
|
|
@@ -36,9 +36,17 @@ import { detectNativePackages } from './detect-native-packages.js';
|
|
|
36
36
|
*
|
|
37
37
|
* `globalThis.imports?.gi` is both the sanctioned GJS probe spelling
|
|
38
38
|
* (`.oxlintrc.json`) and the correct CONDITION, so no `isGjs()` beside it: the
|
|
39
|
-
* question is "is there a GIRepository to prepend to", and a Node
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* question is "is there a GIRepository to prepend to", and a plain Node process has
|
|
40
|
+
* none — nothing here to write to, which is the required no-op.
|
|
41
|
+
*
|
|
42
|
+
* NOT because a Node host wants no prebuild. That justification stood here and is
|
|
43
|
+
* false exactly where it matters: on darwin `@gjsify/webkit-native`'s prebuild is
|
|
44
|
+
* the ONLY WebKit any host has (ADR 0022), and ADR 0024 § 4 puts macOS and Windows
|
|
45
|
+
* applications on Node + node-gi. What is true is narrower — the CLI on Node uses
|
|
46
|
+
* the npm `rolldown` crate — and generalising it to "a Node host never wants a
|
|
47
|
+
* prebuild" is what kept every OTHER Node prebuild env-bound until
|
|
48
|
+
* `@gjsify/node-gi`'s `native-prebuilds.js` (ADR 0021 § The Node host). The Node
|
|
49
|
+
* host is covered THERE, in the package that owns a GIRepository, not here.
|
|
42
50
|
*
|
|
43
51
|
* `dup_default()` is GLib >= 2.86, probed as a capability rather than caught:
|
|
44
52
|
* an older GLib simply keeps today's behaviour (the launcher's environment, or
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Which of msgfmt's two template writers to run. */
|
|
2
|
+
export type MsgfmtMergeMode = '--desktop' | '--xml';
|
|
3
|
+
/** One catalogue: the language tag msgfmt is TOLD, and the `.po` holding it. */
|
|
4
|
+
export interface MsgfmtCatalogue {
|
|
5
|
+
/** Passed as `--locale=`. Named explicitly so no `LINGUAS` file is needed — constraint 2. */
|
|
6
|
+
locale: string;
|
|
7
|
+
/** Path to a `.po`. msgfmt reads only this format here: a `.mo` gives `syntax error`, exit 1. */
|
|
8
|
+
po: string;
|
|
9
|
+
}
|
|
10
|
+
export interface MsgfmtMergeOptions {
|
|
11
|
+
mode: MsgfmtMergeMode;
|
|
12
|
+
/** The file the FIRST call substitutes into. Every later call substitutes into the previous output. */
|
|
13
|
+
template: string;
|
|
14
|
+
/** The suffix every intermediate keeps — load-bearing for `--xml`, see constraint 4. */
|
|
15
|
+
extension: string;
|
|
16
|
+
catalogues: readonly MsgfmtCatalogue[];
|
|
17
|
+
/** A directory this function may fill with intermediates; the caller owns its removal. */
|
|
18
|
+
workDir: string;
|
|
19
|
+
/** Called with each msgfmt argv, for `--verbose`. */
|
|
20
|
+
onCall?: (args: readonly string[]) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fold every catalogue into one file, chaining `msgfmt` once per language.
|
|
24
|
+
*
|
|
25
|
+
* Returns the PATH of the last file written — the caller decides whether to read
|
|
26
|
+
* it, post-process it or copy it. With no catalogues that is `template` itself,
|
|
27
|
+
* so a project with nothing to merge never spawns msgfmt at all.
|
|
28
|
+
*
|
|
29
|
+
* `stdio: 'pipe'` so a failure's stderr survives on the thrown error rather than
|
|
30
|
+
* going to the terminal and leaving the caller with an exit code.
|
|
31
|
+
*
|
|
32
|
+
* Throws when two catalogues name the same locale — constraint 5, which msgfmt
|
|
33
|
+
* itself reports as two successes and one invalid file.
|
|
34
|
+
*/
|
|
35
|
+
export declare function mergeCatalogues(options: MsgfmtMergeOptions): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Merging gettext catalogues INTO a template — the `msgfmt --desktop` / `--xml` half.
|
|
2
|
+
//
|
|
3
|
+
// Two callers need this and they are not variants of each other: `gjsify gettext`
|
|
4
|
+
// substitutes a template a user wrote, `gjsify ship` substitutes metadata it just
|
|
5
|
+
// rendered. What they share is every constraint below, all five measured against
|
|
6
|
+
// GNU gettext 0.26, and each one fails a DIFFERENT way — two of them silently.
|
|
7
|
+
// Written twice, the silent ones are the copies that lose them.
|
|
8
|
+
//
|
|
9
|
+
// 1. `--desktop` and `--xml` REQUIRE `--template`:
|
|
10
|
+
// `msgfmt: --desktop requires a "--template template" specification`, exit 1.
|
|
11
|
+
// Neither format can be produced from `.po` files alone, so a caller without a
|
|
12
|
+
// template has nothing to ask for.
|
|
13
|
+
//
|
|
14
|
+
// 2. The bulk form (`-d <podir>`) fails SILENTLY. With no `LINGUAS` file beside
|
|
15
|
+
// the `.po` files:
|
|
16
|
+
// msgfmt --desktop --template=app.desktop.in -d po -o out.desktop
|
|
17
|
+
// exit 0 · stderr `po/LINGUAS does not exist` · out.desktop UNTRANSLATED
|
|
18
|
+
// and `desktop-file-validate out.desktop` then exits 0 as well. Exit 0, an
|
|
19
|
+
// English file and a passing oracle is the worst of the three outcomes, which
|
|
20
|
+
// is why this module names each language with `--locale=` instead: that form
|
|
21
|
+
// needs no `LINGUAS` and cannot degrade to a no-op without a non-zero exit.
|
|
22
|
+
//
|
|
23
|
+
// 3. The template and the output must never be the SAME path. msgfmt truncates
|
|
24
|
+
// `--output-file` before it reads the template, so chaining in place destroys
|
|
25
|
+
// it: measured, `--desktop` wrote a 0-byte file and exited 0, `--xml` printed
|
|
26
|
+
// `cannot read <file>: Document is empty` and died on SIGSEGV (exit 139).
|
|
27
|
+
// Hence the numbered intermediates below rather than one file rewritten.
|
|
28
|
+
//
|
|
29
|
+
// 4. `--xml` finds its ITS rules by FILENAME PATTERN, not by reading the document.
|
|
30
|
+
// gettext walks `/usr/share/gettext/its/*.loc`, where each rule pairs a glob
|
|
31
|
+
// with a root element; AppStream's is `pattern="*.metainfo.xml"` +
|
|
32
|
+
// `localName="component"`, so the same bytes named `foo.xml` die with
|
|
33
|
+
// `msgfmt: cannot locate ITS rules for foo.xml` (exit 1). `extension` is
|
|
34
|
+
// therefore part of this function's contract, not a temp-file detail.
|
|
35
|
+
//
|
|
36
|
+
// Stated as "`--xml` needs a `.metainfo.xml` name" the rule would be wrong in
|
|
37
|
+
// the direction that matters next: `shared-mime-info.loc` pairs `pattern="*.xml"`
|
|
38
|
+
// with `localName="mime-info"`, so a MIME package needs no suffix care at all.
|
|
39
|
+
// The constraint belongs to the AppStream rule, not to `--xml`.
|
|
40
|
+
//
|
|
41
|
+
// 5. A locale may appear at most ONCE in a chain. Two calls naming the same
|
|
42
|
+
// `--locale` both APPEND — neither replaces the other's key — and the result
|
|
43
|
+
// is a file BOTH oracles reject, from two exit-0 msgfmt calls:
|
|
44
|
+
// msgfmt --desktop --template=app.desktop.in --locale=de -o d0 a.po # 0
|
|
45
|
+
// msgfmt --desktop --template=d0 --locale=de -o d1 b.po # 0
|
|
46
|
+
// desktop-file-validate d1
|
|
47
|
+
// → error: file contains multiple keys named "Name[de]" # 1
|
|
48
|
+
// appstreamcli validate --no-net x1.metainfo.xml (same shape, --xml)
|
|
49
|
+
// → E: tag-duplicated name (lang=de) · Validation failed # 3
|
|
50
|
+
// So this is worse than constraint 2: not an untranslated file that installs
|
|
51
|
+
// fine, but an INVALID one, produced without a single non-zero exit. It is
|
|
52
|
+
// refused here rather than deduplicated here, because the caller holding two
|
|
53
|
+
// catalogues for one language is the only party that can say which
|
|
54
|
+
// translation wins — `utils/ship/localize-metadata.ts` answers it with
|
|
55
|
+
// `msgcat --use-first`.
|
|
56
|
+
import { execFileSync } from 'node:child_process';
|
|
57
|
+
import { join } from 'node:path';
|
|
58
|
+
/**
|
|
59
|
+
* Fold every catalogue into one file, chaining `msgfmt` once per language.
|
|
60
|
+
*
|
|
61
|
+
* Returns the PATH of the last file written — the caller decides whether to read
|
|
62
|
+
* it, post-process it or copy it. With no catalogues that is `template` itself,
|
|
63
|
+
* so a project with nothing to merge never spawns msgfmt at all.
|
|
64
|
+
*
|
|
65
|
+
* `stdio: 'pipe'` so a failure's stderr survives on the thrown error rather than
|
|
66
|
+
* going to the terminal and leaving the caller with an exit code.
|
|
67
|
+
*
|
|
68
|
+
* Throws when two catalogues name the same locale — constraint 5, which msgfmt
|
|
69
|
+
* itself reports as two successes and one invalid file.
|
|
70
|
+
*/
|
|
71
|
+
export function mergeCatalogues(options) {
|
|
72
|
+
let current = options.template;
|
|
73
|
+
// BEFORE the first spawn, so a caller that got this wrong is told rather than
|
|
74
|
+
// handed a half-written chain to clean up. `seen` is a Set of the locale, not
|
|
75
|
+
// of the catalogue: the same language reached through two `.po` files is
|
|
76
|
+
// exactly the case that produces the duplicate key.
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const { locale } of options.catalogues) {
|
|
79
|
+
if (seen.has(locale)) {
|
|
80
|
+
throw new Error(`mergeCatalogues: two catalogues both claim locale "${locale}". msgfmt APPENDS each ` +
|
|
81
|
+
`--locale, so the chain would emit a duplicate key (desktop-file-validate: ` +
|
|
82
|
+
`"multiple keys named Name[${locale}]"; appstreamcli: "tag-duplicated") from two ` +
|
|
83
|
+
'exit-0 calls. Merge them into one .po first (msgcat --use-first) and pass that.');
|
|
84
|
+
}
|
|
85
|
+
seen.add(locale);
|
|
86
|
+
}
|
|
87
|
+
for (const [index, catalogue] of options.catalogues.entries()) {
|
|
88
|
+
const next = join(options.workDir, `${index}${options.extension}`);
|
|
89
|
+
const args = [
|
|
90
|
+
options.mode,
|
|
91
|
+
`--template=${current}`,
|
|
92
|
+
`--locale=${catalogue.locale}`,
|
|
93
|
+
`--output-file=${next}`,
|
|
94
|
+
catalogue.po,
|
|
95
|
+
];
|
|
96
|
+
options.onCall?.(args);
|
|
97
|
+
execFileSync('msgfmt', args, { stdio: 'pipe' });
|
|
98
|
+
// Every language after the first merges INTO what the previous one wrote.
|
|
99
|
+
current = next;
|
|
100
|
+
}
|
|
101
|
+
return current;
|
|
102
|
+
}
|
package/lib/utils/npm-otp.d.ts
CHANGED
|
@@ -31,6 +31,23 @@ export declare class OtpProvider {
|
|
|
31
31
|
private cached;
|
|
32
32
|
private readonly promptFn;
|
|
33
33
|
private readonly registry;
|
|
34
|
+
/**
|
|
35
|
+
* Bumped every time a NEW code becomes current. Concurrent callers compare the
|
|
36
|
+
* epoch they last saw against this one to tell "my code was rejected" from
|
|
37
|
+
* "somebody already replaced it while I was waiting" — the second needs no
|
|
38
|
+
* prompt, it needs the other one's code.
|
|
39
|
+
*/
|
|
40
|
+
private generation;
|
|
41
|
+
/**
|
|
42
|
+
* The prompt currently on screen, if any. THE fix for a sweep that probes with
|
|
43
|
+
* concurrency > 1: `promptLine` puts the terminal in RAW mode and echoes each
|
|
44
|
+
* keystroke by hand (utils/prompt.ts), so two live prompts mean two `data`
|
|
45
|
+
* listeners on the same stdin and every digit is echoed TWICE. Measured on a
|
|
46
|
+
* real 703-package sweep at the default concurrency of 4: five prompts stacked
|
|
47
|
+
* up and the typed code came back as `666644440000999944444444` — four echoes
|
|
48
|
+
* per keystroke. Whoever arrives second awaits this promise instead.
|
|
49
|
+
*/
|
|
50
|
+
private pending;
|
|
34
51
|
/**
|
|
35
52
|
* @param seed Initial cached code (from `--otp`), or undefined.
|
|
36
53
|
* @param promptFn How to read a fresh code. Defaults to the TTY-aware
|
|
@@ -41,17 +58,25 @@ export declare class OtpProvider {
|
|
|
41
58
|
constructor(seed?: string, promptFn?: OtpPromptFn, options?: OtpProviderOptions);
|
|
42
59
|
/** The currently cached OTP code, if any. */
|
|
43
60
|
current(): string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* How many times a new code has become current. Capture it before an attempt
|
|
63
|
+
* and hand it back to {@link refresh} / {@link invalidate} so a concurrent
|
|
64
|
+
* caller neither prompts for a code somebody already typed nor throws that
|
|
65
|
+
* code away on its own stale rejection.
|
|
66
|
+
*/
|
|
67
|
+
epoch(): number;
|
|
44
68
|
/**
|
|
45
69
|
* Prompt for a fresh code, cache it (in-process + file cache), and return the
|
|
46
70
|
* trimmed input — `''` when nothing was entered, which callers read as "give up".
|
|
47
71
|
*/
|
|
48
|
-
refresh(question?: string): Promise<string>;
|
|
72
|
+
refresh(question?: string, sinceEpoch?: number): Promise<string>;
|
|
73
|
+
private promptOnce;
|
|
49
74
|
/**
|
|
50
75
|
* Drop the cached code (in-process AND the cross-invocation file entry) after npm
|
|
51
76
|
* rejects it, so the NEXT command re-prompts instead of replaying a single-use
|
|
52
77
|
* code the registry already burned.
|
|
53
78
|
*/
|
|
54
|
-
invalidate(): void;
|
|
79
|
+
invalidate(sinceEpoch?: number): void;
|
|
55
80
|
}
|
|
56
81
|
export interface OtpRetryOptions {
|
|
57
82
|
/**
|