@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.
Files changed (122) hide show
  1. package/dist/cli.gjs.mjs +169 -169
  2. package/lib/actions/build.d.ts +7 -0
  3. package/lib/actions/build.js +18 -3
  4. package/lib/bundler-pick.js +61 -87
  5. package/lib/cli-fail.d.ts +4 -7
  6. package/lib/cli-fail.js +29 -55
  7. package/lib/commands/affected.js +161 -286
  8. package/lib/commands/install.d.ts +63 -66
  9. package/lib/commands/install.js +372 -536
  10. package/lib/config.d.ts +14 -0
  11. package/lib/config.js +18 -1
  12. package/lib/templates/install.mjs.tmpl +164 -75
  13. package/lib/types/cli-build-options.d.ts +33 -67
  14. package/lib/types/command.d.ts +9 -12
  15. package/lib/types/config-data.d.ts +137 -221
  16. package/lib/utils/auth-npmrc.d.ts +5 -10
  17. package/lib/utils/auth-npmrc.js +11 -21
  18. package/lib/utils/bin-shim.d.ts +40 -25
  19. package/lib/utils/bin-shim.js +51 -26
  20. package/lib/utils/clear-targets.d.ts +8 -11
  21. package/lib/utils/clear-targets.js +22 -29
  22. package/lib/utils/copy-targets.d.ts +13 -19
  23. package/lib/utils/copy-targets.js +31 -44
  24. package/lib/utils/dep-aggregation.d.ts +7 -15
  25. package/lib/utils/dep-aggregation.js +9 -22
  26. package/lib/utils/detect-native-packages.d.ts +89 -122
  27. package/lib/utils/detect-native-packages.js +119 -169
  28. package/lib/utils/dir-link.d.ts +29 -48
  29. package/lib/utils/dir-link.js +33 -56
  30. package/lib/utils/discover-showcases.d.ts +6 -7
  31. package/lib/utils/discover-showcases.js +11 -15
  32. package/lib/utils/dlx-cache.d.ts +29 -38
  33. package/lib/utils/dlx-cache.js +53 -63
  34. package/lib/utils/force-exit.d.ts +0 -3
  35. package/lib/utils/force-exit.js +13 -24
  36. package/lib/utils/gi-search-path.d.ts +16 -24
  37. package/lib/utils/gi-search-path.js +54 -82
  38. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  39. package/lib/utils/gjs-bundle-guard.js +35 -57
  40. package/lib/utils/gjs-source-escape.d.ts +2 -2
  41. package/lib/utils/gjs-source-escape.js +21 -39
  42. package/lib/utils/gjsify-shim.d.ts +17 -20
  43. package/lib/utils/gjsify-shim.js +80 -120
  44. package/lib/utils/html-entry.d.ts +8 -12
  45. package/lib/utils/html-entry.js +19 -28
  46. package/lib/utils/install-backend-native.d.ts +123 -161
  47. package/lib/utils/install-backend-native.js +536 -737
  48. package/lib/utils/install-backend.d.ts +42 -57
  49. package/lib/utils/install-backend.js +16 -21
  50. package/lib/utils/install-cache-fs.d.ts +10 -15
  51. package/lib/utils/install-cache-fs.js +18 -26
  52. package/lib/utils/install-lock.d.ts +5 -6
  53. package/lib/utils/install-lock.js +46 -65
  54. package/lib/utils/install-packument-cache.d.ts +27 -33
  55. package/lib/utils/install-packument-cache.js +44 -57
  56. package/lib/utils/install-progress.d.ts +2 -9
  57. package/lib/utils/install-progress.js +14 -39
  58. package/lib/utils/install-tarball-cache.d.ts +14 -20
  59. package/lib/utils/install-tarball-cache.js +41 -59
  60. package/lib/utils/library-output.d.ts +9 -22
  61. package/lib/utils/library-output.js +19 -37
  62. package/lib/utils/load-npmrc.d.ts +5 -6
  63. package/lib/utils/load-npmrc.js +18 -27
  64. package/lib/utils/node-bundle-guard.d.ts +4 -8
  65. package/lib/utils/node-bundle-guard.js +21 -35
  66. package/lib/utils/node-script.js +40 -53
  67. package/lib/utils/node-version.d.ts +5 -14
  68. package/lib/utils/node-version.js +15 -24
  69. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  70. package/lib/utils/normalize-bundler-options.js +24 -42
  71. package/lib/utils/npm-oidc.d.ts +23 -37
  72. package/lib/utils/npm-oidc.js +28 -53
  73. package/lib/utils/npm-otp-cache.d.ts +4 -4
  74. package/lib/utils/npm-otp-cache.js +20 -25
  75. package/lib/utils/npm-otp.d.ts +20 -30
  76. package/lib/utils/npm-otp.js +32 -51
  77. package/lib/utils/onboard-probe.d.ts +7 -13
  78. package/lib/utils/onboard-probe.js +16 -27
  79. package/lib/utils/parse-spec.d.ts +3 -3
  80. package/lib/utils/parse-spec.js +4 -5
  81. package/lib/utils/pkg-json-edit.d.ts +7 -12
  82. package/lib/utils/pkg-json-edit.js +13 -26
  83. package/lib/utils/pkg-json.d.ts +11 -18
  84. package/lib/utils/pkg-json.js +19 -28
  85. package/lib/utils/prefixed-output.d.ts +3 -4
  86. package/lib/utils/prefixed-output.js +13 -19
  87. package/lib/utils/prompt.d.ts +3 -4
  88. package/lib/utils/prompt.js +14 -18
  89. package/lib/utils/publish-diagnose.d.ts +11 -16
  90. package/lib/utils/publish-diagnose.js +15 -28
  91. package/lib/utils/publish-headers.d.ts +7 -11
  92. package/lib/utils/publish-headers.js +36 -65
  93. package/lib/utils/publishable-packages.d.ts +4 -6
  94. package/lib/utils/publishable-packages.js +14 -18
  95. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  96. package/lib/utils/resolve-gjs-entry.js +10 -27
  97. package/lib/utils/resolve-npm-package.d.ts +6 -11
  98. package/lib/utils/resolve-npm-package.js +29 -63
  99. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  100. package/lib/utils/resolve-plugin-by-name.js +9 -14
  101. package/lib/utils/run-gjs.d.ts +16 -21
  102. package/lib/utils/run-gjs.js +31 -56
  103. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  104. package/lib/utils/run-lifecycle-script.js +23 -42
  105. package/lib/utils/run-node.d.ts +21 -33
  106. package/lib/utils/run-node.js +43 -64
  107. package/lib/utils/runtimes.d.ts +25 -35
  108. package/lib/utils/runtimes.js +34 -46
  109. package/lib/utils/spawn.d.ts +32 -48
  110. package/lib/utils/spawn.js +51 -84
  111. package/lib/utils/stdin.d.ts +2 -2
  112. package/lib/utils/stdin.js +10 -25
  113. package/lib/utils/suggest.d.ts +6 -7
  114. package/lib/utils/suggest.js +8 -10
  115. package/lib/utils/system-gi.d.ts +36 -27
  116. package/lib/utils/system-gi.js +85 -114
  117. package/lib/utils/trust-registry.d.ts +4 -8
  118. package/lib/utils/trust-registry.js +7 -15
  119. package/lib/utils/win32-command.d.ts +8 -14
  120. package/lib/utils/win32-command.js +29 -43
  121. package/lib/utils/workspace-root.js +7 -12
  122. package/package.json +18 -18
@@ -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
- // containing `otp` (or, for malformed responses, a body mentioning
6
- // "one-time pass" / `EOTP`) — see
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
- // Two pieces were previously duplicated in `publish.ts` and `trust.ts`:
10
- // 1. detecting the challenge (`isOtpChallenge`);
11
- // 2. prompting for a code, then retrying the request with it.
12
- //
13
- // They are centralised here so a single `OtpProvider` can be injected across
14
- // MANY operations in one run. `gjsify onboard` sweeps ~110 packages and would
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 the user typed). `refresh()` prompts for a new
46
- * code and caches it (in-process AND — when a `registry` is set — in the
47
- * short-lived file cache). A single provider instance shared across many
48
- * publish + trust operations is what makes `gjsify onboard` need only one OTP
49
- * entry for the whole sweep; the file cache extends that reuse across SEPARATE
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
- * it (the trimmed input, or `''` when nothing was entered — the caller
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
- * after npm rejects it as invalid/consumed — so the NEXT command re-prompts
100
- * instead of replaying a single-use code that the registry already burned.
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
- * Flow:
112
- * 1. First attempt — without an OTP (or WITH the cached code when
113
- * `seedFirstAttempt` is set).
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 actual fetch + all response interpretation; this only
123
- * owns the OTP dance.
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 (`--otp`/file-cached) code was rejected/consumed — drop it so
134
- // it is not replayed and the cross-invocation cache entry is cleared.
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
- // Cached code rejected/consumed (stale seed, or a sibling command
143
- // already burned the file-cached single-use code) → invalidate.
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
- // The freshly-typed code was also rejected — clear it before re-prompting
155
- // so a wedged sequence never leaves a burned code in the file cache.
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 via the SHARED trust requester —
40
- * the exact `trustUrl` + `buildHeaders` auth + `withOtpRetry` path `gjsify
41
- * trust` uses. On a transient `auth-required` (401 that was not an answerable
42
- * OTP challenge — e.g. a parallel-burst rejection or a lapsed 2FA-skip window),
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 package is read SERIALLY so a
48
- * 2FA-gated read triggers the single shared-OTP prompt (typed once) before the
49
- * concurrent burst — the rest reuse the now-cached code — and so a single token
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
- // be unit-tested against a mocked requester (no live npm).
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
- // The bug this fixes (task #60): `gjsify onboard` read all ~127 packages'
5
- // trust state with a PLAIN authenticated `fetch` (no OTP handling) run in a
6
- // wide concurrent burst — every read came back `401 → auth-required →
7
- // unreadable`, so the sweep planned `0 to publish+trust, 0 to trust, 127
8
- // unreadable`. Meanwhile a targeted `gjsify trust <pkg> --otp <code>` READ the
9
- // same state fine with the SAME token, because its read runs through the shared
10
- // `TrustRequester` (→ `withOtpRetry`): a 401 that is an OTP challenge is
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 via the SHARED trust requester —
43
- * the exact `trustUrl` + `buildHeaders` auth + `withOtpRetry` path `gjsify
44
- * trust` uses. On a transient `auth-required` (401 that was not an answerable
45
- * OTP challenge — e.g. a parallel-burst rejection or a lapsed 2FA-skip window),
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 package is read SERIALLY so a
64
- * 2FA-gated read triggers the single shared-OTP prompt (typed once) before the
65
- * concurrent burst — the rest reuse the now-cached code — and so a single token
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
- * The full spec string is preserved on the registry case so it can be passed
17
- * verbatim to `npm install` (which already understands dist-tags, ranges,
18
- * git URIs, tarball URLs, etc. — we don't re-parse those).
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;
@@ -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
- * The full spec string is preserved on the registry case so it can be passed
12
- * verbatim to `npm install` (which already understands dist-tags, ranges,
13
- * git URIs, tarball URLs, etc. — we don't re-parse those).
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
- // Registry spec: split off the version after the LAST `@` that isn't the
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
- * Collect existing dependencies + devDependencies + optionalDependencies
33
- * from a project package.json into installable specs of the form
34
- * `name@range`. Used by `gjsify install` (no args) to seed the resolver
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`. If the spec didn't include
41
- * a range, callers fill in the installed version after resolution and
42
- * call this again with `installedVersion` set.
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
- // Mirrors npm's `--save-{prod,dev,peer,optional}` semantics:
4
- // - default → dependencies (production)
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
- * Collect existing dependencies + devDependencies + optionalDependencies
52
- * from a project package.json into installable specs of the form
53
- * `name@range`. Used by `gjsify install` (no args) to seed the resolver
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
- // Skip workspace: / link: / file: / portal: specifiers — those
65
- // are workspace-local references handled by Phase D.3, not by
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`. If the spec didn't include
78
- * a range, callers fill in the installed version after resolution and
79
- * call this again with `installedVersion` set.
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
  }
@@ -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`, the decoded form of `EF BB BF`).
3
+ * Drop a leading UTF-8 BOM (`U+FEFF`).
4
4
  *
5
- * `JSON.parse` rejects a BOM — it is not whitespace — so a manifest carrying
6
- * one is "invalid JSON" to every reader here. That is not a hypothetical file
7
- * shape on Windows: Windows PowerShell 5.1 writes a BOM for `-Encoding utf8`,
8
- * which is its DEFAULT for `Out-File`/`Set-Content`, so any script that edits a
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
- * npm reads such a file without complaint (`npm pkg get name` on a BOM'd
12
- * manifest returns the name), so the file is valid as far as the ecosystem is
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 MORE here than in a throwing reader: this one reports
25
- * an unparseable manifest as `null`, which every caller reads as "no
26
- * package.json". On Windows a BOM therefore did not produce an error — it made
27
- * a package look like it had no manifest at all, so `gjsify check` found no
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;
@@ -1,31 +1,23 @@
1
- // Single permissive reader for `package.json` files, shared by the CLI's
2
- // workspace- and dependency-discovery paths (`gjsify run`/`workspace`/`foreach`/
3
- // `check`, native-prebuild scanning). Returns `null` when the file is missing
4
- // OR unparseable — discovery must degrade gracefully on a stray or corrupt
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 its own STRICTER reader in `pkg-json-edit.ts`
8
- // (`readPackageJson` there): it returns `null` only when the file is absent but
9
- // THROWS on invalid JSON, so a corrupt manifest is never silently overwritten
10
- // during `gjsify install <pkg>`. That contract is intentional and load-bearing
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`, the decoded form of `EF BB BF`).
12
+ * Drop a leading UTF-8 BOM (`U+FEFF`).
15
13
  *
16
- * `JSON.parse` rejects a BOM — it is not whitespace — so a manifest carrying
17
- * one is "invalid JSON" to every reader here. That is not a hypothetical file
18
- * shape on Windows: Windows PowerShell 5.1 writes a BOM for `-Encoding utf8`,
19
- * which is its DEFAULT for `Out-File`/`Set-Content`, so any script that edits a
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
- * npm reads such a file without complaint (`npm pkg get name` on a BOM'd
23
- * manifest returns the name), so the file is valid as far as the ecosystem is
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 MORE here than in a throwing reader: this one reports
38
- * an unparseable manifest as `null`, which every caller reads as "no
39
- * package.json". On Windows a BOM therefore did not produce an error — it made
40
- * a package look like it had no manifest at all, so `gjsify check` found no
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`. When
3
- * `buffered` is false (tty), lines are written live. When true (non-tty /
4
- * CI pipe), they are accumulated and emitted only by the returned `flush()`
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, `process.stdout.write` is a BLOCKING `Gio.write_all`. Writing
5
- // each prefixed line LIVE during a PARALLEL run into a backpressuring pipe
6
- // (a CI log collector that drains slowly) stalls the single GLib main loop
7
- // on a full pipe → every parallel child's pipe backs up → their reads stall
8
- // → the whole run HANGS. (A tty or file sink never backpressures, which is
9
- // why this only ever bites in CI.) On a NON-tty sink we therefore BUFFER
10
- // each child's prefixed output and flush it as ONE write when that child
11
- // closes: the child is already done by then, so its own read can't stall,
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`. When
20
- * `buffered` is false (tty), lines are written live. When true (non-tty /
21
- * CI pipe), they are accumulated and emitted only by the returned `flush()`
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 = '';
@@ -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
- * for how a keystroke updates the buffer + echo. Kept pure (no I/O) so it is
20
- * unit-testable: this is what makes Enter (`\r` OR `\n`) reliably submit, masks
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. */