@bobfrankston/npmglobalize 1.0.208 → 1.0.209

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 (3) hide show
  1. package/lib.d.ts +8 -0
  2. package/lib.js +115 -14
  3. package/package.json +1 -1
package/lib.d.ts CHANGED
@@ -485,6 +485,14 @@ export declare function ensureWorkspaceDepModules(rootDir: string, members: Arra
485
485
  * `npm error` lines minus stack frames and brace-only object-dump fragments,
486
486
  * deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
487
487
  export declare function distillNpmErrors(combined: string): string[];
488
+ /** True when a failed install is npm's "no such version" (ETARGET/E404) for the
489
+ * very spec we just published — registry propagation or a stale local
490
+ * packument, not a broken package. `pkgSpec` is `name@version`. */
491
+ export declare function isPropagationFailure(combined: string, pkgSpec: string): boolean;
492
+ /** What to tell the user when an install fails purely because npm cannot see a
493
+ * just-published version yet. Says what IS true (published, will install),
494
+ * what is NOT (nothing wrong with the package), and one command that works. */
495
+ export declare function propagationHelp(pkgSpec: string): string[];
488
496
  /** Scan a puppeteer browser cache (layout <root>/<browser>/<platform-version>)
489
497
  * and repair every version folder that has no browser executable in it — the
490
498
  * residue of a truncated extraction (see extractBrowserArchive for the Node
package/lib.js CHANGED
@@ -2128,13 +2128,30 @@ async function npmInstallWithCleanRetry(dir, verbose, context) {
2128
2128
  * resolvable. We wait longer and re-probe `npm view` for the version
2129
2129
  * string until it shows up (or we hit the cap). */
2130
2130
  function waitForNpmVersion(pkgName, version, isNewPackage = false, maxWaitMs) {
2131
- const effectiveMaxWait = maxWaitMs ?? (isNewPackage ? 600000 : 180000);
2132
- const interval = isNewPackage ? 5000 : 3000;
2131
+ // 2026-09-01 10:15 EDT Claude Code (Opus 5), at Bob's direction.
2132
+ // This loop was polling npm's LOCAL cache, not the registry. Registry
2133
+ // packuments carry `cache-control: public, max-age=300`, and npm's default
2134
+ // (prefer-online=false) serves a cached packument for that whole window
2135
+ // without revalidating — so once the first probe cached a packument that
2136
+ // predates the publish, every probe for the next 5 minutes returned the
2137
+ // same stale answer no matter what the registry actually held. Verified
2138
+ // 2026-09-01 on @bobfrankston/brother-label@1.1.22: the npm debug log for
2139
+ // the failing install shows `http cache … (cache hit)` — it never asked
2140
+ // the registry — while the cached entry's own resHeaders read
2141
+ // `cache-control: public, max-age=300`. `--prefer-online` forces the
2142
+ // staleness check (a conditional request), which is the only way this
2143
+ // poll can observe anything new.
2144
+ // Same run: publish at 13:52:48Z, packument `last-modified 13:57:59Z` —
2145
+ // real propagation took 5m11s, past the old 180s cap for an existing
2146
+ // package. Raised; with a probe that actually revalidates, an early
2147
+ // success still exits immediately, so the higher cap costs nothing.
2148
+ const effectiveMaxWait = maxWaitMs ?? (isNewPackage ? 600000 : 420000);
2149
+ const interval = 5000;
2133
2150
  const maxAttempts = Math.ceil(effectiveMaxWait / interval);
2134
2151
  const suffix = isNewPackage ? ' (new package, may take several minutes)' : '';
2135
2152
  process.stdout.write(`${timestamp()} Waiting for ${pkgName}@${version} on npm registry${suffix}`);
2136
2153
  for (let i = 0; i < maxAttempts; i++) {
2137
- const result = spawnSafe('npm', ['view', `${pkgName}@${version}`, 'version'], {
2154
+ const result = spawnSafe('npm', ['view', '--prefer-online', `${pkgName}@${version}`, 'version'], {
2138
2155
  shell: process.platform === 'win32',
2139
2156
  stdio: ['pipe', 'pipe', 'pipe'],
2140
2157
  encoding: 'utf-8'
@@ -2147,6 +2164,10 @@ function waitForNpmVersion(pkgName, version, isNewPackage = false, maxWaitMs) {
2147
2164
  sleepSync(interval);
2148
2165
  }
2149
2166
  process.stdout.write(' timed out\n');
2167
+ // Timing out here is not a publish failure — the version is on npm, this
2168
+ // client just cannot see it yet. Say so, or the install error that follows
2169
+ // reads as though the package itself were broken.
2170
+ console.log(colors.yellow(` ${pkgName}@${version} was published but is still not visible to npm after ${Math.round(effectiveMaxWait / 1000)}s — trying the install anyway.`));
2150
2171
  return false;
2151
2172
  }
2152
2173
  function cleanNestedDepModules(pkg, cwd, verbose) {
@@ -3729,6 +3750,38 @@ export function distillNpmErrors(combined) {
3729
3750
  }
3730
3751
  return key.slice(0, 12);
3731
3752
  }
3753
+ // 2026-09-01 10:40 EDT — Claude Code (Opus 5), at Bob's direction: "it installed
3754
+ // correctly on a retry - need better error messages". What the run printed was
3755
+ // npm's own text — "No matching version found ... In most cases you or one of
3756
+ // your dependencies are requesting a package version that doesn't exist" —
3757
+ // which is flatly wrong here: WE published that exact version 4 seconds
3758
+ // earlier, and a manual retry a minute later installed it. The two helpers
3759
+ // below name the real situation instead.
3760
+ /** True when a failed install is npm's "no such version" (ETARGET/E404) for the
3761
+ * very spec we just published — registry propagation or a stale local
3762
+ * packument, not a broken package. `pkgSpec` is `name@version`. */
3763
+ export function isPropagationFailure(combined, pkgSpec) {
3764
+ if (pkgSpec === '.')
3765
+ return false; // local-directory install can't be a registry-visibility problem
3766
+ if (!/npm (?:error|ERR!)\s+code E(?:TARGET|404)\b/.test(combined))
3767
+ return false;
3768
+ const name = pkgSpec.replace(/@[^@/]+$/, ''); // strip the trailing @version, keep any @scope
3769
+ return combined.includes(pkgSpec) || combined.includes(name);
3770
+ }
3771
+ /** What to tell the user when an install fails purely because npm cannot see a
3772
+ * just-published version yet. Says what IS true (published, will install),
3773
+ * what is NOT (nothing wrong with the package), and one command that works. */
3774
+ export function propagationHelp(pkgSpec) {
3775
+ return [
3776
+ `${pkgSpec} is published — npm just can't see it from this machine yet.`,
3777
+ ` The registry takes up to several minutes to propagate a new version, and npm`,
3778
+ ` caches the package's version list for 5 minutes (cache-control: max-age=300),`,
3779
+ ` so a retry inside that window can still miss it unless the cache is bypassed.`,
3780
+ ` Nothing is wrong with the package or the publish; the global copy is simply the`,
3781
+ ` previous version until you re-run:`,
3782
+ ` npm install -g --prefer-online ${pkgSpec}`,
3783
+ ];
3784
+ }
3732
3785
  /** Write the full output of a failed npm command to a log file under the temp
3733
3786
  * dir and return its path (null if the write itself fails). */
3734
3787
  function saveNpmLog(label, content) {
@@ -4054,8 +4107,16 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
4054
4107
  // stays behind npm's allowScripts gate.
4055
4108
  const allowArgs = allowOwnScriptsArgs(cwd);
4056
4109
  while (true) {
4057
- console.log(colors.cyan(`> npm install -g ${pkgSpec}${allowArgs.length ? ` ${allowArgs.join(' ')}` : ''}`));
4058
- result = await runCommandAsync('npm', ['install', '-g', pkgSpec, ...allowArgs], { cwd, silent: true });
4110
+ // 2026-09-01 10:15 EDT Claude Code (Opus 5), at Bob's direction.
4111
+ // `--prefer-online`: this install always targets a version published
4112
+ // seconds ago, so a cached packument is exactly the hazard. Without it
4113
+ // all three ETARGET retries re-read the same stale local cache and are
4114
+ // guaranteed to fail identically — confirmed in npm's debug log for
4115
+ // brother-label@1.1.22 (`http cache … (cache hit)`, no registry
4116
+ // request at all). Costs one conditional request per packument.
4117
+ const online = pkgSpec === '.' ? [] : ['--prefer-online'];
4118
+ console.log(colors.cyan(`> npm install -g ${[...online, pkgSpec, ...allowArgs].join(' ')}`));
4119
+ result = await runCommandAsync('npm', ['install', '-g', ...online, pkgSpec, ...allowArgs], { cwd, silent: true });
4059
4120
  const combined = `${result.output}\n${result.stderr}`;
4060
4121
  fullLog += `===== attempt ${attempt + 1}: npm install -g ${pkgSpec} =====\n${combined}\n`;
4061
4122
  if (result.success) {
@@ -4076,8 +4137,19 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
4076
4137
  const cleanupFails = (combined.match(/npm warn cleanup\s+\[Error: EPERM/g) || []).length;
4077
4138
  if (cleanupFails)
4078
4139
  console.log(colors.dim(` (npm couldn't remove ${cleanupFails} leftover dir(s) — EPERM cleanup warnings, details in log)`));
4079
- for (const line of distillNpmErrors(combined))
4140
+ const propagating = isPropagationFailure(combined, pkgSpec);
4141
+ for (const line of distillNpmErrors(combined)) {
4142
+ // npm's stock follow-up to ETARGET blames the request ("you or one of
4143
+ // your dependencies are requesting a version that doesn't exist").
4144
+ // For a version we published seconds ago that sends the reader
4145
+ // hunting a nonexistent dependency bug — drop it and say the real
4146
+ // thing below.
4147
+ if (propagating && /In most cases you or one of your dependencies/.test(line))
4148
+ continue;
4080
4149
  console.error(colors.red(` ${line}`));
4150
+ }
4151
+ if (propagating)
4152
+ console.error(colors.yellow(` ↻ ${pkgSpec} not visible to npm yet — registry propagation, not a bad package.`));
4081
4153
  // Known-fixable failure: repair and retry immediately — the cause is
4082
4154
  // local, so waiting for registry propagation is pointless. Puppeteer
4083
4155
  // reports one corrupted browser per run, so allow a few repair rounds
@@ -4288,7 +4360,10 @@ async function waitForNpmVersionInWsl(spec, maxWaitMs = 180000) {
4288
4360
  const maxAttempts = Math.ceil(maxWaitMs / interval);
4289
4361
  process.stdout.write(`${timestamp()} Waiting for ${spec} on npm registry (WSL view)`);
4290
4362
  for (let i = 0; i < maxAttempts; i++) {
4291
- const r = await runCommandAsync('wsl', ['npm', 'view', spec, 'version'], { silent: true });
4363
+ // 2026-09-01 10:15 EDT Claude Code: --prefer-online for the same
4364
+ // reason as waitForNpmVersion — WSL's npm has its own cache with the
4365
+ // same 300s packument freshness window to poll past.
4366
+ const r = await runCommandAsync('wsl', ['npm', 'view', '--prefer-online', spec, 'version'], { silent: true });
4292
4367
  if (r.success && (r.output || '').trim()) {
4293
4368
  process.stdout.write(' ready\n');
4294
4369
  return true;
@@ -5816,7 +5891,9 @@ async function doLocalInstall(cwd, options) {
5816
5891
  console.log(' [dry-run] Would run: wsl npm install -g .');
5817
5892
  return true;
5818
5893
  }
5819
- const result = await runCommandAsync('npm', ['install', '-g', '.', ...allowOwnScriptsArgs(cwd)], { cwd, silent: false, showCommand: true });
5894
+ // 2026-09-01 10:55 EDT Claude Code: see the note on the sibling local-install
5895
+ // path — captured, distilled output instead of raw npm chatter.
5896
+ const result = await installGlobalWithRetry('.', cwd, false, 1);
5820
5897
  if (result.success) {
5821
5898
  console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
5822
5899
  }
@@ -5932,7 +6009,16 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
5932
6009
  console.log(' [dry-run] Would run: wsl npm install -g .');
5933
6010
  return true;
5934
6011
  }
5935
- const result = await runCommandAsync('npm', ['install', '-g', '.', ...allowOwnScriptsArgs(cwd)], { cwd, silent: false, showCommand: true });
6012
+ // 2026-09-01 10:55 EDT Claude Code (Opus 5), at Bob's direction. Was a
6013
+ // raw streamed `npm install -g .`, so the terminal got npm's chatter
6014
+ // verbatim — including `npm warn allow-scripts .npmrc allow-scripts
6015
+ // setting is being ignored because --allow-scripts was passed`, a
6016
+ // warning npmglobalize provokes on itself by passing that flag, and
6017
+ // which no reader can act on. installGlobalWithRetry captures the
6018
+ // output and prints only what matters (the added/changed/removed line,
6019
+ // or distilled errors plus a log file), which is what every other
6020
+ // install path here already does.
6021
+ const result = await installGlobalWithRetry('.', cwd, false, 1);
5936
6022
  if (result.success) {
5937
6023
  console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
5938
6024
  }
@@ -7243,8 +7329,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
7243
7329
  console.error(colors.yellow(` Run 'npm login' to fix, then: npm install -g ${pkgName}@${pkgVersion}`));
7244
7330
  }
7245
7331
  else {
7246
- console.error(colors.red(`✗ Global install failed`));
7247
- console.error(colors.yellow(` Try running manually: npm install -g ${pkgName}@${pkgVersion}`));
7332
+ const spec = `${pkgName}@${pkgVersion}`;
7333
+ if (isPropagationFailure(`${registryInstallResult.output}\n${registryInstallResult.stderr}`, spec)) {
7334
+ for (const line of propagationHelp(spec))
7335
+ console.error(colors.yellow(` ${line}`));
7336
+ }
7337
+ else {
7338
+ console.error(colors.red(`✗ Global install failed`));
7339
+ console.error(colors.yellow(` Try running manually: npm install -g --prefer-online ${spec}`));
7340
+ }
7248
7341
  }
7249
7342
  }
7250
7343
  }
@@ -8088,9 +8181,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
8088
8181
  recordBuildIssue(pkgName, 'warning', `Global install failed (${auth.error || 'auth issue'}) — run 'npm login'`);
8089
8182
  }
8090
8183
  else {
8091
- console.error(colors.red(`✗ Global install failed`));
8092
- console.error(colors.yellow(` Try running manually: npm install -g ${pkgName}@${pkgVersion}`));
8093
- recordBuildIssue(pkgName, 'warning', 'Global install failed');
8184
+ const spec = `${pkgName}@${pkgVersion}`;
8185
+ if (isPropagationFailure(`${installResult.output}\n${installResult.stderr}`, spec)) {
8186
+ for (const line of propagationHelp(spec))
8187
+ console.error(colors.yellow(` ${line}`));
8188
+ recordBuildIssue(pkgName, 'warning', `Published, but npm could not see ${spec} yet — install with: npm install -g --prefer-online ${spec}`);
8189
+ }
8190
+ else {
8191
+ console.error(colors.red(`✗ Global install failed`));
8192
+ console.error(colors.yellow(` Try running manually: npm install -g --prefer-online ${spec}`));
8193
+ recordBuildIssue(pkgName, 'warning', 'Global install failed');
8194
+ }
8094
8195
  }
8095
8196
  }
8096
8197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.208",
3
+ "version": "1.0.209",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",