@gjsify/cli 0.7.3 → 0.7.5
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 +96 -96
- package/lib/commands/affected.js +28 -4
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +12 -0
- package/lib/commands/publish.js +16 -1
- package/lib/commands/run.js +8 -0
- package/lib/utils/install-backend-native.d.ts +1 -1
- package/lib/utils/install-backend-native.js +71 -7
- package/lib/utils/install-backend.d.ts +11 -0
- package/lib/utils/npm-oidc.d.ts +13 -0
- package/lib/utils/npm-oidc.js +55 -25
- package/lib/utils/prompt.js +4 -0
- package/package.json +17 -17
package/lib/commands/affected.js
CHANGED
|
@@ -96,6 +96,13 @@ const GLOBAL_TRIGGERS = [
|
|
|
96
96
|
/^packages\/infra\/cli\//,
|
|
97
97
|
/^packages\/infra\/rolldown-plugin-gjsify\//,
|
|
98
98
|
/^packages\/infra\/resolve-npm\//,
|
|
99
|
+
// The test framework every spec imports. It is a *devDependency* of
|
|
100
|
+
// virtually every workspace, so with the prod-deps-only closure below
|
|
101
|
+
// (`includeDev: false`) a change to it would otherwise yield a near-empty
|
|
102
|
+
// closure and silently skip every downstream test — yet a bug in a matcher
|
|
103
|
+
// can break assertions anywhere. Treat it as a global trigger so a
|
|
104
|
+
// `@gjsify/unit` change re-runs the full suite.
|
|
105
|
+
/^packages\/gjs\/unit\//,
|
|
99
106
|
// Cross-cutting dep + lockfile + root config.
|
|
100
107
|
/^gjsify-lock\.json$/,
|
|
101
108
|
/^package\.json$/,
|
|
@@ -210,12 +217,29 @@ function classifyAndExpand(workspaces, rootDir, changedFiles) {
|
|
|
210
217
|
skipAll: false,
|
|
211
218
|
};
|
|
212
219
|
}
|
|
213
|
-
// Default: closure walk
|
|
214
|
-
|
|
220
|
+
// Default: closure walk over PRODUCTION dependencies only (`includeDev:
|
|
221
|
+
// false`). The closure answers "whose tests must re-run because a package
|
|
222
|
+
// they depend on changed" — and that is a RUNTIME relationship. Walking
|
|
223
|
+
// devDependency edges instead conflated it with the build/test toolchain:
|
|
224
|
+
// every workspace devDepends `@gjsify/cli`/`@gjsify/tsc`/`@gjsify/unit`,
|
|
225
|
+
// and the umbrella `@gjsify/node-polyfills` prod-depends every polyfill, so
|
|
226
|
+
// ANY single-package change fanned out to ~210 of 221 workspaces — the
|
|
227
|
+
// closure was the whole monorepo and selective CI bought almost nothing.
|
|
228
|
+
// Prod-only collapses a typical seed to a handful (e.g. sqlite 210→4,
|
|
229
|
+
// fetch 210→34) while keeping every real runtime dependent. Safe against
|
|
230
|
+
// under-selection: no `*.spec.ts` imports a *cross-package* sibling via a
|
|
231
|
+
// devDependency (the only such spec imports are self-imports, which are
|
|
232
|
+
// covered by the package being its own seed); the universal test framework
|
|
233
|
+
// `@gjsify/unit` is handled by GLOBAL_TRIGGERS above; and every push-to-
|
|
234
|
+
// main + the nightly cron still run the FULL suite as the backstop.
|
|
235
|
+
const reverse = buildReverseDependencyGraph(workspaces, { includeDev: false });
|
|
215
236
|
const closure = affectedClosure(reverse, [...matched]);
|
|
216
237
|
// Integration suites whose forward deps overlap with the closure also
|
|
217
|
-
// need to run. We walk the forward graph
|
|
218
|
-
//
|
|
238
|
+
// need to run. We walk the forward graph with `includeDev: true` here
|
|
239
|
+
// (deliberately broader than the reverse closure) so an integration suite
|
|
240
|
+
// is never missed: integration packages declare the pillars they exercise
|
|
241
|
+
// as `dependencies`, but the wider walk also tolerates a devDep-declared
|
|
242
|
+
// edge. Over-running an integration suite is cheap; missing one is not.
|
|
219
243
|
const forward = buildDependencyGraph(workspaces, { includeDev: true });
|
|
220
244
|
let runIntegration = false;
|
|
221
245
|
for (const [from, deps] of forward.edges) {
|
package/lib/commands/install.js
CHANGED
|
@@ -57,6 +57,11 @@ export const installCommand = {
|
|
|
57
57
|
description: 'CI mode: install strictly from gjsify-lock.json, fail if the lockfile is missing or stale. Equivalent to yarn --immutable / npm ci --frozen-lockfile.',
|
|
58
58
|
type: 'boolean',
|
|
59
59
|
default: false,
|
|
60
|
+
})
|
|
61
|
+
.option('refresh-lockfile', {
|
|
62
|
+
description: 'Re-resolve every dependency to the newest version satisfying its range and rewrite the lockfile (bumps in-range transitive deps). Without it, a resolve preserves versions already pinned in the lockfile and only resolves new/changed deps — the npm/yarn/pnpm default. Mirrors yarn install --mode=update-lockfile.',
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
default: false,
|
|
60
65
|
})
|
|
61
66
|
.option('verbose', {
|
|
62
67
|
description: 'Verbose install logging.',
|
|
@@ -97,6 +102,11 @@ export const installCommand = {
|
|
|
97
102
|
console.error('gjsify install --immutable is incompatible with --global.');
|
|
98
103
|
process.exit(1);
|
|
99
104
|
}
|
|
105
|
+
if (args['refresh-lockfile']) {
|
|
106
|
+
console.error('gjsify install --immutable is incompatible with --refresh-lockfile ' +
|
|
107
|
+
'(--immutable forbids rewriting the lockfile). Drop one.');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
100
110
|
}
|
|
101
111
|
if (args.global) {
|
|
102
112
|
if (!args.packages || args.packages.length === 0) {
|
|
@@ -266,6 +276,7 @@ async function projectInstallNative(args, signal) {
|
|
|
266
276
|
// it (the whole point is byte-stability under CI).
|
|
267
277
|
lockfile: !args.immutable,
|
|
268
278
|
frozen: args.immutable,
|
|
279
|
+
refreshLockfile: args['refresh-lockfile'],
|
|
269
280
|
signal,
|
|
270
281
|
progress,
|
|
271
282
|
});
|
|
@@ -604,6 +615,7 @@ async function workspaceInstall(cwd, args, signal) {
|
|
|
604
615
|
verbose: args.verbose,
|
|
605
616
|
lockfile: !args.immutable,
|
|
606
617
|
frozen: args.immutable,
|
|
618
|
+
refreshLockfile: args['refresh-lockfile'],
|
|
607
619
|
overrides,
|
|
608
620
|
signal,
|
|
609
621
|
progress,
|
package/lib/commands/publish.js
CHANGED
|
@@ -298,7 +298,22 @@ export const publishCommand = {
|
|
|
298
298
|
process.exit(1);
|
|
299
299
|
}
|
|
300
300
|
// Auto-detect: fall back to the npmrc token auth that
|
|
301
|
-
// buildPublishHeaders already resolved into `headers
|
|
301
|
+
// buildPublishHeaders already resolved into `headers` — BUT only
|
|
302
|
+
// if one actually exists. In OIDC-only CI there is no npmrc
|
|
303
|
+
// token, so a "fallback" would PUT with no credentials, get a
|
|
304
|
+
// 404, and risk being mistaken for a tolerable outcome — exactly
|
|
305
|
+
// the v0.7.3 incident where a transient OIDC 503 on
|
|
306
|
+
// `@gjsify/process` left it unpublished while the release stayed
|
|
307
|
+
// green. Refuse loudly so a transient OIDC failure can never
|
|
308
|
+
// silently drop a package from a release.
|
|
309
|
+
if (!headers['authorization']) {
|
|
310
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
311
|
+
console.error(`gjsify publish: OIDC token exchange failed for ${packed.name} (${msg}) and no fallback npm token is configured — refusing to publish without credentials.`);
|
|
312
|
+
if (args.json) {
|
|
313
|
+
process.stdout.write(`${JSON.stringify({ ok: false, name: packed.name, version: packed.version, error: 'oidc-failed-no-token' }, null, 2)}\n`);
|
|
314
|
+
}
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
302
317
|
if (verbose) {
|
|
303
318
|
const msg = err instanceof Error ? err.message : String(err);
|
|
304
319
|
console.error(`gjsify publish: OIDC auto-detect failed (${msg}) — falling back to token auth`);
|
package/lib/commands/run.js
CHANGED
|
@@ -41,6 +41,14 @@ export const runCommand = {
|
|
|
41
41
|
// Without this, anything after `--` lands in args._ and is
|
|
42
42
|
// silently dropped by the handler.
|
|
43
43
|
'populate--': true,
|
|
44
|
+
// Forward UNKNOWN flags to the target without requiring `--`:
|
|
45
|
+
// gjsify run dist/app.gjs.mjs serve --port 8080 --year 2025
|
|
46
|
+
// Without this, the top-level `.strict()` (cli-app.ts) rejects
|
|
47
|
+
// --port/--year as "unknown arguments" before they reach the
|
|
48
|
+
// target. Treating unknown options as positional args routes
|
|
49
|
+
// them into the `[args..]` positional (then to the child).
|
|
50
|
+
// Known options (--help / --version) are still handled by gjsify.
|
|
51
|
+
'unknown-options-as-args': true,
|
|
44
52
|
}),
|
|
45
53
|
handler: async (args) => {
|
|
46
54
|
const target = args.target;
|
|
@@ -10,5 +10,5 @@ export interface InstalledTopLevel {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function installPackagesNative(opts: InstallOptions): Promise<InstalledTopLevel[]>;
|
|
12
12
|
export declare function parseSpec(raw: string): ParsedSpec;
|
|
13
|
-
export declare function pickVersion(packument: Packument, range: string): string | null;
|
|
13
|
+
export declare function pickVersion(packument: Packument, range: string, preferred?: string): string | null;
|
|
14
14
|
export {};
|
|
@@ -58,13 +58,21 @@ export async function installPackagesNative(opts) {
|
|
|
58
58
|
log('install: --immutable, using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
|
|
59
59
|
nodes = lockfileToNodes(existingLock);
|
|
60
60
|
}
|
|
61
|
-
else if (existingLock && lockfileMatchesRequest(existingLock, opts.specs)) {
|
|
61
|
+
else if (!opts.refreshLockfile && existingLock && lockfileMatchesRequest(existingLock, opts.specs)) {
|
|
62
62
|
log('install: using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
|
|
63
63
|
nodes = lockfileToNodes(existingLock);
|
|
64
64
|
}
|
|
65
65
|
else {
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// A resolve has to run (new/changed/removed dep, or no lockfile yet).
|
|
67
|
+
// Unless --refresh-lockfile was passed, seed it with the versions
|
|
68
|
+
// already pinned in the existing lockfile so unchanged deps keep their
|
|
69
|
+
// resolved version and only the genuinely new/changed deps move — the
|
|
70
|
+
// npm/yarn/pnpm `install` default. Without this, every `^`-range would
|
|
71
|
+
// re-resolve to the newest registry version, churning the whole tree
|
|
72
|
+
// (and silently bumping transitive deps) on a one-package add.
|
|
73
|
+
const preferred = !opts.refreshLockfile && existingLock ? buildPreferredVersions(existingLock) : undefined;
|
|
74
|
+
log('install: resolving %d top-level spec(s) → %s%s', opts.specs.length, opts.prefix, preferred ? ` (preserving ${preferred.size} locked name(s))` : '');
|
|
75
|
+
nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps, opts.signal, progress, preferred);
|
|
68
76
|
if (opts.lockfile) {
|
|
69
77
|
writeLockfile(lockfilePath, opts.specs, nodes);
|
|
70
78
|
log('install: wrote %s (%d entries)', LOCKFILE_NAME, nodes.length);
|
|
@@ -127,7 +135,15 @@ function parseSpecName(spec) {
|
|
|
127
135
|
* the root. Each placement returns a `ResolvedNode` whose `installPath`
|
|
128
136
|
* captures where it lives in the tree.
|
|
129
137
|
*/
|
|
130
|
-
async function resolveDeps(specs, npmrc, log, overrides, skipDeps, signal, progress
|
|
138
|
+
async function resolveDeps(specs, npmrc, log, overrides, skipDeps, signal, progress,
|
|
139
|
+
/**
|
|
140
|
+
* Lockfile-preservation oracle: `name → versions already pinned in the
|
|
141
|
+
* existing lockfile`. When an edge's range is satisfiable by a pinned
|
|
142
|
+
* version, that version is reused instead of the newest registry match.
|
|
143
|
+
* Undefined ⇒ a fresh resolve that always picks the newest in-range version
|
|
144
|
+
* (first install, or an explicit `--refresh-lockfile`).
|
|
145
|
+
*/
|
|
146
|
+
preferredVersions) {
|
|
131
147
|
progress?.beginPhase('resolve', specs.length);
|
|
132
148
|
const applyOverride = (name, range) => {
|
|
133
149
|
if (!overrides)
|
|
@@ -201,11 +217,14 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps, signal, progr
|
|
|
201
217
|
// Compatible placement reachable; reuse, no new install.
|
|
202
218
|
continue;
|
|
203
219
|
}
|
|
204
|
-
// No compatible existing placement. Resolve a
|
|
220
|
+
// No compatible existing placement. Resolve a version — preferring a
|
|
221
|
+
// version already pinned in the lockfile when it satisfies the range
|
|
222
|
+
// (so an add doesn't gratuitously bump unchanged deps).
|
|
205
223
|
let version = null;
|
|
206
224
|
try {
|
|
207
225
|
const packument = await fetchPkg(edge.name);
|
|
208
|
-
|
|
226
|
+
const preferred = preferredVersionFor(preferredVersions?.get(edge.name), edge.range);
|
|
227
|
+
version = pickVersion(packument, edge.range, preferred);
|
|
209
228
|
if (!version) {
|
|
210
229
|
if (!edge.required)
|
|
211
230
|
continue;
|
|
@@ -452,6 +471,44 @@ function writeLockfile(lockfilePath, specs, nodes) {
|
|
|
452
471
|
};
|
|
453
472
|
fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2) + '\n');
|
|
454
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Build the lockfile-preservation oracle: `name → every version currently
|
|
476
|
+
* pinned in the lockfile` (a name can appear at more than one install path /
|
|
477
|
+
* version when the tree nested a conflicting copy). Consulted during a resolve
|
|
478
|
+
* so unchanged deps keep their pinned version instead of bumping to the newest
|
|
479
|
+
* in-range match.
|
|
480
|
+
*/
|
|
481
|
+
function buildPreferredVersions(lockfile) {
|
|
482
|
+
const byName = new Map();
|
|
483
|
+
for (const [installPath, entry] of Object.entries(lockfile.packages)) {
|
|
484
|
+
const name = nameFromInstallPath(installPath);
|
|
485
|
+
let set = byName.get(name);
|
|
486
|
+
if (!set) {
|
|
487
|
+
set = new Set();
|
|
488
|
+
byName.set(name, set);
|
|
489
|
+
}
|
|
490
|
+
set.add(entry.version);
|
|
491
|
+
}
|
|
492
|
+
return byName;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Pick the pinned version to reuse for an edge: the highest lockfile version of
|
|
496
|
+
* the name that still satisfies the edge's range, or undefined when none do
|
|
497
|
+
* (range was tightened/changed, or the name is brand new) — in which case the
|
|
498
|
+
* resolver falls back to the newest registry match.
|
|
499
|
+
*/
|
|
500
|
+
function preferredVersionFor(locked, range) {
|
|
501
|
+
if (!locked || locked.size === 0)
|
|
502
|
+
return undefined;
|
|
503
|
+
let best;
|
|
504
|
+
for (const v of locked) {
|
|
505
|
+
if (!satisfiesRange(v, range))
|
|
506
|
+
continue;
|
|
507
|
+
if (best === undefined || new SemVer(v).compare(new SemVer(best)) > 0)
|
|
508
|
+
best = v;
|
|
509
|
+
}
|
|
510
|
+
return best;
|
|
511
|
+
}
|
|
455
512
|
function lockfileToNodes(lockfile) {
|
|
456
513
|
return Object.entries(lockfile.packages).map(([installPath, entry]) => ({
|
|
457
514
|
// Recover the package name from the path: the last segment is
|
|
@@ -535,7 +592,14 @@ export function parseSpec(raw) {
|
|
|
535
592
|
return { name: raw.slice(0, at), range: raw.slice(at + 1) || 'latest' };
|
|
536
593
|
}
|
|
537
594
|
// Exported for unit-testing. Internal API.
|
|
538
|
-
export function pickVersion(packument, range) {
|
|
595
|
+
export function pickVersion(packument, range, preferred) {
|
|
596
|
+
// Lockfile preservation: if a version already pinned in the lockfile still
|
|
597
|
+
// satisfies the range and is still published, reuse it instead of bumping to
|
|
598
|
+
// the newest match. `preferred` is only ever a concrete semver (never a
|
|
599
|
+
// dist-tag), so this never hijacks a `latest`/`next` range.
|
|
600
|
+
if (preferred && packument.versions[preferred] && satisfiesRange(preferred, range)) {
|
|
601
|
+
return preferred;
|
|
602
|
+
}
|
|
539
603
|
// dist-tag fast path: `latest`, `next`, ...
|
|
540
604
|
if (packument['dist-tags'][range])
|
|
541
605
|
return packument['dist-tags'][range];
|
|
@@ -18,6 +18,17 @@ export interface InstallOptions {
|
|
|
18
18
|
lockfile?: boolean;
|
|
19
19
|
/** Use `<prefix>/gjsify-lock.json` as the source of truth — fail if missing. */
|
|
20
20
|
frozen?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Native backend only: force a fresh re-resolution that bumps every spec to
|
|
23
|
+
* the newest version satisfying its range, rewriting the lockfile — the
|
|
24
|
+
* pre-0.7.x behaviour. By default (this flag off) a resolve that has to run
|
|
25
|
+
* (a new/changed/removed dep) PRESERVES the versions already pinned in the
|
|
26
|
+
* existing lockfile and only resolves the genuinely new/changed deps, the
|
|
27
|
+
* way `npm install` / `yarn install` / `pnpm install` do. Set this to
|
|
28
|
+
* intentionally pick up in-range updates (≈ `yarn install
|
|
29
|
+
* --mode=update-lockfile` / `npm update` / `pnpm update`).
|
|
30
|
+
*/
|
|
31
|
+
refreshLockfile?: boolean;
|
|
21
32
|
/**
|
|
22
33
|
* Per-package version overrides — `<name> → <range>`. Applied to every
|
|
23
34
|
* edge during dependency resolution, irrespective of the requester.
|
package/lib/utils/npm-oidc.d.ts
CHANGED
|
@@ -5,6 +5,19 @@ interface OidcExchangeOptions {
|
|
|
5
5
|
registry: string;
|
|
6
6
|
/** Optional verbose logger — receives single-line strings. */
|
|
7
7
|
log?: (msg: string) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Max retries on a TRANSIENT exchange failure (5xx / 429 / network error).
|
|
10
|
+
* Default 3. A transient npm-side blip (e.g. a 503 Service Unavailable on a
|
|
11
|
+
* single package mid-release) used to fall straight through to token auth,
|
|
12
|
+
* which in OIDC-only CI has no token → a 404 that broke the release while
|
|
13
|
+
* the workflow stayed green (the `@gjsify/process@0.7.3` v0.7.3 incident).
|
|
14
|
+
* Retrying the exchange keeps the publish on the OIDC path. Non-transient
|
|
15
|
+
* statuses (404 "package not found", 401/403 misconfig) are NOT retried —
|
|
16
|
+
* they're surfaced immediately so `--tolerate-untrusted-new` etc. still work.
|
|
17
|
+
*/
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
/** Base backoff in ms for transient retries; doubles each attempt (capped 8s). Default 1000. Tests pass 0. */
|
|
20
|
+
retryBaseMs?: number;
|
|
8
21
|
}
|
|
9
22
|
export interface OidcExchangeResult {
|
|
10
23
|
/** Short-lived npm token (`Authorization: Bearer <token>`-compatible). */
|
package/lib/utils/npm-oidc.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Reference: refs/npm-cli/lib/utils/oidc.js
|
|
21
21
|
// Original: Copyright (c) npm contributors. Artistic-2.0.
|
|
22
|
+
/** HTTP statuses worth retrying — npm-side transient/overload conditions. */
|
|
23
|
+
const TRANSIENT_EXCHANGE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
24
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
25
|
export class OidcUnavailableError extends Error {
|
|
23
26
|
reason;
|
|
24
27
|
constructor(message, reason) {
|
|
@@ -139,32 +142,59 @@ export async function exchangeOidcForNpmToken(args) {
|
|
|
139
142
|
: encodeURIComponent(packageName);
|
|
140
143
|
const exchangeUrl = `${registryClean}/-/npm/v1/oidc/token/exchange/package/${escapedName}`;
|
|
141
144
|
log?.(`gjsify oidc: POST ${exchangeUrl}`);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
145
|
+
const maxRetries = args.maxRetries ?? 3;
|
|
146
|
+
const retryBaseMs = args.retryBaseMs ?? 1000;
|
|
147
|
+
for (let attempt = 0;; attempt++) {
|
|
148
|
+
let res;
|
|
149
|
+
try {
|
|
150
|
+
res = await fetch(exchangeUrl, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
Authorization: `Bearer ${idToken}`,
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
Accept: 'application/json',
|
|
156
|
+
},
|
|
157
|
+
// npm's exchange endpoint accepts an empty JSON body — the JWT
|
|
158
|
+
// is the proof, no additional claims needed from us.
|
|
159
|
+
body: '{}',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (netErr) {
|
|
163
|
+
// Network-level failure (DNS, connection reset, TLS): transient.
|
|
164
|
+
if (attempt < maxRetries) {
|
|
165
|
+
const delay = Math.min(retryBaseMs * 2 ** attempt, 8000);
|
|
166
|
+
log?.(`gjsify oidc: network error for ${packageName} (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms`);
|
|
167
|
+
await sleep(delay);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
throw new OidcExchangeError(`npm OIDC token exchange network error for ${packageName} after ${attempt + 1} attempt(s): ${netErr instanceof Error ? netErr.message : String(netErr)}`, 0, '', packageName, decodeJwtPayload(idToken) ?? undefined);
|
|
171
|
+
}
|
|
172
|
+
const text = await res.text().catch(() => '');
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
// Retry only TRANSIENT npm-side failures; surface everything else
|
|
175
|
+
// (404/401/403 etc.) immediately so the caller's diagnostics —
|
|
176
|
+
// `--tolerate-untrusted-new`, Trusted-Publisher misconfig hints —
|
|
177
|
+
// still fire on the first response.
|
|
178
|
+
if (TRANSIENT_EXCHANGE_STATUSES.has(res.status) && attempt < maxRetries) {
|
|
179
|
+
const delay = Math.min(retryBaseMs * 2 ** attempt, 8000);
|
|
180
|
+
log?.(`gjsify oidc: transient ${res.status} ${res.statusText} for ${packageName} (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms`);
|
|
181
|
+
await sleep(delay);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
throw new OidcExchangeError(`npm OIDC token exchange failed for ${packageName}: ${res.status} ${res.statusText} — ${text.slice(0, 300)}`, res.status, text, packageName, decodeJwtPayload(idToken) ?? undefined);
|
|
185
|
+
}
|
|
186
|
+
let json;
|
|
187
|
+
try {
|
|
188
|
+
json = JSON.parse(text);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new OidcExchangeError(`npm OIDC token exchange returned non-JSON body for ${packageName}: ${text.slice(0, 200)}`, res.status, text, packageName);
|
|
192
|
+
}
|
|
193
|
+
if (!json.token) {
|
|
194
|
+
throw new OidcExchangeError(`npm OIDC token exchange returned no \`token\` field for ${packageName}`, res.status, text, packageName);
|
|
195
|
+
}
|
|
196
|
+
return json.token;
|
|
166
197
|
}
|
|
167
|
-
return json.token;
|
|
168
198
|
}
|
|
169
199
|
/**
|
|
170
200
|
* End-to-end: probe env → fetch id-token → exchange for npm token.
|
package/lib/utils/prompt.js
CHANGED
|
@@ -29,6 +29,10 @@ export async function promptHidden(question) {
|
|
|
29
29
|
let buf = '';
|
|
30
30
|
stdin.setRawMode(true);
|
|
31
31
|
stdin.resume();
|
|
32
|
+
// setEncoding makes 'data' emit decoded STRINGS — including under GJS,
|
|
33
|
+
// where @gjsify/process honours it via StringDecoder (was a no-op stub;
|
|
34
|
+
// bytes broke `ch === '\r'` so Enter never submitted). So we can iterate
|
|
35
|
+
// characters directly here; do NOT reintroduce a byte-normalising shim.
|
|
32
36
|
stdin.setEncoding('utf-8');
|
|
33
37
|
const onData = (chunk) => {
|
|
34
38
|
for (const ch of chunk) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -120,19 +120,19 @@
|
|
|
120
120
|
"cli"
|
|
121
121
|
],
|
|
122
122
|
"dependencies": {
|
|
123
|
-
"@gjsify/buffer": "^0.7.
|
|
124
|
-
"@gjsify/create-app": "^0.7.
|
|
125
|
-
"@gjsify/node-globals": "^0.7.
|
|
126
|
-
"@gjsify/node-polyfills": "^0.7.
|
|
127
|
-
"@gjsify/npm-registry": "^0.7.
|
|
128
|
-
"@gjsify/resolve-npm": "^0.7.
|
|
129
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.7.
|
|
130
|
-
"@gjsify/rolldown-plugin-pnp": "^0.7.
|
|
131
|
-
"@gjsify/semver": "^0.7.
|
|
132
|
-
"@gjsify/tar": "^0.7.
|
|
133
|
-
"@gjsify/tsc": "^0.7.
|
|
134
|
-
"@gjsify/web-polyfills": "^0.7.
|
|
135
|
-
"@gjsify/workspace": "^0.7.
|
|
123
|
+
"@gjsify/buffer": "^0.7.5",
|
|
124
|
+
"@gjsify/create-app": "^0.7.5",
|
|
125
|
+
"@gjsify/node-globals": "^0.7.5",
|
|
126
|
+
"@gjsify/node-polyfills": "^0.7.5",
|
|
127
|
+
"@gjsify/npm-registry": "^0.7.5",
|
|
128
|
+
"@gjsify/resolve-npm": "^0.7.5",
|
|
129
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.7.5",
|
|
130
|
+
"@gjsify/rolldown-plugin-pnp": "^0.7.5",
|
|
131
|
+
"@gjsify/semver": "^0.7.5",
|
|
132
|
+
"@gjsify/tar": "^0.7.5",
|
|
133
|
+
"@gjsify/tsc": "^0.7.5",
|
|
134
|
+
"@gjsify/web-polyfills": "^0.7.5",
|
|
135
|
+
"@gjsify/workspace": "^0.7.5",
|
|
136
136
|
"cosmiconfig": "^9.0.2",
|
|
137
137
|
"get-tsconfig": "^4.14.0",
|
|
138
138
|
"pkg-types": "^2.3.1",
|
|
@@ -140,13 +140,13 @@
|
|
|
140
140
|
"yargs": "^18.0.0"
|
|
141
141
|
},
|
|
142
142
|
"devDependencies": {
|
|
143
|
-
"@gjsify/unit": "^0.7.
|
|
143
|
+
"@gjsify/unit": "^0.7.5",
|
|
144
144
|
"@types/yargs": "^17.0.35",
|
|
145
145
|
"typescript": "^6.0.3"
|
|
146
146
|
},
|
|
147
147
|
"peerDependencies": {
|
|
148
|
-
"@gjsify/oxfmt-native": "^0.7.
|
|
149
|
-
"@gjsify/rolldown-native": "^0.7.
|
|
148
|
+
"@gjsify/oxfmt-native": "^0.7.5",
|
|
149
|
+
"@gjsify/rolldown-native": "^0.7.5"
|
|
150
150
|
},
|
|
151
151
|
"peerDependenciesMeta": {
|
|
152
152
|
"@gjsify/oxfmt-native": {
|