@bobfrankston/npmglobalize 1.0.167 → 1.0.168

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 (4) hide show
  1. package/lib/git.d.ts +12 -4
  2. package/lib/git.js +29 -11
  3. package/lib.js +69 -58
  4. package/package.json +1 -1
package/lib/git.d.ts CHANGED
@@ -51,10 +51,18 @@ export declare function parseVersionTag(tag: string): number[] | null;
51
51
  export declare function compareVersions(a: number[], b: number[]): number;
52
52
  /** Fix version/tag mismatches */
53
53
  export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
54
- /** Wait for a package version to appear on the npm registry */
55
- export declare function waitForNpmVersion(pkgName: string, version: string, maxWaitMs?: number): boolean;
56
- /** Run npm install -g with retries for registry propagation delay */
57
- export declare function installGlobalWithRetry(pkgSpec: string, cwd: string, maxRetries?: number): {
54
+ /** Wait for a package version to appear on the npm registry.
55
+ * First-time publishes (brand-new package name) take much longer to
56
+ * propagate than version bumps npm has no cached metadata to update,
57
+ * so the registry/CDN can take several minutes before the package is
58
+ * resolvable. We wait longer and re-probe `npm view` for the version
59
+ * string until it shows up (or we hit the cap). */
60
+ export declare function waitForNpmVersion(pkgName: string, version: string, isNewPackage?: boolean, maxWaitMs?: number): boolean;
61
+ /** Run npm install -g with retries for registry propagation delay.
62
+ * Brand-new packages (first-time publish) take much longer to become
63
+ * installable than version bumps, so we use longer waits and more
64
+ * attempts when `isNewPackage` is true. */
65
+ export declare function installGlobalWithRetry(pkgSpec: string, cwd: string, isNewPackage?: boolean, maxRetries?: number): {
58
66
  success: boolean;
59
67
  output: string;
60
68
  stderr: string;
package/lib/git.js CHANGED
@@ -469,11 +469,24 @@ export function fixVersionTagMismatch(cwd, pkg, verbose = false) {
469
469
  return deletedAny;
470
470
  }
471
471
  // ── Group 5: npm install helpers (private) ──────────────────────────
472
- /** Wait for a package version to appear on the npm registry */
473
- export function waitForNpmVersion(pkgName, version, maxWaitMs = 90000) {
474
- const interval = 3000;
475
- const maxAttempts = Math.ceil(maxWaitMs / interval);
476
- process.stdout.write(`Waiting for ${pkgName}@${version} on npm registry`);
472
+ /** Reliable synchronous sleep works regardless of stdio/TTY state.
473
+ * (Windows `timeout /t` exits immediately when stdio is piped, which broke
474
+ * the previous spawn-based sleep.) */
475
+ function sleepSync(ms) {
476
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
477
+ }
478
+ /** Wait for a package version to appear on the npm registry.
479
+ * First-time publishes (brand-new package name) take much longer to
480
+ * propagate than version bumps — npm has no cached metadata to update,
481
+ * so the registry/CDN can take several minutes before the package is
482
+ * resolvable. We wait longer and re-probe `npm view` for the version
483
+ * string until it shows up (or we hit the cap). */
484
+ export function waitForNpmVersion(pkgName, version, isNewPackage = false, maxWaitMs) {
485
+ const effectiveMaxWait = maxWaitMs ?? (isNewPackage ? 600000 : 180000);
486
+ const interval = isNewPackage ? 5000 : 3000;
487
+ const maxAttempts = Math.ceil(effectiveMaxWait / interval);
488
+ const suffix = isNewPackage ? ' (new package, may take several minutes)' : '';
489
+ process.stdout.write(`Waiting for ${pkgName}@${version} on npm registry${suffix}`);
477
490
  for (let i = 0; i < maxAttempts; i++) {
478
491
  const result = spawnSafe('npm', ['view', `${pkgName}@${version}`, 'version'], {
479
492
  shell: process.platform === 'win32',
@@ -485,17 +498,22 @@ export function waitForNpmVersion(pkgName, version, maxWaitMs = 90000) {
485
498
  return true;
486
499
  }
487
500
  process.stdout.write('.');
488
- spawnSafe(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '3', '/nobreak'] : ['3'], { stdio: 'pipe', shell: process.platform === 'win32' });
501
+ sleepSync(interval);
489
502
  }
490
503
  process.stdout.write(' timed out\n');
491
504
  return false;
492
505
  }
493
- /** Run npm install -g with retries for registry propagation delay */
494
- export function installGlobalWithRetry(pkgSpec, cwd, maxRetries = 3) {
506
+ /** Run npm install -g with retries for registry propagation delay.
507
+ * Brand-new packages (first-time publish) take much longer to become
508
+ * installable than version bumps, so we use longer waits and more
509
+ * attempts when `isNewPackage` is true. */
510
+ export function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRetries) {
511
+ const retries = maxRetries ?? (isNewPackage ? 6 : 3);
512
+ const delaySec = isNewPackage ? 30 : 10;
495
513
  let result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
496
- for (let attempt = 1; attempt < maxRetries && !result.success; attempt++) {
497
- console.log(colors.yellow(` Retrying install (attempt ${attempt + 1}/${maxRetries}) in 10 seconds...`));
498
- spawnSafe(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '10', '/nobreak'] : ['10'], { stdio: 'pipe', shell: process.platform === 'win32' });
514
+ for (let attempt = 1; attempt < retries && !result.success; attempt++) {
515
+ console.log(colors.yellow(` Retrying install (attempt ${attempt + 1}/${retries}) in ${delaySec} seconds...`));
516
+ sleepSync(delaySec * 1000);
499
517
  result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
500
518
  }
501
519
  return result;
package/lib.js CHANGED
@@ -1365,11 +1365,24 @@ export function fixVersionTagMismatch(cwd, pkg, verbose = false) {
1365
1365
  }
1366
1366
  return deletedAny;
1367
1367
  }
1368
- /** Wait for a package version to appear on the npm registry */
1369
- function waitForNpmVersion(pkgName, version, maxWaitMs = 90000) {
1370
- const interval = 3000;
1371
- const maxAttempts = Math.ceil(maxWaitMs / interval);
1372
- process.stdout.write(`${timestamp()} Waiting for ${pkgName}@${version} on npm registry`);
1368
+ /** Reliable synchronous sleep works regardless of stdio/TTY state.
1369
+ * (Windows `timeout /t` exits immediately when stdio is piped, which broke
1370
+ * the previous spawn-based sleep.) */
1371
+ function sleepSync(ms) {
1372
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1373
+ }
1374
+ /** Wait for a package version to appear on the npm registry.
1375
+ * First-time publishes (brand-new package name) take much longer to
1376
+ * propagate than version bumps — npm has no cached metadata to update,
1377
+ * so the registry/CDN can take several minutes before the package is
1378
+ * resolvable. We wait longer and re-probe `npm view` for the version
1379
+ * string until it shows up (or we hit the cap). */
1380
+ function waitForNpmVersion(pkgName, version, isNewPackage = false, maxWaitMs) {
1381
+ const effectiveMaxWait = maxWaitMs ?? (isNewPackage ? 600000 : 180000);
1382
+ const interval = isNewPackage ? 5000 : 3000;
1383
+ const maxAttempts = Math.ceil(effectiveMaxWait / interval);
1384
+ const suffix = isNewPackage ? ' (new package, may take several minutes)' : '';
1385
+ process.stdout.write(`${timestamp()} Waiting for ${pkgName}@${version} on npm registry${suffix}`);
1373
1386
  for (let i = 0; i < maxAttempts; i++) {
1374
1387
  const result = spawnSafe('npm', ['view', `${pkgName}@${version}`, 'version'], {
1375
1388
  shell: process.platform === 'win32',
@@ -1381,7 +1394,7 @@ function waitForNpmVersion(pkgName, version, maxWaitMs = 90000) {
1381
1394
  return true;
1382
1395
  }
1383
1396
  process.stdout.write('.');
1384
- spawnSafe(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '3', '/nobreak'] : ['3'], { stdio: 'pipe', shell: process.platform === 'win32' });
1397
+ sleepSync(interval);
1385
1398
  }
1386
1399
  process.stdout.write(' timed out\n');
1387
1400
  return false;
@@ -1687,12 +1700,17 @@ export function ensureWorkspaceDepModules(rootDir, members, verbose = false) {
1687
1700
  console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1688
1701
  }
1689
1702
  }
1690
- /** Run npm install -g with retries for registry propagation delay */
1691
- function installGlobalWithRetry(pkgSpec, cwd, maxRetries = 3) {
1703
+ /** Run npm install -g with retries for registry propagation delay.
1704
+ * Brand-new packages (first-time publish) take much longer to become
1705
+ * installable than version bumps, so we use longer waits and more
1706
+ * attempts when `isNewPackage` is true. */
1707
+ function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRetries) {
1708
+ const retries = maxRetries ?? (isNewPackage ? 6 : 3);
1709
+ const delaySec = isNewPackage ? 30 : 10;
1692
1710
  let result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
1693
- for (let attempt = 1; attempt < maxRetries && !result.success; attempt++) {
1694
- console.log(colors.yellow(` Retrying install (attempt ${attempt + 1}/${maxRetries}) in 10 seconds...`));
1695
- spawnSafe(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '10', '/nobreak'] : ['10'], { stdio: 'pipe', shell: process.platform === 'win32' });
1711
+ for (let attempt = 1; attempt < retries && !result.success; attempt++) {
1712
+ console.log(colors.yellow(` Retrying install (attempt ${attempt + 1}/${retries}) in ${delaySec} seconds...`));
1713
+ sleepSync(delaySec * 1000);
1696
1714
  result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
1697
1715
  }
1698
1716
  return result;
@@ -3039,25 +3057,6 @@ export function getToolVersion() {
3039
3057
  return 'unknown';
3040
3058
  }
3041
3059
  }
3042
- /** Offer to add a bin field if missing — returns true if bin was added, false to skip/abort */
3043
- async function offerAddBin(cwd, pkg) {
3044
- if (pkg.bin)
3045
- return true;
3046
- // Determine likely entry point
3047
- const mainFile = pkg.main || 'index.js';
3048
- const cmdName = (pkg.name || path.basename(cwd)).replace(/^@[^/]+\//, '');
3049
- console.log(colors.yellow('No bin field — this is a library, not a CLI tool.'));
3050
- console.log(colors.yellow('Libraries should not be installed globally. Use npm install <name> in projects instead.'));
3051
- const choice = await promptChoice(`Add bin field to make it a CLI tool?\n 1) Yes, use "${cmdName}" → "${mainFile}"\n 2) Skip global install (default)\nChoice:`, ['1', '2', '']);
3052
- if (choice === '1') {
3053
- pkg.bin = { [cmdName]: mainFile };
3054
- writePackageJson(cwd, pkg);
3055
- console.log(colors.green(`✓ Added bin: { "${cmdName}": "${mainFile}" }`));
3056
- return true;
3057
- }
3058
- // choice is '2' or '' (default) — skip
3059
- return false;
3060
- }
3061
3060
  /** Perform local-only install (npm install -g .) — extracted for reuse from git-init prompts */
3062
3061
  async function doLocalInstall(cwd, options) {
3063
3062
  const { dryRun = false, wsl = false } = options;
@@ -3067,12 +3066,8 @@ async function doLocalInstall(cwd, options) {
3067
3066
  console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
3068
3067
  console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
3069
3068
  if (!pkg.bin) {
3070
- const proceed = await offerAddBin(cwd, pkg);
3071
- if (!proceed) {
3072
- console.log(colors.yellow('Skipping global install — library packages should not be installed globally.'));
3073
- console.log(colors.yellow(`To use in projects: npm install ${pkgName}`));
3074
- return true;
3075
- }
3069
+ console.log(colors.dim(` (library skipping global install)`));
3070
+ return true;
3076
3071
  }
3077
3072
  if (dryRun) {
3078
3073
  console.log(' [dry-run] Would run: npm install -g .');
@@ -3175,7 +3170,10 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3175
3170
  }
3176
3171
  }
3177
3172
  console.log('');
3178
- // -local: skip all transform/publish, just install from local directory with file: deps intact
3173
+ // -local: skip all transform/publish, just install from local directory with file: deps intact.
3174
+ // -local is testing-only and must NOT mutate package.json — packages are meant to be installed
3175
+ // as published public packages by end users; the bin field is whatever the published shape is.
3176
+ // Library members (no bin) are silently skipped: nothing for global install to expose.
3179
3177
  if (local) {
3180
3178
  const pkg = readPackageJson(cwd);
3181
3179
  const pkgName = pkg.name || path.basename(cwd);
@@ -3183,12 +3181,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3183
3181
  console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
3184
3182
  console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
3185
3183
  if (!pkg.bin) {
3186
- const proceed = await offerAddBin(cwd, pkg);
3187
- if (!proceed) {
3188
- console.log(colors.yellow('Skipping global install — library packages should not be installed globally.'));
3189
- console.log(colors.yellow(`To use in projects: npm install ${pkgName}`));
3190
- return true;
3191
- }
3184
+ console.log(colors.dim(` (library skipping global install)`));
3185
+ return true;
3192
3186
  }
3193
3187
  if (dryRun) {
3194
3188
  console.log(' [dry-run] Would run: npm install -g .');
@@ -3633,6 +3627,10 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3633
3627
  }
3634
3628
  // Check npm visibility and current publication status
3635
3629
  let currentAccess = checkNpmAccess(pkg.name);
3630
+ // Track whether this name is brand-new on npm so post-publish waits use
3631
+ // longer timeouts — first-time publishes propagate slowly through the
3632
+ // registry/CDN even after `npm publish` reports success.
3633
+ const wasNewPackage = !currentAccess;
3636
3634
  let isScoped = pkg.name.startsWith('@');
3637
3635
  // Check if public intent was explicitly declared anywhere:
3638
3636
  // CLI (--npm public), config file (.globalize.json5), or package.json publishConfig
@@ -4279,11 +4277,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4279
4277
  const pkgName = pkg.name;
4280
4278
  const pkgVersion = pkg.version;
4281
4279
  if (!pkg.bin && (install || link || wsl)) {
4282
- const proceed = await offerAddBin(cwd, pkg);
4283
- if (!proceed) {
4284
- console.log(colors.yellow('Skipping global install — library packages should not be installed globally.'));
4285
- console.log(colors.yellow(`To use in projects: npm install ${pkgName}`));
4286
- }
4280
+ console.log(colors.dim(` ${pkgName}: library skipping global install`));
4287
4281
  }
4288
4282
  if (pkg.bin && (install || link || wsl)) {
4289
4283
  if (link) {
@@ -5079,11 +5073,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
5079
5073
  let globalInstallOk = false;
5080
5074
  let wslInstallOk = false;
5081
5075
  if (!updatedPkg.bin && (install || link || wsl)) {
5082
- const proceed = await offerAddBin(cwd, updatedPkg);
5083
- if (!proceed) {
5084
- console.log(colors.yellow('Skipping global install — library packages should not be installed globally.'));
5085
- console.log(colors.yellow(`To use in projects: npm install ${pkgName}`));
5086
- }
5076
+ console.log(colors.dim(` ${pkgName}: library skipping global install`));
5087
5077
  }
5088
5078
  if (updatedPkg.bin && (install || link || wsl)) {
5089
5079
  if (link) {
@@ -5127,8 +5117,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
5127
5117
  else {
5128
5118
  console.log(`${timestamp()} Installing globally from registry: ${pkgName}@${pkgVersion}...`);
5129
5119
  if (!dryRun) {
5130
- waitForNpmVersion(pkgName, pkgVersion);
5131
- const installResult = installGlobalWithRetry(`${pkgName}@${pkgVersion}`, cwd);
5120
+ waitForNpmVersion(pkgName, pkgVersion, wasNewPackage);
5121
+ const installResult = installGlobalWithRetry(`${pkgName}@${pkgVersion}`, cwd, wasNewPackage);
5132
5122
  if (installResult.success) {
5133
5123
  globalInstallOk = true;
5134
5124
  console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
@@ -5157,7 +5147,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
5157
5147
  const useLocalWsl = link || !!updatedPkg.workspaces;
5158
5148
  const wslArgs = useLocalWsl ? ['npm', 'install', '-g', '.'] : ['npm', 'install', '-g', `${pkgName}@${pkgVersion}`];
5159
5149
  if (!useLocalWsl)
5160
- waitForNpmVersion(pkgName, pkgVersion);
5150
+ waitForNpmVersion(pkgName, pkgVersion, wasNewPackage);
5161
5151
  console.log(`Installing in WSL${useLocalWsl ? ' (local)' : ' from registry'}: ${pkgName}@${pkgVersion}...`);
5162
5152
  if (!dryRun) {
5163
5153
  const wslResult = installInWsl(wslArgs, { cwd });
@@ -5289,6 +5279,26 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
5289
5279
  console.log(`Packages (${packages.length}): ${packages.map(p => p.name).join(', ')}`);
5290
5280
  console.log(`Publish order: ${publishOrder.join(' → ')}`);
5291
5281
  console.log('');
5282
+ // Warn if -local / -install are present at the workspace root: those are
5283
+ // component-level concerns. In workspace mode they only act on members
5284
+ // that have a `bin` field; libraries are silently skipped. Telling the
5285
+ // user up front avoids the surprise of "I asked for install but most
5286
+ // packages didn't install."
5287
+ const installSources = [];
5288
+ if (options.local || configOptions.local) {
5289
+ const src = configOptions.local && !options.local ? '.globalize.json5' : 'CLI';
5290
+ installSources.push(`-local (${src})`);
5291
+ }
5292
+ if (options.install || configOptions.install) {
5293
+ const src = configOptions.install && !options.install ? '.globalize.json5' : 'CLI';
5294
+ installSources.push(`-install (${src})`);
5295
+ }
5296
+ if (installSources.length > 0) {
5297
+ console.log(colors.yellow(`! ${installSources.join(', ')} at workspace root — these are component-level flags.`));
5298
+ console.log(colors.yellow(` Only members with a \`bin\` field will install globally; libraries are skipped.`));
5299
+ console.log(colors.yellow(` For per-component control, run npmglobalize inside the component directory.`));
5300
+ console.log('');
5301
+ }
5292
5302
  const rootPkg = readPackageJson(rootDir);
5293
5303
  // Handle --cleanup: restore deps for all packages and return
5294
5304
  if (options.cleanup) {
@@ -5432,13 +5442,14 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
5432
5442
  }
5433
5443
  }
5434
5444
  }
5445
+ const relPath = path.relative(rootDir, pkgInfo.dir).split(path.sep).join('/') || '.';
5435
5446
  if (reason) {
5436
5447
  flaggedForWork.add(pkgName);
5437
- console.log(` ${colors.yellow('⟳')} ${pkgName} — ${reason}`);
5448
+ console.log(` ${colors.yellow('⟳')} ${pkgName} — ${reason} ${colors.dim(relPath)}`);
5438
5449
  }
5439
5450
  else {
5440
5451
  skipReasons.set(pkgName, 'clean, published, build fresh');
5441
- console.log(` ${colors.green('✓')} ${pkgName} — skip (clean, published, build fresh)`);
5452
+ console.log(` ${colors.green('✓')} ${pkgName} — skip (clean, published, build fresh) ${colors.dim(relPath)}`);
5442
5453
  }
5443
5454
  }
5444
5455
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.167",
3
+ "version": "1.0.168",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",