@gjsify/cli 0.59.0 → 0.60.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 +124 -122
- package/lib/commands/install.d.ts +10 -0
- package/lib/commands/install.js +24 -6
- package/lib/commands/publish.d.ts +25 -0
- package/lib/commands/publish.js +108 -2
- package/lib/utils/discover-showcases.js +5 -0
- package/lib/utils/gjsify-shim.d.ts +18 -0
- package/lib/utils/gjsify-shim.js +70 -21
- package/lib/utils/publish-hold.d.ts +89 -0
- package/lib/utils/publish-hold.js +213 -0
- package/lib/utils/publish-status.d.ts +83 -0
- package/lib/utils/publish-status.js +193 -0
- package/lib/utils/ship/app-runtime.d.ts +3 -2
- package/lib/utils/ship/app-runtime.js +3 -2
- package/package.json +20 -20
- package/showcases.json +0 -42
|
@@ -21,6 +21,16 @@ interface InstallOptions {
|
|
|
21
21
|
force?: boolean;
|
|
22
22
|
}
|
|
23
23
|
export declare const installCommand: Command<unknown, InstallOptions>;
|
|
24
|
+
/**
|
|
25
|
+
* Version of the `@gjsify/cli` the workspace at `cwd` pins: the one DECLARED in
|
|
26
|
+
* its package.json (`^`/`~`/`=` prefix stripped; only a plain version counts).
|
|
27
|
+
* The installed copy is consulted only when package.json declares no
|
|
28
|
+
* `@gjsify/cli` at all — `install` replaces a stale `node_modules`, so it must
|
|
29
|
+
* not override a declared pin (that flagged the very state being repaired).
|
|
30
|
+
* A declared non-version spec (`workspace:^`, a range, a tag) falls back to the in-repo
|
|
31
|
+
* source so the check still fires on a fresh checkout of the gjsify monorepo.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readWorkspaceCliVersion(cwd: string): string | null;
|
|
24
34
|
/**
|
|
25
35
|
* Which dependency NAMES are optional across the given manifests.
|
|
26
36
|
*
|
package/lib/commands/install.js
CHANGED
|
@@ -350,13 +350,31 @@ function readRunningCliVersion() {
|
|
|
350
350
|
return null;
|
|
351
351
|
}
|
|
352
352
|
/**
|
|
353
|
-
* Version of the `@gjsify/cli` the workspace at `cwd` pins
|
|
354
|
-
*
|
|
355
|
-
*
|
|
353
|
+
* Version of the `@gjsify/cli` the workspace at `cwd` pins: the one DECLARED in
|
|
354
|
+
* its package.json (`^`/`~`/`=` prefix stripped; only a plain version counts).
|
|
355
|
+
* The installed copy is consulted only when package.json declares no
|
|
356
|
+
* `@gjsify/cli` at all — `install` replaces a stale `node_modules`, so it must
|
|
357
|
+
* not override a declared pin (that flagged the very state being repaired).
|
|
358
|
+
* A declared non-version spec (`workspace:^`, a range, a tag) falls back to the in-repo
|
|
359
|
+
* source so the check still fires on a fresh checkout of the gjsify monorepo.
|
|
356
360
|
*/
|
|
357
|
-
function readWorkspaceCliVersion(cwd) {
|
|
358
|
-
|
|
359
|
-
|
|
361
|
+
export function readWorkspaceCliVersion(cwd) {
|
|
362
|
+
const pkg = readPackageJson(join(cwd, 'package.json'));
|
|
363
|
+
let declared = false;
|
|
364
|
+
for (const kind of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
365
|
+
const spec = pkg?.[kind]?.[CLI_PACKAGE_NAME];
|
|
366
|
+
if (typeof spec === 'string')
|
|
367
|
+
declared = true;
|
|
368
|
+
const m = typeof spec === 'string' ? /^[\^~=v]*(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(spec.trim()) : null;
|
|
369
|
+
if (m)
|
|
370
|
+
return m[1];
|
|
371
|
+
}
|
|
372
|
+
if (!declared) {
|
|
373
|
+
const installed = readCliVersionFrom(join(cwd, 'node_modules', CLI_PACKAGE_NAME, 'package.json'));
|
|
374
|
+
if (installed)
|
|
375
|
+
return installed;
|
|
376
|
+
}
|
|
377
|
+
return readCliVersionFrom(join(cwd, 'packages', 'infra', 'cli', 'package.json'));
|
|
360
378
|
}
|
|
361
379
|
/**
|
|
362
380
|
* Warn (never block) when the running gjsify CLI differs from the `@gjsify/cli`
|
|
@@ -2,6 +2,8 @@ import type { Command } from '../types/index.js';
|
|
|
2
2
|
import { type Diagnose404Result } from '../utils/publish-diagnose.js';
|
|
3
3
|
import { type ReadbackResult } from '../utils/publish-readback.js';
|
|
4
4
|
import { type WorkspacePin } from '../utils/publish-pins.js';
|
|
5
|
+
import { type HoldReason } from '../utils/publish-hold.js';
|
|
6
|
+
import { type Classification } from '../utils/publish-status.js';
|
|
5
7
|
import { OtpProvider } from '../utils/npm-otp.js';
|
|
6
8
|
interface PublishOptions {
|
|
7
9
|
path?: string;
|
|
@@ -12,6 +14,9 @@ interface PublishOptions {
|
|
|
12
14
|
'tolerate-untrusted-new'?: boolean;
|
|
13
15
|
'verify-timeout'?: number;
|
|
14
16
|
'verify-defer'?: boolean;
|
|
17
|
+
'hold-timeout'?: number;
|
|
18
|
+
'report-ledger'?: boolean;
|
|
19
|
+
ref?: string;
|
|
15
20
|
provenance?: boolean;
|
|
16
21
|
'dry-run'?: boolean;
|
|
17
22
|
json?: boolean;
|
|
@@ -60,6 +65,11 @@ export interface PublishWorkspaceInput {
|
|
|
60
65
|
* success, which is the pre-v0.46.0 behaviour and why this defaults on.
|
|
61
66
|
*/
|
|
62
67
|
verifyBudgetMs?: number;
|
|
68
|
+
/**
|
|
69
|
+
* How long to wait for an intra-repo pin the registry does not serve before
|
|
70
|
+
* holding this package. Default {@link DEFAULT_HOLD_WAIT_MS}.
|
|
71
|
+
*/
|
|
72
|
+
holdWaitMs?: number;
|
|
63
73
|
}
|
|
64
74
|
/** Discriminated result of {@link publishWorkspace}; the caller owns presentation. */
|
|
65
75
|
export type PublishOutcome = {
|
|
@@ -101,6 +111,21 @@ export type PublishOutcome = {
|
|
|
101
111
|
putUrl: string;
|
|
102
112
|
payloadBytes: number;
|
|
103
113
|
readback: ReadbackResult;
|
|
114
|
+
/** What the unserved version IS (validating / staged / blocked / …). */
|
|
115
|
+
classification: Classification;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The tarball was NOT sent, because an intra-repo pin in it names a version
|
|
119
|
+
* the registry does not serve — an accepted-but-"validating" upload, say.
|
|
120
|
+
* `ok` is the tolerance, as for `blocked-missing-dependency`.
|
|
121
|
+
*/
|
|
122
|
+
| {
|
|
123
|
+
ok: false;
|
|
124
|
+
action: 'held-unpublished-dependency';
|
|
125
|
+
name: string;
|
|
126
|
+
version: string;
|
|
127
|
+
registry: string;
|
|
128
|
+
reasons: HoldReason[];
|
|
104
129
|
} | {
|
|
105
130
|
ok: true;
|
|
106
131
|
action: 'republish-tolerated';
|
package/lib/commands/publish.js
CHANGED
|
@@ -50,6 +50,8 @@ import { getNpmTrustedToken, hasGithubOidcEnv, OidcExchangeError, OidcUnavailabl
|
|
|
50
50
|
import { diagnose404, is404DiagnosticCandidate } from '../utils/publish-diagnose.js';
|
|
51
51
|
import { DEFAULT_VERIFY_BUDGET_MS, formatUnconfirmedPublish, probePackageName, verifyPublishedVersion, } from '../utils/publish-readback.js';
|
|
52
52
|
import { collectWorkspacePins, REQUIRED_BLOCK } from '../utils/publish-pins.js';
|
|
53
|
+
import { appendLedger, DEFAULT_HOLD_WAIT_MS, findUnavailablePins, holdsPublish, readLedger, recordUnserved, renderLedgerSummary, summariseReasons, } from '../utils/publish-hold.js';
|
|
54
|
+
import { describeClass, statusTokenFromEnv } from '../utils/publish-status.js';
|
|
53
55
|
import { loadNpmrc } from '../utils/load-npmrc.js';
|
|
54
56
|
import { OtpProvider, withOtpRetry, isOtpChallenge } from '../utils/npm-otp.js';
|
|
55
57
|
import { promptLine } from '../utils/prompt.js';
|
|
@@ -90,6 +92,20 @@ export const publishCommand = {
|
|
|
90
92
|
description: 'Report an unconfirmed read-back and exit 0 instead of 1. ONLY for a caller that re-checks the same set afterwards — `gjsify run npm:publish` passes it because `scripts/verify-published-closure.mjs` sweeps the whole roster at the end of release.yml. Everywhere else an unconfirmed publish must fail the job that made it.',
|
|
91
93
|
type: 'boolean',
|
|
92
94
|
default: false,
|
|
95
|
+
})
|
|
96
|
+
.option('hold-timeout', {
|
|
97
|
+
description: 'Seconds to wait for an intra-repo dependency pin whose exact version the registry does not serve (yet) before HOLDING this package instead of publishing it. A version npm accepted but never serves (status "validating") would make every dependent uninstallable; the write lag measured on v0.46.0 reaches 252s, hence the default. `0` probes once and holds without waiting. A hold exits 1, or 0 under `--verify-defer`.',
|
|
98
|
+
type: 'number',
|
|
99
|
+
default: DEFAULT_HOLD_WAIT_MS / 1000,
|
|
100
|
+
})
|
|
101
|
+
.option('report-ledger', {
|
|
102
|
+
description: 'Print the job-summary markdown for the publish ledger named by $GJSIFY_PUBLISH_LEDGER (versions npm accepted but does not serve, packages held because of them, the recovery command) and exit. Pair with `--ref <tag>`.',
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
default: false,
|
|
105
|
+
})
|
|
106
|
+
.option('ref', {
|
|
107
|
+
description: 'With --report-ledger: the git ref to put in the printed recovery command.',
|
|
108
|
+
type: 'string',
|
|
93
109
|
})
|
|
94
110
|
.option('provenance', {
|
|
95
111
|
description: "Pass-through flag — recorded in the payload but no signing happens (gjsify doesn't ship a sigstore signer yet).",
|
|
@@ -142,6 +158,17 @@ export const publishCommand = {
|
|
|
142
158
|
const checkTrustedOnly = args['check-trusted'] === true;
|
|
143
159
|
const trustedFlag = args.trusted;
|
|
144
160
|
const verbose = Boolean(process.env.GJSIFY_PUBLISH_DEBUG);
|
|
161
|
+
if (args['report-ledger'] === true) {
|
|
162
|
+
const summary = renderLedgerSummary(readLedger(process.env.GJSIFY_PUBLISH_LEDGER), args.ref ?? '<tag>');
|
|
163
|
+
if (summary)
|
|
164
|
+
process.stdout.write(`${summary}\n`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const holdSeconds = args['hold-timeout'] ?? DEFAULT_HOLD_WAIT_MS / 1000;
|
|
168
|
+
if (!Number.isFinite(holdSeconds) || holdSeconds < 0) {
|
|
169
|
+
console.error(`gjsify publish: --hold-timeout must be a non-negative number of seconds, got ${JSON.stringify(args['hold-timeout'])}`);
|
|
170
|
+
return process.exit(2);
|
|
171
|
+
}
|
|
145
172
|
if (provenance) {
|
|
146
173
|
console.warn('gjsify publish: --provenance recorded but not signed (no sigstore integration yet).');
|
|
147
174
|
}
|
|
@@ -250,6 +277,7 @@ export const publishCommand = {
|
|
|
250
277
|
otpProvider,
|
|
251
278
|
seedOtpFirst: otp !== undefined,
|
|
252
279
|
verifyBudgetMs,
|
|
280
|
+
holdWaitMs: Math.round(holdSeconds * 1000),
|
|
253
281
|
});
|
|
254
282
|
reportPublishOutcome(outcome, args.json === true, deferUnconfirmed);
|
|
255
283
|
},
|
|
@@ -355,6 +383,43 @@ export async function publishWorkspace(input) {
|
|
|
355
383
|
};
|
|
356
384
|
}
|
|
357
385
|
}
|
|
386
|
+
// 5b. Do not PUT a tarball whose pin names a VERSION the registry does not
|
|
387
|
+
// serve (v0.59.x: adwaita-fonts "Validating" while its dependents shipped).
|
|
388
|
+
// Unlike 5 this waits, because a version is late for minutes after its own
|
|
389
|
+
// 2xx; see publish-hold.ts.
|
|
390
|
+
const ledgerPath = process.env.GJSIFY_PUBLISH_LEDGER;
|
|
391
|
+
if (pins.length > 0) {
|
|
392
|
+
const reasons = await findUnavailablePins({
|
|
393
|
+
registry: registryClean,
|
|
394
|
+
pins,
|
|
395
|
+
ledger: readLedger(ledgerPath),
|
|
396
|
+
ledgerPath,
|
|
397
|
+
authorization: headers['authorization'],
|
|
398
|
+
statusToken: statusTokenFromEnv(process.env),
|
|
399
|
+
waitBudgetMs: input.holdWaitMs ?? DEFAULT_HOLD_WAIT_MS,
|
|
400
|
+
log: verbose ? (m) => console.error(m) : undefined,
|
|
401
|
+
});
|
|
402
|
+
for (const r of reasons.filter((x) => !holdsPublish(x))) {
|
|
403
|
+
console.error(`gjsify publish: ${r.block}.${r.name}@${r.version} is not served (${r.detail}) — optional edge, publishing anyway`);
|
|
404
|
+
}
|
|
405
|
+
const holding = reasons.filter(holdsPublish);
|
|
406
|
+
if (holding.length > 0) {
|
|
407
|
+
appendLedger(ledgerPath, {
|
|
408
|
+
ev: 'held',
|
|
409
|
+
name: packed.name,
|
|
410
|
+
version: packed.version,
|
|
411
|
+
reasons: holding,
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
action: 'held-unpublished-dependency',
|
|
416
|
+
name: packed.name,
|
|
417
|
+
version: packed.version,
|
|
418
|
+
registry: registryClean,
|
|
419
|
+
reasons: holding,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
358
423
|
// Trusted Publishing (OIDC) path — `--trusted` forces it, `undefined`
|
|
359
424
|
// auto-detects from the GitHub OIDC env. Unused by `gjsify onboard` (local
|
|
360
425
|
// token auth), preserved verbatim for `gjsify publish` in CI.
|
|
@@ -529,6 +594,14 @@ export async function publishWorkspace(input) {
|
|
|
529
594
|
putUrl: url,
|
|
530
595
|
payloadBytes: bodyStr.length,
|
|
531
596
|
readback,
|
|
597
|
+
classification: await recordUnserved({
|
|
598
|
+
ledgerPath,
|
|
599
|
+
registry: registryClean,
|
|
600
|
+
name: packed.name,
|
|
601
|
+
version: packed.version,
|
|
602
|
+
verdict: readback.verdict,
|
|
603
|
+
statusToken: statusTokenFromEnv(process.env),
|
|
604
|
+
}),
|
|
532
605
|
};
|
|
533
606
|
}
|
|
534
607
|
return {
|
|
@@ -576,6 +649,14 @@ export async function publishWorkspace(input) {
|
|
|
576
649
|
putUrl: url,
|
|
577
650
|
payloadBytes: bodyStr.length,
|
|
578
651
|
readback,
|
|
652
|
+
classification: await recordUnserved({
|
|
653
|
+
ledgerPath,
|
|
654
|
+
registry: registryClean,
|
|
655
|
+
name: packed.name,
|
|
656
|
+
version: packed.version,
|
|
657
|
+
verdict: readback.verdict,
|
|
658
|
+
statusToken: statusTokenFromEnv(process.env),
|
|
659
|
+
}),
|
|
579
660
|
};
|
|
580
661
|
}
|
|
581
662
|
return {
|
|
@@ -701,8 +782,31 @@ function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
|
701
782
|
warnUnverified(outcome.name, outcome.version, outcome.registry);
|
|
702
783
|
return;
|
|
703
784
|
}
|
|
785
|
+
case 'held-unpublished-dependency': {
|
|
786
|
+
const because = summariseReasons(outcome.reasons);
|
|
787
|
+
if (asJson) {
|
|
788
|
+
process.stdout.write(`${JSON.stringify({
|
|
789
|
+
ok: false,
|
|
790
|
+
action: 'held-unpublished-dependency',
|
|
791
|
+
name: outcome.name,
|
|
792
|
+
version: outcome.version,
|
|
793
|
+
registry: outcome.registry,
|
|
794
|
+
reasons: outcome.reasons,
|
|
795
|
+
deferred: deferUnconfirmed,
|
|
796
|
+
}, null, 2)}\n`);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
process.stderr.write(`~ ${outcome.name}@${outcome.version} (HELD — not published: ${because})\n`);
|
|
800
|
+
}
|
|
801
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
802
|
+
process.stderr.write(`::warning title=Package held::${outcome.name}@${outcome.version} was NOT published: ${because}\n`);
|
|
803
|
+
}
|
|
804
|
+
// The sweep defers like an unconfirmed publish: the closure job goes red
|
|
805
|
+
// on the missing package, and the job summary carries the recovery.
|
|
806
|
+
return deferUnconfirmed ? undefined : process.exit(1);
|
|
807
|
+
}
|
|
704
808
|
case 'publish-unconfirmed': {
|
|
705
|
-
const message = formatUnconfirmedPublish(outcome)
|
|
809
|
+
const message = `${formatUnconfirmedPublish(outcome)}\n state: ${describeClass(outcome.classification.cls)} — ${outcome.classification.detail}`;
|
|
706
810
|
if (asJson) {
|
|
707
811
|
process.stdout.write(`${JSON.stringify({
|
|
708
812
|
ok: false,
|
|
@@ -720,6 +824,8 @@ function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
|
720
824
|
verdictDetail: outcome.readback.verdictDetail,
|
|
721
825
|
recordedAt: outcome.readback.recordedAt ?? null,
|
|
722
826
|
deferred: deferUnconfirmed,
|
|
827
|
+
class: outcome.classification.cls,
|
|
828
|
+
classDetail: outcome.classification.detail,
|
|
723
829
|
}, null, 2)}\n`);
|
|
724
830
|
}
|
|
725
831
|
else {
|
|
@@ -736,7 +842,7 @@ function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
|
736
842
|
const claimed = outcome.claim === 'already-published' ? 'was reported already published' : 'was accepted';
|
|
737
843
|
process.stderr.write(`::warning title=Publish unconfirmed::${outcome.name}@${outcome.version} ${claimed} ` +
|
|
738
844
|
`(HTTP ${outcome.putStatus}) but did not resolve on ${outcome.registry} within ` +
|
|
739
|
-
`${(outcome.readback.elapsedMs / 1000).toFixed(1)}s [${outcome.readback.verdict}] — ` +
|
|
845
|
+
`${(outcome.readback.elapsedMs / 1000).toFixed(1)}s [${outcome.readback.verdict}; ${describeClass(outcome.classification.cls)}] — ` +
|
|
740
846
|
'the release-closure job must confirm it\n');
|
|
741
847
|
}
|
|
742
848
|
return;
|
|
@@ -7,6 +7,11 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
function manifestPath() {
|
|
10
|
+
// Test seam: e2e points this at a fixture catalog (a gjs-only workspace showcase),
|
|
11
|
+
// since the curated list no longer holds one.
|
|
12
|
+
const override = process.env.GJSIFY_SHOWCASES_MANIFEST;
|
|
13
|
+
if (override)
|
|
14
|
+
return override;
|
|
10
15
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
16
|
// `showcases.json` sits at the CLI package root, but its depth below this
|
|
12
17
|
// module depends on how the CLI was loaded: two levels from
|
|
@@ -41,6 +41,24 @@ export declare function needsSelfShim(opts: {
|
|
|
41
41
|
* reuse it instead of creating their own.
|
|
42
42
|
*/
|
|
43
43
|
export declare function ensureGjsifyShimOnPath(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Write `files` (relative path → content) into a directory NAMED BY THEIR HASH and
|
|
46
|
+
* return it. Same content, same directory: a run finds what the last one wrote and
|
|
47
|
+
* writes nothing, so shim dirs no longer pile up one per invocation (5930 in a
|
|
48
|
+
* `/tmp`, none ever removed).
|
|
49
|
+
*
|
|
50
|
+
* A reused dir, rather than one removed on exit, is what lets a child outlive its
|
|
51
|
+
* parent: a detached grandchild keeps resolving `gjsify` through the inherited PATH
|
|
52
|
+
* after the CLI that made the shim is gone, and a dir that is deleted at exit
|
|
53
|
+
* breaks it. Content-addressed means a file in it never changes, so a running
|
|
54
|
+
* process can never see a different shim than the one it was started with; each
|
|
55
|
+
* member lands by tmp+`rename`, so a concurrent run sees it whole or not at all.
|
|
56
|
+
*
|
|
57
|
+
* Lives under `$XDG_CACHE_HOME/gjsify/shim` — a `/tmp` path is world-writable, and
|
|
58
|
+
* a PATH entry there lets another local user plant a `gjsify`. A cache dir that
|
|
59
|
+
* cannot be written (read-only HOME) falls back to a private `mkdtemp` dir.
|
|
60
|
+
*/
|
|
61
|
+
export declare function materializeShimDir(files: Record<string, string>, root?: string): string;
|
|
44
62
|
/**
|
|
45
63
|
* The tree whose `node_modules` holds the running CLI and its prebuilds: the
|
|
46
64
|
* install prefix above the first `node_modules` segment, or the workspace root
|
package/lib/utils/gjsify-shim.js
CHANGED
|
@@ -39,13 +39,15 @@
|
|
|
39
39
|
// came through the workspace's own `node_modules/.bin/gjsify`, that shim IS the CLI
|
|
40
40
|
// the tree wants. Widening this to every Node invocation would silently move in-repo
|
|
41
41
|
// nested builds off the GJS bundle.
|
|
42
|
-
import {
|
|
42
|
+
import { createHash } from 'node:crypto';
|
|
43
|
+
import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, existsSync, readFileSync, renameSync, rmSync, } from 'node:fs';
|
|
43
44
|
import { tmpdir } from 'node:os';
|
|
44
45
|
import { join, delimiter, relative, isAbsolute, dirname } from 'node:path';
|
|
45
46
|
import { fileURLToPath } from 'node:url';
|
|
46
47
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
47
48
|
import { buildLauncherShims, buildNativeEnvPreamble, buildShLauncher, shQuoteArg } from './bin-shim.js';
|
|
48
49
|
import { detectNativePackages } from './detect-native-packages.js';
|
|
50
|
+
import { gjsifyCacheRoot } from './install-cache-fs.js';
|
|
49
51
|
import { resolveBinOnPath } from './install-global.js';
|
|
50
52
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
51
53
|
let _ensured = false;
|
|
@@ -128,8 +130,6 @@ export function ensureGjsifyShimOnPath() {
|
|
|
128
130
|
const workspaceRoot = findWorkspaceRoot(process.cwd()) ?? process.cwd();
|
|
129
131
|
if (!needsSelfShim({ gjs, selfEntry, workspaceRoot }))
|
|
130
132
|
return;
|
|
131
|
-
const dir = mkdtempSync(join(tmpdir(), 'gjsify-shim-'));
|
|
132
|
-
const shim = join(dir, 'gjsify');
|
|
133
133
|
// The interpreter is NAMED, not pathed, matching `buildShLauncher` and
|
|
134
134
|
// `buildLauncherShims`, so a missing one fails the same way on every OS. Not
|
|
135
135
|
// `process.execPath`: the batch member cannot quote an interpreter argument
|
|
@@ -143,23 +143,78 @@ export function ensureGjsifyShimOnPath() {
|
|
|
143
143
|
// beifahrer consumer on macOS: `gjsify workspace <pkg> build` died in
|
|
144
144
|
// `get columns` with "Unsupported type void".
|
|
145
145
|
const envPreamble = gjs ? selfShimPreamble(selfEntry) : '';
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const files = {
|
|
147
|
+
gjsify: gjs
|
|
148
|
+
? buildShLauncher(selfEntry, { isGjsBundle: true, envPreamble, gjs: interpreter })
|
|
149
|
+
: `#!/bin/sh\nexec "${interpreter}" "${selfEntry}" "$@"\n`,
|
|
150
|
+
};
|
|
150
151
|
// cmd.exe and pwsh cannot run the extension-less member: not on PATHEXT, and
|
|
151
152
|
// Windows has no shebang handling. `buildLauncherShims` ports npm's
|
|
152
153
|
// three-sibling answer, including the IF/ELSE ERRORLEVEL handling that
|
|
153
154
|
// `&&`/`||` chaining gets wrong.
|
|
154
155
|
if (!gjs && process.platform === 'win32') {
|
|
155
156
|
const { cmd, ps1 } = buildLauncherShims({ interpreter, interpreterArgs: [], target: selfEntry });
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
files['gjsify.cmd'] = cmd;
|
|
158
|
+
files['gjsify.ps1'] = ps1;
|
|
158
159
|
}
|
|
160
|
+
const nodeShim = buildNodeShim(gjs, selfEntry, envPreamble);
|
|
161
|
+
if (nodeShim)
|
|
162
|
+
files[`${NODE_SHIM_SUBDIR}/node`] = nodeShim;
|
|
163
|
+
const dir = materializeShimDir(files);
|
|
159
164
|
_selfShimActive = true;
|
|
160
165
|
process.env.GJSIFY_SHIM_DIR = dir;
|
|
161
166
|
process.env.PATH = dir + delimiter + (process.env.PATH ?? '');
|
|
162
|
-
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Write `files` (relative path → content) into a directory NAMED BY THEIR HASH and
|
|
170
|
+
* return it. Same content, same directory: a run finds what the last one wrote and
|
|
171
|
+
* writes nothing, so shim dirs no longer pile up one per invocation (5930 in a
|
|
172
|
+
* `/tmp`, none ever removed).
|
|
173
|
+
*
|
|
174
|
+
* A reused dir, rather than one removed on exit, is what lets a child outlive its
|
|
175
|
+
* parent: a detached grandchild keeps resolving `gjsify` through the inherited PATH
|
|
176
|
+
* after the CLI that made the shim is gone, and a dir that is deleted at exit
|
|
177
|
+
* breaks it. Content-addressed means a file in it never changes, so a running
|
|
178
|
+
* process can never see a different shim than the one it was started with; each
|
|
179
|
+
* member lands by tmp+`rename`, so a concurrent run sees it whole or not at all.
|
|
180
|
+
*
|
|
181
|
+
* Lives under `$XDG_CACHE_HOME/gjsify/shim` — a `/tmp` path is world-writable, and
|
|
182
|
+
* a PATH entry there lets another local user plant a `gjsify`. A cache dir that
|
|
183
|
+
* cannot be written (read-only HOME) falls back to a private `mkdtemp` dir.
|
|
184
|
+
*/
|
|
185
|
+
export function materializeShimDir(files, root = gjsifyCacheRoot('shim', 'v1')) {
|
|
186
|
+
const names = Object.keys(files).sort();
|
|
187
|
+
const hash = createHash('sha256');
|
|
188
|
+
for (const name of names)
|
|
189
|
+
hash.update(`${name}\0${files[name]}\0`);
|
|
190
|
+
const key = hash.digest('hex').slice(0, 16);
|
|
191
|
+
try {
|
|
192
|
+
return writeShimDir(join(root, key), files, names);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Unwritable cache (read-only HOME, full disk): the shim is still needed
|
|
196
|
+
// for this run, so take the per-run dir this function exists to avoid.
|
|
197
|
+
return writeShimDir(mkdtempSync(join(tmpdir(), 'gjsify-shim-')), files, names);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function writeShimDir(dir, files, names) {
|
|
201
|
+
for (const name of names) {
|
|
202
|
+
const path = join(dir, name);
|
|
203
|
+
if (existsSync(path) && readFileSync(path, 'utf8') === files[name])
|
|
204
|
+
continue;
|
|
205
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
206
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
207
|
+
try {
|
|
208
|
+
writeFileSync(tmp, files[name], { mode: 0o755 });
|
|
209
|
+
chmodSync(tmp, 0o755);
|
|
210
|
+
renameSync(tmp, path);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
rmSync(tmp, { force: true });
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return dir;
|
|
163
218
|
}
|
|
164
219
|
/**
|
|
165
220
|
* The tree whose `node_modules` holds the running CLI and its prebuilds: the
|
|
@@ -268,22 +323,17 @@ export function nodeShimDir() {
|
|
|
268
323
|
* resolves NOWHERE, and it is reachable only from a package script's PATH (see
|
|
269
324
|
* {@link nodeShimDir}).
|
|
270
325
|
*/
|
|
271
|
-
function
|
|
326
|
+
function buildNodeShim(gjs, selfEntry, envPreamble) {
|
|
272
327
|
if (!gjs)
|
|
273
|
-
return;
|
|
274
|
-
const sub = join(dir, NODE_SHIM_SUBDIR);
|
|
275
|
-
if (existsSync(join(sub, 'node')))
|
|
276
|
-
return; // inherited from a parent gjsify
|
|
328
|
+
return null;
|
|
277
329
|
if (resolveBinOnPath('node'))
|
|
278
|
-
return; // a real Node is present — leave it alone
|
|
330
|
+
return null; // a real Node is present — leave it alone
|
|
279
331
|
const interpreter = process.env.GJS_CONSOLE || 'gjs';
|
|
280
|
-
mkdirSync(sub, { recursive: true });
|
|
281
|
-
const shim = join(sub, 'node');
|
|
282
332
|
// A leading FLAG is refused rather than forwarded: `node --test x.mjs` wants
|
|
283
333
|
// Node's own test runner and `node -e '…'` an eval, neither of which
|
|
284
334
|
// `--node-script` can honour, and yargs would take `--test` FOR the script
|
|
285
335
|
// path (`unknown-options-as-args` turns an unknown flag into a positional).
|
|
286
|
-
|
|
336
|
+
return ('#!/bin/sh\n' +
|
|
287
337
|
// Same SIP hop as the self-shim — see `buildSelfShimScript`.
|
|
288
338
|
dyldCarryPreamble(process.platform, process.env) +
|
|
289
339
|
'case "$1" in\n' +
|
|
@@ -293,6 +343,5 @@ function writeNodeShim(dir, gjs, selfEntry, envPreamble) {
|
|
|
293
343
|
'esac\n' +
|
|
294
344
|
// The same hop as the `gjsify` shim, so the same preamble.
|
|
295
345
|
envPreamble +
|
|
296
|
-
`exec "${interpreter}" -m "${selfEntry}" run --node-script "$@"\n
|
|
297
|
-
chmodSync(shim, 0o755);
|
|
346
|
+
`exec "${interpreter}" -m "${selfEntry}" run --node-script "$@"\n`);
|
|
298
347
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type DependencyBlock, type WorkspacePin } from './publish-pins.js';
|
|
2
|
+
import { classifyUnserved, type Classification, type UnservedClass } from './publish-status.js';
|
|
3
|
+
/** Wait for a pin this long before holding. Covers the measured 252 s write lag (publish-readback.ts). */
|
|
4
|
+
export declare const DEFAULT_HOLD_WAIT_MS = 300000;
|
|
5
|
+
export interface HoldReason {
|
|
6
|
+
/** The dependency that is not installable. */
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
block: DependencyBlock;
|
|
10
|
+
/** `held` = the dependency was itself held in this run, so nothing of it was published. */
|
|
11
|
+
cls: UnservedClass | 'held';
|
|
12
|
+
detail: string;
|
|
13
|
+
}
|
|
14
|
+
export type LedgerEntry =
|
|
15
|
+
/** This run PUT the version, npm accepted it, and the packument does not serve it. */
|
|
16
|
+
{
|
|
17
|
+
ev: 'unserved';
|
|
18
|
+
name: string;
|
|
19
|
+
version: string;
|
|
20
|
+
cls: UnservedClass;
|
|
21
|
+
detail: string;
|
|
22
|
+
}
|
|
23
|
+
/** This run did NOT publish the package, because of the listed dependencies. */
|
|
24
|
+
| {
|
|
25
|
+
ev: 'held';
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
reasons: HoldReason[];
|
|
29
|
+
}
|
|
30
|
+
/** A later probe found a version an earlier `unserved` entry recorded now served; it supersedes that entry. */
|
|
31
|
+
| {
|
|
32
|
+
ev: 'served';
|
|
33
|
+
name: string;
|
|
34
|
+
version: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function appendLedger(path: string | undefined, entry: LedgerEntry): void;
|
|
37
|
+
/** Tolerates a missing file (no entries yet) and skips unparseable lines rather than losing the rest. */
|
|
38
|
+
export declare function readLedger(path: string | undefined): LedgerEntry[];
|
|
39
|
+
/**
|
|
40
|
+
* The version a rewritten pin asks for, when the spec names exactly one:
|
|
41
|
+
* `0.59.1`, `^0.59.1`, `~0.59.1`. `undefined` for a range, tag or URL — those are
|
|
42
|
+
* not decidable against one version and the name-level guard covers them.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pinVersion(spec: string): string | undefined;
|
|
45
|
+
export interface FindUnavailablePinsInput {
|
|
46
|
+
registry: string;
|
|
47
|
+
pins: readonly WorkspacePin[];
|
|
48
|
+
ledger: readonly LedgerEntry[];
|
|
49
|
+
/** Where a re-probe that finds a ledger-`unserved` version served records it, so later dependents skip the probe. */
|
|
50
|
+
ledgerPath?: string;
|
|
51
|
+
/** Forwarded to the packument GETs, as the publish's own read-back does. */
|
|
52
|
+
authorization?: string;
|
|
53
|
+
statusToken?: string;
|
|
54
|
+
/** How long to wait for a pin that may merely be late. 0 = one probe, no waiting. */
|
|
55
|
+
waitBudgetMs: number;
|
|
56
|
+
fetchImpl?: typeof fetch;
|
|
57
|
+
sleep?: (ms: number) => Promise<void>;
|
|
58
|
+
now?: () => number;
|
|
59
|
+
log?: (msg: string) => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every pin whose exact version the registry does not serve (after waiting for
|
|
63
|
+
* one that may be late), with the reason. Empty = safe to publish.
|
|
64
|
+
*
|
|
65
|
+
* Three cheap exits come first, in order: the ledger already knows this version
|
|
66
|
+
* is unserved or held; one packument probe finds it served (nine in ten); the
|
|
67
|
+
* status endpoint says it can never be served. Only a pin that is none of those —
|
|
68
|
+
* possibly just late — pays the wait.
|
|
69
|
+
*/
|
|
70
|
+
export declare function findUnavailablePins(input: FindUnavailablePinsInput): Promise<HoldReason[]>;
|
|
71
|
+
/** Only a `dependencies` edge holds a publish, as in the name-level guard; the rest are reported. */
|
|
72
|
+
export declare const holdsPublish: (reason: HoldReason) => boolean;
|
|
73
|
+
export declare function summariseReasons(reasons: readonly HoldReason[]): string;
|
|
74
|
+
/** Classify a version THIS process PUT and could not read back, and write it to the ledger. */
|
|
75
|
+
export declare function recordUnserved(input: {
|
|
76
|
+
ledgerPath: string | undefined;
|
|
77
|
+
registry: string;
|
|
78
|
+
name: string;
|
|
79
|
+
version: string;
|
|
80
|
+
verdict: Parameters<typeof classifyUnserved>[0];
|
|
81
|
+
statusToken?: string;
|
|
82
|
+
fetchImpl?: typeof fetch;
|
|
83
|
+
}): Promise<Classification>;
|
|
84
|
+
/**
|
|
85
|
+
* The job-summary markdown for a ledger: what is unserved and why, what was held
|
|
86
|
+
* and why, and the exact command that publishes the held set once their
|
|
87
|
+
* dependencies are installable. `undefined` when nothing needs saying.
|
|
88
|
+
*/
|
|
89
|
+
export declare function renderLedgerSummary(entries: readonly LedgerEntry[], ref: string): string | undefined;
|