@gjsify/cli 0.36.0 → 0.38.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 +169 -169
- package/lib/actions/build.d.ts +7 -0
- package/lib/actions/build.js +18 -3
- package/lib/bundler-pick.js +61 -87
- package/lib/cli-fail.d.ts +4 -7
- package/lib/cli-fail.js +29 -55
- package/lib/commands/affected.js +161 -286
- package/lib/commands/install.d.ts +63 -66
- package/lib/commands/install.js +372 -536
- package/lib/config.d.ts +14 -0
- package/lib/config.js +18 -1
- package/lib/templates/install.mjs.tmpl +164 -75
- package/lib/types/cli-build-options.d.ts +33 -67
- package/lib/types/command.d.ts +9 -12
- package/lib/types/config-data.d.ts +137 -221
- package/lib/utils/auth-npmrc.d.ts +5 -10
- package/lib/utils/auth-npmrc.js +11 -21
- package/lib/utils/bin-shim.d.ts +40 -25
- package/lib/utils/bin-shim.js +51 -26
- package/lib/utils/clear-targets.d.ts +8 -11
- package/lib/utils/clear-targets.js +22 -29
- package/lib/utils/copy-targets.d.ts +13 -19
- package/lib/utils/copy-targets.js +31 -44
- package/lib/utils/dep-aggregation.d.ts +7 -15
- package/lib/utils/dep-aggregation.js +9 -22
- package/lib/utils/detect-native-packages.d.ts +89 -122
- package/lib/utils/detect-native-packages.js +119 -169
- package/lib/utils/dir-link.d.ts +29 -48
- package/lib/utils/dir-link.js +33 -56
- package/lib/utils/discover-showcases.d.ts +6 -7
- package/lib/utils/discover-showcases.js +11 -15
- package/lib/utils/dlx-cache.d.ts +29 -38
- package/lib/utils/dlx-cache.js +53 -63
- package/lib/utils/force-exit.d.ts +0 -3
- package/lib/utils/force-exit.js +13 -24
- package/lib/utils/gi-search-path.d.ts +16 -24
- package/lib/utils/gi-search-path.js +54 -82
- package/lib/utils/gjs-bundle-guard.d.ts +5 -13
- package/lib/utils/gjs-bundle-guard.js +35 -57
- package/lib/utils/gjs-source-escape.d.ts +2 -2
- package/lib/utils/gjs-source-escape.js +21 -39
- package/lib/utils/gjsify-shim.d.ts +17 -20
- package/lib/utils/gjsify-shim.js +80 -120
- package/lib/utils/html-entry.d.ts +8 -12
- package/lib/utils/html-entry.js +19 -28
- package/lib/utils/install-backend-native.d.ts +123 -161
- package/lib/utils/install-backend-native.js +536 -737
- package/lib/utils/install-backend.d.ts +42 -57
- package/lib/utils/install-backend.js +16 -21
- package/lib/utils/install-cache-fs.d.ts +10 -15
- package/lib/utils/install-cache-fs.js +18 -26
- package/lib/utils/install-lock.d.ts +5 -6
- package/lib/utils/install-lock.js +46 -65
- package/lib/utils/install-packument-cache.d.ts +27 -33
- package/lib/utils/install-packument-cache.js +44 -57
- package/lib/utils/install-progress.d.ts +2 -9
- package/lib/utils/install-progress.js +14 -39
- package/lib/utils/install-tarball-cache.d.ts +14 -20
- package/lib/utils/install-tarball-cache.js +41 -59
- package/lib/utils/library-output.d.ts +9 -22
- package/lib/utils/library-output.js +19 -37
- package/lib/utils/load-npmrc.d.ts +5 -6
- package/lib/utils/load-npmrc.js +18 -27
- package/lib/utils/node-bundle-guard.d.ts +4 -8
- package/lib/utils/node-bundle-guard.js +21 -35
- package/lib/utils/node-script.js +40 -53
- package/lib/utils/node-version.d.ts +5 -14
- package/lib/utils/node-version.js +15 -24
- package/lib/utils/normalize-bundler-options.d.ts +7 -11
- package/lib/utils/normalize-bundler-options.js +24 -42
- package/lib/utils/npm-oidc.d.ts +23 -37
- package/lib/utils/npm-oidc.js +28 -53
- package/lib/utils/npm-otp-cache.d.ts +4 -4
- package/lib/utils/npm-otp-cache.js +20 -25
- package/lib/utils/npm-otp.d.ts +20 -30
- package/lib/utils/npm-otp.js +32 -51
- package/lib/utils/onboard-probe.d.ts +7 -13
- package/lib/utils/onboard-probe.js +16 -27
- package/lib/utils/parse-spec.d.ts +3 -3
- package/lib/utils/parse-spec.js +4 -5
- package/lib/utils/pkg-json-edit.d.ts +7 -12
- package/lib/utils/pkg-json-edit.js +13 -26
- package/lib/utils/pkg-json.d.ts +11 -18
- package/lib/utils/pkg-json.js +19 -28
- package/lib/utils/prefixed-output.d.ts +3 -4
- package/lib/utils/prefixed-output.js +13 -19
- package/lib/utils/prompt.d.ts +3 -4
- package/lib/utils/prompt.js +14 -18
- package/lib/utils/publish-diagnose.d.ts +11 -16
- package/lib/utils/publish-diagnose.js +15 -28
- package/lib/utils/publish-headers.d.ts +7 -11
- package/lib/utils/publish-headers.js +36 -65
- package/lib/utils/publishable-packages.d.ts +4 -6
- package/lib/utils/publishable-packages.js +14 -18
- package/lib/utils/resolve-gjs-entry.d.ts +5 -8
- package/lib/utils/resolve-gjs-entry.js +10 -27
- package/lib/utils/resolve-npm-package.d.ts +6 -11
- package/lib/utils/resolve-npm-package.js +29 -63
- package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
- package/lib/utils/resolve-plugin-by-name.js +9 -14
- package/lib/utils/run-gjs.d.ts +16 -21
- package/lib/utils/run-gjs.js +31 -56
- package/lib/utils/run-lifecycle-script.d.ts +5 -9
- package/lib/utils/run-lifecycle-script.js +23 -42
- package/lib/utils/run-node.d.ts +21 -33
- package/lib/utils/run-node.js +43 -64
- package/lib/utils/runtimes.d.ts +25 -35
- package/lib/utils/runtimes.js +34 -46
- package/lib/utils/spawn.d.ts +32 -48
- package/lib/utils/spawn.js +51 -84
- package/lib/utils/stdin.d.ts +2 -2
- package/lib/utils/stdin.js +10 -25
- package/lib/utils/suggest.d.ts +6 -7
- package/lib/utils/suggest.js +8 -10
- package/lib/utils/system-gi.d.ts +36 -27
- package/lib/utils/system-gi.js +85 -114
- package/lib/utils/trust-registry.d.ts +4 -8
- package/lib/utils/trust-registry.js +7 -15
- package/lib/utils/win32-command.d.ts +8 -14
- package/lib/utils/win32-command.js +29 -43
- package/lib/utils/workspace-root.js +7 -12
- package/package.json +18 -18
package/lib/utils/npm-otp.js
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
// Shared npm 2FA / OTP handling for the auth-aware CLI commands
|
|
2
2
|
// (`gjsify publish`, `gjsify trust`, `gjsify onboard`).
|
|
3
3
|
//
|
|
4
|
-
// npm signals "OTP required" via HTTP 401 + a `www-authenticate` header
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// refs/npm-cli/node_modules/npm-registry-fetch/lib/check-response.js.
|
|
4
|
+
// npm signals "OTP required" via HTTP 401 + a `www-authenticate` header containing
|
|
5
|
+
// `otp` (or, for malformed responses, a body mentioning "one-time pass" / `EOTP`) —
|
|
6
|
+
// see refs/npm-cli/node_modules/npm-registry-fetch/lib/check-response.js.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// otherwise prompt once per package; with one shared provider the user types
|
|
16
|
-
// an OTP once — the cached code is tried first on every subsequent challenge,
|
|
17
|
-
// and a fresh prompt only happens when npm actually rejects/expires it.
|
|
18
|
-
//
|
|
19
|
-
// Standalone `publish`/`trust` keep their existing single-command UX: each
|
|
20
|
-
// still constructs its own provider and prompts on demand.
|
|
8
|
+
// Detection and prompt-then-retry live here so ONE `OtpProvider` can be injected
|
|
9
|
+
// across many operations in a single run: `gjsify onboard` sweeps every publishable
|
|
10
|
+
// package and would otherwise prompt per package, whereas a shared provider tries the cached
|
|
11
|
+
// code first on every subsequent challenge and only re-prompts when npm actually
|
|
12
|
+
// rejects or expires it. Standalone `publish`/`trust` each construct their own
|
|
13
|
+
// provider and prompt on demand.
|
|
21
14
|
import { promptLine } from './prompt.js';
|
|
22
15
|
import { clearCachedOtp, readCachedOtp, writeCachedOtp } from './npm-otp-cache.js';
|
|
23
16
|
const DEFAULT_QUESTION = 'This operation requires a one-time password.\nEnter OTP: ';
|
|
24
17
|
/**
|
|
25
18
|
* True when a response is an npm OTP / 2FA challenge (401 + a `www-authenticate`
|
|
26
19
|
* header naming `otp`, or a body mentioning a one-time password / `EOTP`).
|
|
27
|
-
* Unifies the detection logic previously inlined in both publish.ts and trust.ts.
|
|
28
20
|
*/
|
|
29
21
|
export async function isOtpChallenge(res) {
|
|
30
22
|
if (res.status !== 401)
|
|
@@ -42,12 +34,11 @@ export async function isOtpChallenge(res) {
|
|
|
42
34
|
* Holds ONE OTP code across a run and knows how to prompt for a fresh one.
|
|
43
35
|
*
|
|
44
36
|
* `current()` returns the cached code (seeded from `--otp`, the cross-invocation
|
|
45
|
-
* file cache, or the last one
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `gjsify` invocations (`login → onboard → publish → trust`).
|
|
37
|
+
* file cache, or the last one typed); `refresh()` prompts for a new one and caches
|
|
38
|
+
* it in-process AND — when a `registry` is set — in the short-lived file cache. One
|
|
39
|
+
* shared instance is what makes `gjsify onboard` need a single OTP entry for a whole
|
|
40
|
+
* sweep; the file cache extends that reuse across SEPARATE invocations
|
|
41
|
+
* (`login → onboard → publish → trust`).
|
|
51
42
|
*/
|
|
52
43
|
export class OtpProvider {
|
|
53
44
|
cached;
|
|
@@ -81,9 +72,8 @@ export class OtpProvider {
|
|
|
81
72
|
return this.cached;
|
|
82
73
|
}
|
|
83
74
|
/**
|
|
84
|
-
* Prompt for a fresh code, cache it (in-process + file cache), and return
|
|
85
|
-
*
|
|
86
|
-
* treats empty as "give up").
|
|
75
|
+
* Prompt for a fresh code, cache it (in-process + file cache), and return the
|
|
76
|
+
* trimmed input — `''` when nothing was entered, which callers read as "give up".
|
|
87
77
|
*/
|
|
88
78
|
async refresh(question = DEFAULT_QUESTION) {
|
|
89
79
|
const code = (await this.promptFn(question)).trim();
|
|
@@ -95,9 +85,9 @@ export class OtpProvider {
|
|
|
95
85
|
return code;
|
|
96
86
|
}
|
|
97
87
|
/**
|
|
98
|
-
* Drop the cached code (in-process AND the cross-invocation file entry)
|
|
99
|
-
*
|
|
100
|
-
*
|
|
88
|
+
* Drop the cached code (in-process AND the cross-invocation file entry) after npm
|
|
89
|
+
* rejects it, so the NEXT command re-prompts instead of replaying a single-use
|
|
90
|
+
* code the registry already burned.
|
|
101
91
|
*/
|
|
102
92
|
invalidate() {
|
|
103
93
|
this.cached = undefined;
|
|
@@ -106,21 +96,14 @@ export class OtpProvider {
|
|
|
106
96
|
}
|
|
107
97
|
}
|
|
108
98
|
/**
|
|
109
|
-
* Run `doFetch` with cache-first OTP retry against a shared {@link OtpProvider}
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* 2. On a 2FA challenge, retry with the currently-cached code (unless step 1
|
|
115
|
-
* already sent it).
|
|
116
|
-
* 3. If still challenged (cache empty/rejected/expired), prompt for a fresh
|
|
117
|
-
* code — caching it on the provider so the NEXT operation tries it first —
|
|
118
|
-
* and retry, up to `maxPrompts` times.
|
|
119
|
-
* 4. Return the last response (a challenge the caller must handle) when the
|
|
120
|
-
* user declines to enter a code.
|
|
99
|
+
* Run `doFetch` with cache-first OTP retry against a shared {@link OtpProvider}:
|
|
100
|
+
* attempt without an OTP (or with the cached code under `seedFirstAttempt`), retry
|
|
101
|
+
* with the cached code on a challenge, then prompt for fresh codes up to
|
|
102
|
+
* `maxPrompts` times. A declined prompt returns the last challenge response for the
|
|
103
|
+
* caller to handle.
|
|
121
104
|
*
|
|
122
|
-
* The caller owns the
|
|
123
|
-
*
|
|
105
|
+
* The caller owns the fetch and all response interpretation; this owns only the OTP
|
|
106
|
+
* dance.
|
|
124
107
|
*/
|
|
125
108
|
export async function withOtpRetry(doFetch, provider, opts = {}) {
|
|
126
109
|
const maxPrompts = opts.maxPrompts ?? 2;
|
|
@@ -130,20 +113,18 @@ export async function withOtpRetry(doFetch, provider, opts = {}) {
|
|
|
130
113
|
let res = await doFetch(seededFirst ? cached : undefined);
|
|
131
114
|
if (!(await isOtpChallenge(res)))
|
|
132
115
|
return res;
|
|
133
|
-
// The seeded
|
|
134
|
-
//
|
|
116
|
+
// The seeded code was rejected/consumed — drop it so it is not replayed and the
|
|
117
|
+
// cross-invocation cache entry is cleared.
|
|
135
118
|
if (seededFirst)
|
|
136
119
|
provider.invalidate();
|
|
137
|
-
// Try the cached code if we haven't already sent it.
|
|
138
120
|
if (!seededFirst && provider.current() !== undefined) {
|
|
139
121
|
res = await doFetch(provider.current());
|
|
140
122
|
if (!(await isOtpChallenge(res)))
|
|
141
123
|
return res;
|
|
142
|
-
//
|
|
143
|
-
//
|
|
124
|
+
// Stale seed, or a sibling command already burned the file-cached single-use
|
|
125
|
+
// code.
|
|
144
126
|
provider.invalidate();
|
|
145
127
|
}
|
|
146
|
-
// Cache empty, rejected, or expired → prompt for a fresh code and retry.
|
|
147
128
|
for (let i = 0; i < maxPrompts; i++) {
|
|
148
129
|
const fresh = await provider.refresh(question);
|
|
149
130
|
if (!fresh)
|
|
@@ -151,8 +132,8 @@ export async function withOtpRetry(doFetch, provider, opts = {}) {
|
|
|
151
132
|
res = await doFetch(fresh);
|
|
152
133
|
if (!(await isOtpChallenge(res)))
|
|
153
134
|
return res;
|
|
154
|
-
//
|
|
155
|
-
//
|
|
135
|
+
// Clear a rejected fresh code before re-prompting, so a wedged sequence never
|
|
136
|
+
// leaves a burned code in the file cache.
|
|
156
137
|
provider.invalidate();
|
|
157
138
|
}
|
|
158
139
|
return res;
|
|
@@ -4,7 +4,6 @@ import type { TrustRequester } from '../commands/trust.js';
|
|
|
4
4
|
import { type TrustState } from './trust-registry.js';
|
|
5
5
|
/** What a package needs, computed from its published + trust state. */
|
|
6
6
|
export type PkgAction = 'skip' | 'trust' | 'publish-and-trust' | 'blocked';
|
|
7
|
-
/** A per-package plan produced by the probe phase. */
|
|
8
7
|
export interface PkgPlan {
|
|
9
8
|
ws: Workspace;
|
|
10
9
|
registry: string;
|
|
@@ -13,7 +12,6 @@ export interface PkgPlan {
|
|
|
13
12
|
httpStatus: number;
|
|
14
13
|
action: PkgAction;
|
|
15
14
|
}
|
|
16
|
-
/** Everything a probe needs beyond the requester + workspace. */
|
|
17
15
|
export interface ProbeContext {
|
|
18
16
|
registryOverride?: string;
|
|
19
17
|
npmrc: NpmrcConfig;
|
|
@@ -29,26 +27,22 @@ export interface ProbeOptions {
|
|
|
29
27
|
/** Injected delay (tests pass a no-op). Default: real `setTimeout`. */
|
|
30
28
|
sleep?: (ms: number) => Promise<void>;
|
|
31
29
|
}
|
|
32
|
-
/** Small backoff before the one-shot retry of a transient 401 read. */
|
|
33
30
|
export declare const PROBE_RETRY_DELAY_MS = 400;
|
|
34
31
|
/** Default probe concurrency — kept SMALL so a single token never bursts npm. */
|
|
35
32
|
export declare const DEFAULT_PROBE_CONCURRENCY = 4;
|
|
36
33
|
/** Map a classified trust state to the action the onboard sweep should take. */
|
|
37
34
|
export declare function actionForState(state: TrustState): PkgAction;
|
|
38
35
|
/**
|
|
39
|
-
* Read one package's published + trust state
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* it retries ONCE after a short backoff.
|
|
36
|
+
* Read one package's published + trust state through the SHARED trust requester.
|
|
37
|
+
* A surviving `auth-required` means the 401 was not an answerable OTP challenge —
|
|
38
|
+
* a parallel-burst rejection or a lapsed 2FA-skip window — and both recover on a
|
|
39
|
+
* single retry after a short backoff.
|
|
44
40
|
*/
|
|
45
41
|
export declare function probeTrustState(request: TrustRequester, ws: Workspace, ctx: ProbeContext, opts?: ProbeOptions): Promise<PkgPlan>;
|
|
46
42
|
/**
|
|
47
|
-
* Probe every selected workspace. The FIRST
|
|
48
|
-
* 2FA-gated read triggers the
|
|
49
|
-
*
|
|
50
|
-
* never faces a 127-wide parallel read. The remaining packages are read with a
|
|
51
|
-
* bounded (small) concurrency cap.
|
|
43
|
+
* Probe every selected workspace. The FIRST is read SERIALLY on purpose: a
|
|
44
|
+
* 2FA-gated read triggers the shared-OTP prompt once, before the burst, so the rest
|
|
45
|
+
* reuse the cached code instead of each prompting.
|
|
52
46
|
*/
|
|
53
47
|
export declare function probeAllTrustStates(request: TrustRequester, selected: readonly Workspace[], concurrency: number, ctx: ProbeContext, opts?: ProbeOptions): Promise<PkgPlan[]>;
|
|
54
48
|
/** Map with bounded concurrency, preserving input order in the result. */
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
// Trusted-Publisher STATE-READ path for `gjsify onboard`, extracted so it can
|
|
2
|
-
//
|
|
1
|
+
// Trusted-Publisher STATE-READ path for `gjsify onboard`, extracted so it can be
|
|
2
|
+
// unit-tested against a mocked requester (no live npm).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// answered with the shared code, and non-OTP transient 401s are tolerated.
|
|
12
|
-
//
|
|
13
|
-
// Fix: onboard now reads through the EXACT same `TrustRequester` path as
|
|
14
|
-
// `gjsify trust` (identical `trustUrl` + `buildHeaders` auth + OTP handling),
|
|
15
|
-
// PLUS a one-shot retry on a transient 401, PLUS a first-serial-then-bounded
|
|
16
|
-
// probe order so the ONE shared OTP is prompted at most once and npm is never
|
|
17
|
-
// hit with a 127-wide parallel burst from a single token.
|
|
4
|
+
// Onboard MUST read through the same `TrustRequester` as `gjsify trust`, never a
|
|
5
|
+
// plain authenticated `fetch`. It once used the latter, in a wide concurrent burst:
|
|
6
|
+
// every one of ~127 reads came back `401 → auth-required → unreadable` and the
|
|
7
|
+
// sweep planned `0 to publish+trust, 0 to trust, 127 unreadable`, while a targeted
|
|
8
|
+
// `gjsify trust <pkg> --otp <code>` read the same state fine on the SAME token —
|
|
9
|
+
// because the shared requester goes through `withOtpRetry`, which answers a 401
|
|
10
|
+
// that is an OTP challenge and tolerates transient non-OTP ones.
|
|
18
11
|
import { DEFAULT_REGISTRY, registryFor } from '@gjsify/npm-registry';
|
|
19
12
|
import { classifyTrustList, trustUrl } from './trust-registry.js';
|
|
20
|
-
/** Small backoff before the one-shot retry of a transient 401 read. */
|
|
21
13
|
export const PROBE_RETRY_DELAY_MS = 400;
|
|
22
14
|
/** Default probe concurrency — kept SMALL so a single token never bursts npm. */
|
|
23
15
|
export const DEFAULT_PROBE_CONCURRENCY = 4;
|
|
@@ -39,11 +31,10 @@ function defaultSleep(ms) {
|
|
|
39
31
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
32
|
}
|
|
41
33
|
/**
|
|
42
|
-
* Read one package's published + trust state
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* it retries ONCE after a short backoff.
|
|
34
|
+
* Read one package's published + trust state through the SHARED trust requester.
|
|
35
|
+
* A surviving `auth-required` means the 401 was not an answerable OTP challenge —
|
|
36
|
+
* a parallel-burst rejection or a lapsed 2FA-skip window — and both recover on a
|
|
37
|
+
* single retry after a short backoff.
|
|
47
38
|
*/
|
|
48
39
|
export async function probeTrustState(request, ws, ctx, opts = {}) {
|
|
49
40
|
const registry = ctx.registryOverride ?? registryFor(ws.name, ctx.npmrc) ?? DEFAULT_REGISTRY;
|
|
@@ -60,11 +51,9 @@ export async function probeTrustState(request, ws, ctx, opts = {}) {
|
|
|
60
51
|
return { ws, registry, url, state, httpStatus: res.status, action: actionForState(state) };
|
|
61
52
|
}
|
|
62
53
|
/**
|
|
63
|
-
* Probe every selected workspace. The FIRST
|
|
64
|
-
* 2FA-gated read triggers the
|
|
65
|
-
*
|
|
66
|
-
* never faces a 127-wide parallel read. The remaining packages are read with a
|
|
67
|
-
* bounded (small) concurrency cap.
|
|
54
|
+
* Probe every selected workspace. The FIRST is read SERIALLY on purpose: a
|
|
55
|
+
* 2FA-gated read triggers the shared-OTP prompt once, before the burst, so the rest
|
|
56
|
+
* reuse the cached code instead of each prompting.
|
|
68
57
|
*/
|
|
69
58
|
export async function probeAllTrustStates(request, selected, concurrency, ctx, opts = {}) {
|
|
70
59
|
if (selected.length === 0)
|
|
@@ -13,8 +13,8 @@ export type ParsedSpec = {
|
|
|
13
13
|
* Local: starts with `./`, `../`, `/`, or is an existing directory path.
|
|
14
14
|
* Registry: `<name>` | `<name>@<version>` | `@scope/<name>` | `@scope/<name>@<version>`
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* `spec` keeps the input verbatim so the registry case can be handed straight to
|
|
17
|
+
* `npm install`, which already understands dist-tags, ranges, git URIs and tarball
|
|
18
|
+
* URLs — none of those are re-parsed here.
|
|
19
19
|
*/
|
|
20
20
|
export declare function parseSpec(input: string): ParsedSpec;
|
package/lib/utils/parse-spec.js
CHANGED
|
@@ -8,9 +8,9 @@ const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
|
8
8
|
* Local: starts with `./`, `../`, `/`, or is an existing directory path.
|
|
9
9
|
* Registry: `<name>` | `<name>@<version>` | `@scope/<name>` | `@scope/<name>@<version>`
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* `spec` keeps the input verbatim so the registry case can be handed straight to
|
|
12
|
+
* `npm install`, which already understands dist-tags, ranges, git URIs and tarball
|
|
13
|
+
* URLs — none of those are re-parsed here.
|
|
14
14
|
*/
|
|
15
15
|
export function parseSpec(input) {
|
|
16
16
|
if (!input)
|
|
@@ -21,8 +21,7 @@ export function parseSpec(input) {
|
|
|
21
21
|
if (existsSync(input)) {
|
|
22
22
|
return { kind: 'local', path: resolve(input) };
|
|
23
23
|
}
|
|
24
|
-
//
|
|
25
|
-
// leading scope separator.
|
|
24
|
+
// Split the version off the LAST `@` — `lastAt > 0` skips a leading scope `@`.
|
|
26
25
|
let name = input;
|
|
27
26
|
let version;
|
|
28
27
|
const lastAt = input.lastIndexOf('@');
|
|
@@ -29,21 +29,16 @@ export declare function parseSpec(spec: string): {
|
|
|
29
29
|
range?: string;
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `
|
|
35
|
-
* with the project's existing dependency manifest — equivalent to
|
|
36
|
-
* `npm install` reading `package.json`.
|
|
32
|
+
* Existing dependencies + devDependencies + optionalDependencies as `name@range`
|
|
33
|
+
* specs — what seeds the resolver for a bare `gjsify install`, the way `npm install`
|
|
34
|
+
* reads `package.json`.
|
|
37
35
|
*/
|
|
38
36
|
export declare function projectSpecsFromPackageJson(pkg: PackageJson): string[];
|
|
39
37
|
/**
|
|
40
|
-
* Add or update a dependency entry in `pkg`.
|
|
41
|
-
* a
|
|
42
|
-
*
|
|
38
|
+
* Add or update a dependency entry in `pkg`. When the spec carried no range, the
|
|
39
|
+
* caller calls this a second time after resolution, with `range` derived from the
|
|
40
|
+
* installed version via {@link defaultRangeFromVersion}.
|
|
43
41
|
*/
|
|
44
42
|
export declare function addDependencyEntry(pkg: PackageJson, name: string, range: string, kind: DependencyKind): void;
|
|
45
|
-
/**
|
|
46
|
-
* Default version range when the user didn't pin one: `^x.y.z` from the
|
|
47
|
-
* installed version. Mirrors npm's `save-prefix` default (`^`).
|
|
48
|
-
*/
|
|
43
|
+
/** Range used when the user pinned none — npm's `save-prefix` default (`^`). */
|
|
49
44
|
export declare function defaultRangeFromVersion(version: string): string;
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
// Helpers for editing `package.json` during `gjsify install <pkg
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// - --save-dev → devDependencies
|
|
6
|
-
// - --save-peer → peerDependencies
|
|
7
|
-
// - --save-optional → optionalDependencies
|
|
8
|
-
//
|
|
9
|
-
// Version specifier resolution mirrors npm's default (`^x.y.z` from the
|
|
10
|
-
// installed version), unless the user passed an explicit range in the spec
|
|
11
|
-
// (`react@^18` → keep `^18`).
|
|
1
|
+
// Helpers for editing `package.json` during `gjsify install <pkg>`, mirroring npm:
|
|
2
|
+
// `--save-{dev,peer,optional}` pick the matching block and no flag means
|
|
3
|
+
// `dependencies`; the saved range is `^x.y.z` off the installed version unless the
|
|
4
|
+
// spec carried an explicit one (`react@^18` stays `^18`).
|
|
12
5
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
13
6
|
export function readPackageJson(pkgPath) {
|
|
14
7
|
if (!existsSync(pkgPath))
|
|
@@ -48,11 +41,9 @@ export function parseSpec(spec) {
|
|
|
48
41
|
return { name: spec.slice(0, at), range: spec.slice(at + 1) };
|
|
49
42
|
}
|
|
50
43
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `
|
|
54
|
-
* with the project's existing dependency manifest — equivalent to
|
|
55
|
-
* `npm install` reading `package.json`.
|
|
44
|
+
* Existing dependencies + devDependencies + optionalDependencies as `name@range`
|
|
45
|
+
* specs — what seeds the resolver for a bare `gjsify install`, the way `npm install`
|
|
46
|
+
* reads `package.json`.
|
|
56
47
|
*/
|
|
57
48
|
export function projectSpecsFromPackageJson(pkg) {
|
|
58
49
|
const out = [];
|
|
@@ -61,9 +52,8 @@ export function projectSpecsFromPackageJson(pkg) {
|
|
|
61
52
|
if (!block)
|
|
62
53
|
continue;
|
|
63
54
|
for (const [name, range] of Object.entries(block)) {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// the project-local install path.
|
|
55
|
+
// `workspace:`/`link:`/`file:`/`portal:` are workspace-local references
|
|
56
|
+
// resolved by the workspace install path, not this project-local one.
|
|
67
57
|
if (typeof range !== 'string')
|
|
68
58
|
continue;
|
|
69
59
|
if (/^(workspace|link|file|portal|git\+|https?):/.test(range))
|
|
@@ -74,9 +64,9 @@ export function projectSpecsFromPackageJson(pkg) {
|
|
|
74
64
|
return out;
|
|
75
65
|
}
|
|
76
66
|
/**
|
|
77
|
-
* Add or update a dependency entry in `pkg`.
|
|
78
|
-
* a
|
|
79
|
-
*
|
|
67
|
+
* Add or update a dependency entry in `pkg`. When the spec carried no range, the
|
|
68
|
+
* caller calls this a second time after resolution, with `range` derived from the
|
|
69
|
+
* installed version via {@link defaultRangeFromVersion}.
|
|
80
70
|
*/
|
|
81
71
|
export function addDependencyEntry(pkg, name, range, kind) {
|
|
82
72
|
if (pkg[kind] === undefined) {
|
|
@@ -84,10 +74,7 @@ export function addDependencyEntry(pkg, name, range, kind) {
|
|
|
84
74
|
}
|
|
85
75
|
pkg[kind][name] = range;
|
|
86
76
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Default version range when the user didn't pin one: `^x.y.z` from the
|
|
89
|
-
* installed version. Mirrors npm's `save-prefix` default (`^`).
|
|
90
|
-
*/
|
|
77
|
+
/** Range used when the user pinned none — npm's `save-prefix` default (`^`). */
|
|
91
78
|
export function defaultRangeFromVersion(version) {
|
|
92
79
|
return `^${version}`;
|
|
93
80
|
}
|
package/lib/utils/pkg-json.d.ts
CHANGED
|
@@ -1,30 +1,23 @@
|
|
|
1
1
|
import type { PackageJson } from './pkg-json-edit.js';
|
|
2
2
|
/**
|
|
3
|
-
* Drop a leading UTF-8 BOM (`U+FEFF
|
|
3
|
+
* Drop a leading UTF-8 BOM (`U+FEFF`).
|
|
4
4
|
*
|
|
5
|
-
* `JSON.parse` rejects a BOM — it is not whitespace —
|
|
6
|
-
* one
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* manifest produces one. Notepad offers "UTF-8 with BOM" too.
|
|
5
|
+
* `JSON.parse` rejects a BOM — it is not whitespace — yet Windows PowerShell 5.1
|
|
6
|
+
* writes one for `-Encoding utf8`, its DEFAULT for `Out-File`/`Set-Content`, and
|
|
7
|
+
* npm reads such a manifest without complaint. The file is valid to the
|
|
8
|
+
* ecosystem, so gjsify must not be the strict one.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* concerned and gjsify was the strict one. Stripping matches npm, VS Code and
|
|
14
|
-
* `parse-json`'s own guidance; nothing downstream wants the character.
|
|
15
|
-
*
|
|
16
|
-
* Only a LEADING BOM: `U+FEFF` elsewhere is a zero-width no-break space inside
|
|
17
|
-
* a string value, and removing it would corrupt the data.
|
|
10
|
+
* Only a LEADING BOM: `U+FEFF` elsewhere is a zero-width no-break space inside a
|
|
11
|
+
* string value, and stripping it would corrupt the data.
|
|
18
12
|
*/
|
|
19
13
|
export declare function stripBom(text: string): string;
|
|
20
14
|
/**
|
|
21
15
|
* Read + parse a `package.json` at `pkgPath`. Permissive: any error (missing
|
|
22
16
|
* file, unreadable, invalid JSON) yields `null`.
|
|
23
17
|
*
|
|
24
|
-
* The BOM strip matters
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `check` script, discovery skipped the package, and nothing said why.
|
|
18
|
+
* The BOM strip matters more here than in a throwing reader, because callers
|
|
19
|
+
* read `null` as "no package.json": on Windows a BOM produced no error at all,
|
|
20
|
+
* it made the package look manifest-less, so `gjsify check` found no `check`
|
|
21
|
+
* script, discovery skipped the package, and nothing said why.
|
|
29
22
|
*/
|
|
30
23
|
export declare function readPackageJson(pkgPath: string): PackageJson | null;
|
package/lib/utils/pkg-json.js
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// manifest rather than crash the whole command.
|
|
1
|
+
// Permissive `package.json` reader for the CLI's workspace- and
|
|
2
|
+
// dependency-discovery paths: `null` when the file is missing OR unparseable, so
|
|
3
|
+
// discovery degrades gracefully on a stray or corrupt manifest instead of
|
|
4
|
+
// crashing the whole command.
|
|
6
5
|
//
|
|
7
|
-
// The install-save path keeps
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// — do not fold it into this one.
|
|
6
|
+
// The install-save path deliberately keeps a STRICTER reader in
|
|
7
|
+
// `pkg-json-edit.ts` — `null` only when absent, THROWS on invalid JSON — so
|
|
8
|
+
// `gjsify install <pkg>` never silently overwrites a corrupt manifest. Do not
|
|
9
|
+
// fold the two together.
|
|
12
10
|
import { readFileSync } from 'node:fs';
|
|
13
11
|
/**
|
|
14
|
-
* Drop a leading UTF-8 BOM (`U+FEFF
|
|
12
|
+
* Drop a leading UTF-8 BOM (`U+FEFF`).
|
|
15
13
|
*
|
|
16
|
-
* `JSON.parse` rejects a BOM — it is not whitespace —
|
|
17
|
-
* one
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* manifest produces one. Notepad offers "UTF-8 with BOM" too.
|
|
14
|
+
* `JSON.parse` rejects a BOM — it is not whitespace — yet Windows PowerShell 5.1
|
|
15
|
+
* writes one for `-Encoding utf8`, its DEFAULT for `Out-File`/`Set-Content`, and
|
|
16
|
+
* npm reads such a manifest without complaint. The file is valid to the
|
|
17
|
+
* ecosystem, so gjsify must not be the strict one.
|
|
21
18
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* concerned and gjsify was the strict one. Stripping matches npm, VS Code and
|
|
25
|
-
* `parse-json`'s own guidance; nothing downstream wants the character.
|
|
26
|
-
*
|
|
27
|
-
* Only a LEADING BOM: `U+FEFF` elsewhere is a zero-width no-break space inside
|
|
28
|
-
* a string value, and removing it would corrupt the data.
|
|
19
|
+
* Only a LEADING BOM: `U+FEFF` elsewhere is a zero-width no-break space inside a
|
|
20
|
+
* string value, and stripping it would corrupt the data.
|
|
29
21
|
*/
|
|
30
22
|
export function stripBom(text) {
|
|
31
23
|
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
@@ -34,11 +26,10 @@ export function stripBom(text) {
|
|
|
34
26
|
* Read + parse a `package.json` at `pkgPath`. Permissive: any error (missing
|
|
35
27
|
* file, unreadable, invalid JSON) yields `null`.
|
|
36
28
|
*
|
|
37
|
-
* The BOM strip matters
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `check` script, discovery skipped the package, and nothing said why.
|
|
29
|
+
* The BOM strip matters more here than in a throwing reader, because callers
|
|
30
|
+
* read `null` as "no package.json": on Windows a BOM produced no error at all,
|
|
31
|
+
* it made the package look manifest-less, so `gjsify check` found no `check`
|
|
32
|
+
* script, discovery skipped the package, and nothing said why.
|
|
42
33
|
*/
|
|
43
34
|
export function readPackageJson(pkgPath) {
|
|
44
35
|
try {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prefix every line of `src` with `prefix` and route it to `sink`.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* — call it once the child has closed.
|
|
2
|
+
* Prefix every line of `src` with `prefix` and route it to `sink`. `buffered`
|
|
3
|
+
* withholds everything until the returned `flush()` runs — call that only once
|
|
4
|
+
* the child has closed.
|
|
6
5
|
*/
|
|
7
6
|
export declare function prefixLines(src: NodeJS.ReadableStream, sink: NodeJS.WritableStream, prefix: string, buffered: boolean): () => void;
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
// Shared line-prefixed child-output forwarding for the parallel runners
|
|
2
|
-
// (`gjsify foreach`, `gjsify check`)
|
|
2
|
+
// (`gjsify foreach`, `gjsify check`) — one home for it, because the second,
|
|
3
|
+
// unbuffered copy kept the hang below alive after the first was fixed.
|
|
3
4
|
//
|
|
4
|
-
// Under GJS
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// and concurrent flushes serialize into brief loop stalls instead of a
|
|
13
|
-
// deadlock. On a tty (interactive) lines are written live for responsive
|
|
14
|
-
// output.
|
|
15
|
-
//
|
|
16
|
-
// This helper is the single home for that logic — `foreach` shipped the fix
|
|
17
|
-
// first; `check` had an unbuffered copy that kept the hang class alive.
|
|
5
|
+
// Under GJS `process.stdout.write` is a BLOCKING `Gio.write_all`. Writing lines
|
|
6
|
+
// live during a PARALLEL run into a backpressuring sink (a slow-draining CI log
|
|
7
|
+
// collector) stalls the single GLib main loop on a full pipe → every child's pipe
|
|
8
|
+
// backs up → their reads stall → the run HANGS. A tty or file sink never
|
|
9
|
+
// backpressures, which is why this only bites in CI. So on a NON-tty sink each
|
|
10
|
+
// child's output is buffered and flushed as ONE write after it closes: the child
|
|
11
|
+
// can no longer stall, and concurrent flushes serialize into brief loop stalls
|
|
12
|
+
// instead of a deadlock. A tty gets live lines, for responsiveness.
|
|
18
13
|
/**
|
|
19
|
-
* Prefix every line of `src` with `prefix` and route it to `sink`.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* — call it once the child has closed.
|
|
14
|
+
* Prefix every line of `src` with `prefix` and route it to `sink`. `buffered`
|
|
15
|
+
* withholds everything until the returned `flush()` runs — call that only once
|
|
16
|
+
* the child has closed.
|
|
23
17
|
*/
|
|
24
18
|
export function prefixLines(src, sink, prefix, buffered) {
|
|
25
19
|
let buf = '';
|
package/lib/utils/prompt.d.ts
CHANGED
|
@@ -15,10 +15,9 @@ export interface KeyOutcome {
|
|
|
15
15
|
interrupt: boolean;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Pure key handler for the raw-mode line editor — the single source of truth
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* passwords, and treats Ctrl-C as an interrupt rather than text.
|
|
18
|
+
* Pure key handler for the raw-mode line editor — the single source of truth for how a keystroke
|
|
19
|
+
* updates the buffer + echo. Pure (no I/O) so the rules are unit-testable: Enter is `\r` OR `\n`,
|
|
20
|
+
* passwords mask, and Ctrl-C is an interrupt rather than text.
|
|
22
21
|
*/
|
|
23
22
|
export declare function applyKey(buf: string, ch: string, mask: boolean): KeyOutcome;
|
|
24
23
|
/** Print a question and read one visible line. */
|