@bobfrankston/npmglobalize 1.0.12 → 1.0.18

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/lib.js CHANGED
@@ -6,11 +6,13 @@ import path from 'path';
6
6
  import { execSync, spawnSync } from 'child_process';
7
7
  import readline from 'readline';
8
8
  import libversion from 'libnpmversion';
9
+ import JSON5 from 'json5';
9
10
  /** ANSI color codes */
10
11
  const colors = {
11
12
  red: (text) => `\x1b[31m${text}\x1b[0m`,
12
13
  yellow: (text) => `\x1b[33m${text}\x1b[0m`,
13
14
  green: (text) => `\x1b[32m${text}\x1b[0m`,
15
+ italic: (text) => `\x1b[3m${text}\x1b[0m`,
14
16
  };
15
17
  const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
16
18
  /** Read and parse package.json from a directory */
@@ -21,54 +23,100 @@ export function readPackageJson(dir) {
21
23
  }
22
24
  return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
23
25
  }
24
- /** Read .globalize.jsonc config file */
26
+ /** Read .globalize.json5 config file */
25
27
  export function readConfig(dir) {
26
- const configPath = path.join(dir, '.globalize.jsonc');
28
+ const configPath = path.join(dir, '.globalize.json5');
27
29
  if (!fs.existsSync(configPath)) {
28
30
  return {};
29
31
  }
30
32
  try {
31
33
  const content = fs.readFileSync(configPath, 'utf-8');
32
- // Strip comments for JSON parsing (simple implementation)
33
- const jsonContent = content
34
- .split('\n')
35
- .map(line => line.replace(/\/\/.*$/, '').trim())
36
- .filter(line => line.length > 0)
37
- .join('\n');
38
- return JSON.parse(jsonContent);
34
+ return JSON5.parse(content);
39
35
  }
40
36
  catch (error) {
41
- console.warn(`Warning: Could not parse .globalize.jsonc config: ${error.message}`);
37
+ console.warn(`Warning: Could not parse .globalize.json5 config: ${error.message}`);
42
38
  return {};
43
39
  }
44
40
  }
45
- /** Write .globalize.jsonc config file */
46
- export function writeConfig(dir, config) {
47
- const configPath = path.join(dir, '.globalize.jsonc');
41
+ /** Write .globalize.json5 config file */
42
+ export function writeConfig(dir, config, explicitKeys) {
43
+ const configPath = path.join(dir, '.globalize.json5');
44
+ // Define defaults to omit
45
+ const defaults = {
46
+ bump: 'patch',
47
+ files: true,
48
+ quiet: true,
49
+ gitVisibility: 'private',
50
+ npmVisibility: 'public',
51
+ install: false,
52
+ wsl: false,
53
+ force: false,
54
+ verbose: false
55
+ };
56
+ // Read existing config to preserve values
57
+ const existing = readConfig(dir);
58
+ // Filter out temporary flags and default values (unless explicitly set)
59
+ const filtered = {};
60
+ const omitKeys = new Set(['applyOnly', 'cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error']);
61
+ for (const [key, value] of Object.entries(config)) {
62
+ if (omitKeys.has(key))
63
+ continue;
64
+ // Keep if: already in config, explicitly set via CLI, or differs from default
65
+ const isExplicit = explicitKeys?.has(key);
66
+ const wasInConfig = key in existing;
67
+ const differsFromDefault = defaults[key] !== value;
68
+ if (isExplicit || wasInConfig || differsFromDefault) {
69
+ filtered[key] = value;
70
+ }
71
+ }
48
72
  // Build content with comments
49
73
  const lines = [
50
74
  '{',
51
- ' // Only set options that differ from defaults',
52
- ' // Defaults: bump=patch, files=true, quiet=true, gitVisibility=private, npmVisibility=public',
75
+ ' // npmglobalize configuration (JSON5 format - trailing commas OK)',
76
+ ' // Explicitly set values are preserved here',
53
77
  ''
54
78
  ];
55
79
  // Add configured values
56
- for (const [key, value] of Object.entries(config)) {
57
- const jsonValue = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
58
- lines.push(` "${key}": ${jsonValue},`);
59
- }
60
- // Add commented examples for other options
61
- lines.push('');
62
- lines.push(' // Available options:');
63
- lines.push(' // "bump": "patch" | "minor" | "major"');
64
- lines.push(' // "install": true | false // Auto-install globally after publish');
65
- lines.push(' // "wsl": true | false // Also install in WSL');
66
- lines.push(' // "files": true | false // Keep file: paths after publish');
67
- lines.push(' // "force": true | false // Continue despite git errors');
68
- lines.push(' // "quiet": true | false // Suppress npm warnings');
69
- lines.push(' // "verbose": true | false // Show detailed output');
70
- lines.push(' // "gitVisibility": "private" | "public"');
71
- lines.push(' // "npmVisibility": "private" | "public"');
80
+ const entries = Object.entries(filtered);
81
+ if (entries.length > 0) {
82
+ entries.forEach(([key, value]) => {
83
+ const jsonValue = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
84
+ const comma = ','; // JSON5 allows trailing commas
85
+ // Add inline comment for clarity
86
+ let comment = '';
87
+ if (key === 'install')
88
+ comment = ' // Auto-install globally after publish';
89
+ else if (key === 'wsl')
90
+ comment = ' // Also install in WSL';
91
+ else if (key === 'files')
92
+ comment = ' // Keep file: paths after publish';
93
+ else if (key === 'force')
94
+ comment = ' // Continue despite git errors';
95
+ else if (key === 'quiet')
96
+ comment = ' // Suppress npm warnings';
97
+ else if (key === 'verbose')
98
+ comment = ' // Show detailed output';
99
+ else if (key === 'gitVisibility')
100
+ comment = ' // private (default) or public';
101
+ else if (key === 'npmVisibility')
102
+ comment = ' // private or public (default)';
103
+ else if (key === 'bump')
104
+ comment = ' // patch (default), minor, or major';
105
+ lines.push(` "${key}": ${jsonValue}${comma}${comment}`);
106
+ });
107
+ lines.push('');
108
+ }
109
+ // Add commented reference for all options
110
+ lines.push(' // Defaults (omitted above):');
111
+ lines.push(' // "bump": "patch" // Version bump: patch, minor, major');
112
+ lines.push(' // "install": false // Auto-install globally after publish');
113
+ lines.push(' // "wsl": false // Also install in WSL');
114
+ lines.push(' // "files": true // Keep file: paths after publish');
115
+ lines.push(' // "force": false // Continue despite git errors');
116
+ lines.push(' // "quiet": true // Suppress npm warnings');
117
+ lines.push(' // "verbose": false // Show detailed output');
118
+ lines.push(' // "gitVisibility": "private" // Git repo: private or public');
119
+ lines.push(' // "npmVisibility": "public" // npm package: private or public');
72
120
  lines.push('}');
73
121
  fs.writeFileSync(configPath, lines.join('\n') + '\n');
74
122
  }
@@ -165,13 +213,13 @@ export function hasBackup(pkg) {
165
213
  }
166
214
  /** Run a command and return success status */
167
215
  export function runCommand(cmd, args, options = {}) {
168
- const { silent = false, cwd } = options;
216
+ const { silent = false, cwd, shell = false } = options;
169
217
  try {
170
218
  const result = spawnSync(cmd, args, {
171
219
  encoding: 'utf-8',
172
220
  stdio: silent ? 'pipe' : 'inherit',
173
221
  cwd,
174
- shell: true // Use shell for better Windows compatibility and output capture
222
+ shell // Use shell when explicitly requested (e.g., for npm publish on Windows)
175
223
  });
176
224
  // For non-silent commands, we can't capture output when using 'inherit'
177
225
  // So we return empty string for output, but the user sees it in the terminal
@@ -379,9 +427,155 @@ Authentication Options:
379
427
  - Run: ${colors.green('npm login')}
380
428
  - Follow interactive prompts
381
429
 
430
+ ${colors.italic('Note: y:\\dev\\utils\\npmglobalize has set-npm-token.ps1 that may help fix token')}
431
+ ${colors.italic(' problems, but no promises.')}
432
+
382
433
  Note: npm now requires either 2FA or a granular token with bypass enabled.
383
434
  `;
384
435
  }
436
+ /** Recommended .gitignore patterns */
437
+ const RECOMMENDED_GITIGNORE = [
438
+ 'node_modules/',
439
+ '.env*',
440
+ '*cert*/',
441
+ '*.pem',
442
+ '*.key',
443
+ '*.p12',
444
+ '*.pfx',
445
+ 'token',
446
+ 'tokens',
447
+ '*.token',
448
+ '.globalize.json5',
449
+ '*.log',
450
+ '.DS_Store',
451
+ 'Thumbs.db'
452
+ ];
453
+ /** Recommended .npmignore patterns */
454
+ const RECOMMENDED_NPMIGNORE = [
455
+ '.git/',
456
+ '.gitignore',
457
+ '.gitattributes',
458
+ 'certs/',
459
+ '*cert*/',
460
+ '.env*',
461
+ 'token*',
462
+ 'cruft/',
463
+ 'prev/',
464
+ 'tests/',
465
+ '*.md',
466
+ '!README.md',
467
+ '*.log',
468
+ '.DS_Store',
469
+ 'Thumbs.db',
470
+ 'package-lock.json',
471
+ '*.ts',
472
+ '!*.d.ts',
473
+ '*.map',
474
+ 'tsconfig.json',
475
+ '.vscode/',
476
+ '.globalize.json5'
477
+ ];
478
+ /** Check if ignore files need updates */
479
+ function checkIgnoreFiles(cwd, options) {
480
+ const changes = [];
481
+ // Check if asis is set in config or passed as option
482
+ if (options.asis) {
483
+ if (options.verbose) {
484
+ console.log(colors.yellow(' Skipping ignore file checks (--asis or asis in config)'));
485
+ }
486
+ return { needsUpdate: false, changes: [] };
487
+ }
488
+ // Check .gitignore
489
+ const gitignorePath = path.join(cwd, '.gitignore');
490
+ if (fs.existsSync(gitignorePath)) {
491
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
492
+ const lines = content.split('\n').map(l => l.trim());
493
+ for (const pattern of RECOMMENDED_GITIGNORE) {
494
+ if (!lines.some(line => line === pattern || line === pattern.replace('/', ''))) {
495
+ changes.push(` .gitignore missing: ${pattern}`);
496
+ }
497
+ }
498
+ }
499
+ // Check .npmignore
500
+ const npmignorePath = path.join(cwd, '.npmignore');
501
+ if (fs.existsSync(npmignorePath)) {
502
+ const content = fs.readFileSync(npmignorePath, 'utf-8');
503
+ const lines = content.split('\n').map(l => l.trim());
504
+ // Check for missing TypeScript exclusions
505
+ const hasMapExclusion = lines.some(line => line === '*.map');
506
+ const hasTsExclusion = lines.some(line => line === '*.ts');
507
+ const hasDtsInclude = lines.some(line => line === '!*.d.ts');
508
+ if (!hasMapExclusion) {
509
+ changes.push(' .npmignore missing: *.map');
510
+ }
511
+ if (!hasTsExclusion) {
512
+ changes.push(' .npmignore missing: *.ts');
513
+ }
514
+ if (!hasDtsInclude && hasTsExclusion) {
515
+ changes.push(' .npmignore missing: !*.d.ts (to keep type definitions)');
516
+ }
517
+ // Check for token files
518
+ if (!lines.some(line => line.includes('token'))) {
519
+ changes.push(' .npmignore missing: token* (security)');
520
+ }
521
+ }
522
+ return { needsUpdate: changes.length > 0, changes };
523
+ }
524
+ /** Update ignore files to conform to best practices */
525
+ function conformIgnoreFiles(cwd) {
526
+ // Update .gitignore
527
+ const gitignorePath = path.join(cwd, '.gitignore');
528
+ if (fs.existsSync(gitignorePath)) {
529
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
530
+ const lines = new Set(content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')));
531
+ let updated = false;
532
+ for (const pattern of RECOMMENDED_GITIGNORE) {
533
+ const normalized = pattern.replace('/', '');
534
+ if (!lines.has(pattern) && !lines.has(normalized)) {
535
+ lines.add(pattern);
536
+ updated = true;
537
+ }
538
+ }
539
+ if (updated) {
540
+ const newContent = Array.from(lines).sort().join('\n') + '\n';
541
+ fs.writeFileSync(gitignorePath, newContent);
542
+ console.log(colors.green(' ✓ Updated .gitignore'));
543
+ }
544
+ }
545
+ // Update .npmignore
546
+ const npmignorePath = path.join(cwd, '.npmignore');
547
+ if (fs.existsSync(npmignorePath)) {
548
+ const content = fs.readFileSync(npmignorePath, 'utf-8');
549
+ const lines = content.split('\n').map(l => l.trim());
550
+ const newLines = new Set(lines.filter(l => l));
551
+ let updated = false;
552
+ // Add TypeScript exclusions
553
+ if (!newLines.has('*.ts')) {
554
+ newLines.add('*.ts');
555
+ updated = true;
556
+ }
557
+ if (!newLines.has('!*.d.ts')) {
558
+ newLines.add('!*.d.ts');
559
+ updated = true;
560
+ }
561
+ if (!newLines.has('*.map')) {
562
+ newLines.add('*.map');
563
+ updated = true;
564
+ }
565
+ // Add other missing patterns
566
+ for (const pattern of RECOMMENDED_NPMIGNORE) {
567
+ if (!newLines.has(pattern)) {
568
+ newLines.add(pattern);
569
+ updated = true;
570
+ }
571
+ }
572
+ if (updated) {
573
+ const newContent = Array.from(newLines).join('\n') + '\n';
574
+ fs.writeFileSync(npmignorePath, newContent);
575
+ console.log(colors.green(' ✓ Updated .npmignore'));
576
+ }
577
+ }
578
+ }
385
579
  /** Ensure .gitignore exists and includes node_modules */
386
580
  function ensureGitignore(cwd) {
387
581
  const gitignorePath = path.join(cwd, '.gitignore');
@@ -487,7 +681,37 @@ export async function initGit(cwd, visibility, dryRun) {
487
681
  }
488
682
  /** Main globalize function */
489
683
  export async function globalize(cwd, options = {}) {
490
- const { bump = 'patch', applyOnly = false, cleanup = false, install = false, wsl = false, force = false, files = true, dryRun = false, quiet = true, verbose = false, init = false, gitVisibility = 'private', npmVisibility = 'public', message } = options;
684
+ const { bump = 'patch', applyOnly = false, cleanup = false, install = false, wsl = false, force = false, files = true, dryRun = false, quiet = true, verbose = false, init = false, gitVisibility = 'private', npmVisibility = 'public', message, conform = false, asis = false } = options;
685
+ // Check ignore files first (unless cleanup mode)
686
+ if (!cleanup && !asis) {
687
+ const checkResult = checkIgnoreFiles(cwd, { conform, asis, verbose });
688
+ if (checkResult.needsUpdate) {
689
+ console.log(colors.yellow('\nIgnore file recommendations:'));
690
+ for (const change of checkResult.changes) {
691
+ console.log(colors.yellow(change));
692
+ }
693
+ console.log('');
694
+ if (conform) {
695
+ console.log('Updating ignore files (--conform)...');
696
+ conformIgnoreFiles(cwd);
697
+ console.log('');
698
+ }
699
+ else {
700
+ console.log(colors.yellow('Run with --conform to apply these changes'));
701
+ console.log(colors.yellow('Or set "asis": true in .globalize.json5 to suppress checks'));
702
+ console.log(colors.yellow('Or use --asis flag to skip this check'));
703
+ console.log('');
704
+ const shouldContinue = await confirm('Continue anyway?', true);
705
+ if (!shouldContinue) {
706
+ return false;
707
+ }
708
+ }
709
+ }
710
+ else if (verbose) {
711
+ console.log(colors.green('✓ Ignore files look good'));
712
+ console.log('');
713
+ }
714
+ }
491
715
  // Handle cleanup mode
492
716
  if (cleanup) {
493
717
  console.log('Restoring dependencies from backup...');
@@ -659,6 +883,60 @@ export async function globalize(cwd, options = {}) {
659
883
  if (!currentGitStatus.hasUncommitted && !message) {
660
884
  console.log('');
661
885
  console.log('No changes to commit and no custom message specified.');
886
+ // If install flag is set, verify/install even without publishing
887
+ if (install || wsl) {
888
+ console.log('');
889
+ console.log('Checking global installation...');
890
+ const pkgName = pkg.name;
891
+ if (install) {
892
+ const verifyResult = runCommand('npm', ['list', '-g', '--depth=0', pkgName], { cwd, silent: true });
893
+ if (verifyResult.success) {
894
+ console.log(colors.green(`✓ Already installed globally: ${pkgName}`));
895
+ }
896
+ else {
897
+ console.log(colors.yellow(`Package not installed globally. Installing latest version...`));
898
+ const installResult = runCommand('npm', ['install', '-g', `${pkgName}@latest`], { cwd, silent: false });
899
+ if (installResult.success) {
900
+ const reVerify = runCommand('npm', ['list', '-g', '--depth=0', pkgName], { cwd, silent: true });
901
+ if (reVerify.success) {
902
+ const version = pkg.version;
903
+ console.log(colors.green(`✓ Installed and verified globally: ${pkgName}@${version}`));
904
+ }
905
+ else {
906
+ console.log(colors.yellow(`⚠ Install appeared successful but verification failed`));
907
+ }
908
+ }
909
+ else {
910
+ console.error(colors.red(`✗ Global install failed`));
911
+ }
912
+ }
913
+ }
914
+ if (wsl) {
915
+ const verifyResult = runCommand('wsl', ['npm', 'list', '-g', '--depth=0', pkgName], { cwd, silent: true });
916
+ if (verifyResult.success) {
917
+ console.log(colors.green(`✓ Already installed in WSL: ${pkgName}`));
918
+ }
919
+ else {
920
+ console.log(colors.yellow(`Package not installed in WSL. Installing latest version...`));
921
+ const wslInstallResult = runCommand('wsl', ['npm', 'install', '-g', `${pkgName}@latest`], { cwd, silent: false });
922
+ if (wslInstallResult.success) {
923
+ const reVerify = runCommand('wsl', ['npm', 'list', '-g', '--depth=0', pkgName], { cwd, silent: true });
924
+ if (reVerify.success) {
925
+ const version = pkg.version;
926
+ console.log(colors.green(`✓ Installed and verified in WSL: ${pkgName}@${version}`));
927
+ }
928
+ else {
929
+ console.log(colors.yellow(`⚠ WSL install appeared successful but verification failed`));
930
+ }
931
+ }
932
+ else {
933
+ console.error(colors.yellow('✗ WSL install failed (is npm installed in WSL?)'));
934
+ }
935
+ }
936
+ }
937
+ console.log('');
938
+ return true;
939
+ }
662
940
  console.log('Nothing to release. Use -m "message" to force a release.');
663
941
  return true;
664
942
  }
@@ -671,8 +949,22 @@ export async function globalize(cwd, options = {}) {
671
949
  const commitMsg = message || 'Pre-release commit';
672
950
  console.log(`Committing changes: ${commitMsg}`);
673
951
  if (!dryRun) {
674
- runCommand('git', ['add', '-A'], { cwd });
675
- runCommand('git', ['commit', '-m', commitMsg], { cwd });
952
+ const addResult = runCommand('git', ['add', '-A'], { cwd });
953
+ if (!addResult.success) {
954
+ console.error(colors.red('ERROR: Failed to add files to git:'), addResult.stderr);
955
+ if (!force) {
956
+ return false;
957
+ }
958
+ console.log(colors.yellow('Continuing with --force...'));
959
+ }
960
+ const commitResult = runCommand('git', ['commit', '-m', commitMsg], { cwd });
961
+ if (!commitResult.success) {
962
+ console.error(colors.red('ERROR: Failed to commit changes:'), commitResult.stderr);
963
+ if (!force) {
964
+ return false;
965
+ }
966
+ console.log(colors.yellow('Continuing with --force...'));
967
+ }
676
968
  }
677
969
  else {
678
970
  console.log(' [dry-run] Would commit changes');
@@ -701,69 +993,85 @@ export async function globalize(cwd, options = {}) {
701
993
  }
702
994
  // Publish
703
995
  console.log('Publishing to npm...');
704
- const npmArgs = ['publish'];
705
- if (quiet) {
706
- npmArgs.push('--quiet');
707
- }
708
996
  if (!dryRun) {
709
- // Run with silent:true to capture the error message
710
- const publishResult = runCommand('npm', npmArgs, { cwd, silent: true });
711
- if (!publishResult.success) {
712
- console.error(colors.red('\nERROR: npm publish failed\n'));
713
- // Combine stdout and stderr for error analysis
714
- const errorOutput = (publishResult.output + '\n' + publishResult.stderr).toLowerCase();
715
- // Check for specific error types
716
- if (errorOutput.includes('e403') && (errorOutput.includes('two-factor') || errorOutput.includes('2fa'))) {
717
- console.error(colors.red('⚠ 2FA Required'));
718
- console.error('');
719
- console.error('Your npm account requires two-factor authentication or a');
720
- console.error('granular access token with "bypass 2FA" enabled.');
721
- console.error('');
722
- console.error(colors.yellow('Your options:'));
723
- console.error('');
724
- console.error('1. Enable 2FA on your npm account:');
725
- console.error(colors.green(' https://www.npmjs.com/settings/[username]/tfa'));
726
- console.error('');
727
- console.error('2. Create a granular access token:');
728
- console.error(colors.green(' https://www.npmjs.com/settings/[username]/tokens'));
729
- console.error(' - Select "Granular Access Token"');
730
- console.error(' - Set permissions: Read and write');
731
- console.error(' - Enable "Bypass 2FA requirement"');
732
- console.error(' - Then set via environment variable or .npmrc:');
733
- console.error(colors.green(' $env:NPM_TOKEN="npm_xxx..."'));
734
- console.error(' Or add to ~/.npmrc:');
735
- console.error(colors.green(' //registry.npmjs.org/:_authToken=npm_xxx...'));
736
- console.error('');
997
+ // Create tarball first
998
+ const packResult = runCommand('npm', ['pack'], { cwd, silent: true, shell: true });
999
+ if (!packResult.success) {
1000
+ console.error(colors.red('ERROR: Failed to create package tarball'));
1001
+ if (packResult.stderr)
1002
+ console.error(packResult.stderr);
1003
+ return false;
1004
+ }
1005
+ // Get the tarball filename from npm pack output
1006
+ const tarballName = packResult.output.trim().split('\n').pop()?.trim();
1007
+ if (!tarballName) {
1008
+ console.error(colors.red('ERROR: Could not determine tarball filename'));
1009
+ return false;
1010
+ }
1011
+ const tarballPath = path.join(cwd, tarballName);
1012
+ if (verbose) {
1013
+ console.log(`Created tarball: ${tarballName}`);
1014
+ }
1015
+ // Publish the tarball
1016
+ const npmArgs = ['publish', tarballName];
1017
+ if (npmVisibility === 'public') {
1018
+ npmArgs.push('--access', 'public');
1019
+ }
1020
+ if (quiet) {
1021
+ npmArgs.push('--quiet');
1022
+ }
1023
+ // Retry logic for E409 conflicts (publishing too fast)
1024
+ const maxRetries = 12; // 12 attempts * 5 seconds = 60 seconds total
1025
+ const retryDelay = 5; // 5 seconds between attempts
1026
+ let publishResult;
1027
+ let attempt = 0;
1028
+ while (attempt < maxRetries) {
1029
+ if (attempt > 0) {
1030
+ console.log(colors.yellow(`⏱ Waiting ${retryDelay} seconds before retry (attempt ${attempt + 1}/${maxRetries})...`));
1031
+ await new Promise(resolve => setTimeout(resolve, retryDelay * 1000));
737
1032
  }
738
- else if (errorOutput.includes('e404') || errorOutput.includes('not found')) {
739
- console.error(colors.yellow('Package not found or access denied.'));
740
- console.error('');
741
- console.error('Possible causes:');
742
- console.error(' • Package name is not available');
743
- console.error(' • You don\'t have permission to publish to this package');
744
- console.error(' Scope (@username/) requires organization access');
745
- console.error('');
1033
+ publishResult = runCommand('npm', npmArgs, { cwd, silent: true, shell: true });
1034
+ if (publishResult.success) {
1035
+ break; // Success!
1036
+ }
1037
+ // Check if it's an E409 error
1038
+ const output = (publishResult.output + '\n' + publishResult.stderr).toLowerCase();
1039
+ const isE409 = output.includes('e409') || output.includes('409 conflict');
1040
+ if (!isE409) {
1041
+ break; // Not an E409, don't retry
1042
+ }
1043
+ if (attempt === 0) {
1044
+ console.log(colors.yellow('⚠ npm is still processing the previous version, retrying...'));
746
1045
  }
747
- else if (errorOutput.includes('e402') || errorOutput.includes('payment required')) {
748
- console.error(colors.yellow('Payment required for private packages.'));
1046
+ attempt++;
1047
+ }
1048
+ // Clean up tarball
1049
+ try {
1050
+ fs.unlinkSync(tarballPath);
1051
+ if (verbose) {
1052
+ console.log(`Cleaned up tarball: ${tarballName}`);
1053
+ }
1054
+ }
1055
+ catch (e) {
1056
+ // Ignore cleanup errors
1057
+ }
1058
+ if (!publishResult.success) {
1059
+ console.error(colors.red('\nERROR: npm publish failed\n'));
1060
+ // Check for E409 conflict (publishing too fast)
1061
+ const output = (publishResult.output + '\n' + publishResult.stderr).toLowerCase();
1062
+ if (output.includes('e409') || output.includes('409 conflict')) {
1063
+ console.error(colors.yellow('⚠ Publishing too quickly'));
749
1064
  console.error('');
750
- console.error('Private scoped packages require a paid npm account.');
751
- console.error('Either upgrade your account or make the package public.');
1065
+ console.error('npm is still processing the previous version after multiple retries.');
1066
+ console.error(colors.green('Solution: Wait a few more minutes and try again'));
752
1067
  console.error('');
753
1068
  }
754
1069
  else {
755
- // Show the actual error output
756
- if (publishResult.stderr) {
757
- console.error(publishResult.stderr);
758
- }
759
- if (publishResult.output) {
760
- console.error(publishResult.output);
761
- }
762
- console.error('');
763
1070
  console.error(colors.yellow('Common causes:'));
764
1071
  console.error(colors.yellow(' 1. Not logged in - run: npm login'));
765
1072
  console.error(colors.yellow(' 2. Version already published'));
766
1073
  console.error(colors.yellow(' 3. Authentication token expired'));
1074
+ console.error('');
767
1075
  }
768
1076
  if (transformed) {
769
1077
  console.log('Run --cleanup to restore file: dependencies');
@@ -788,21 +1096,46 @@ export async function globalize(cwd, options = {}) {
788
1096
  }
789
1097
  // Global install
790
1098
  const pkgName = pkg.name;
1099
+ const pkgVersion = pkg.version;
791
1100
  if (install) {
792
- console.log(`Installing globally: ${pkgName}...`);
1101
+ console.log(`Installing globally: ${pkgName}@${pkgVersion}...`);
793
1102
  if (!dryRun) {
794
- runCommand('npm', ['install', '-g', pkgName], { cwd });
1103
+ const installResult = runCommand('npm', ['install', '-g', `${pkgName}@latest`], { cwd, silent: false });
1104
+ if (installResult.success) {
1105
+ // Verify installation by checking if command exists
1106
+ const verifyResult = runCommand('npm', ['list', '-g', '--depth=0', pkgName], { cwd, silent: true });
1107
+ if (verifyResult.success) {
1108
+ console.log(colors.green(`✓ Installed and verified globally: ${pkgName}@${pkgVersion}`));
1109
+ }
1110
+ else {
1111
+ console.log(colors.yellow(`⚠ Install appeared successful but verification failed`));
1112
+ }
1113
+ }
1114
+ else {
1115
+ console.error(colors.red(`✗ Global install failed`));
1116
+ console.error(colors.yellow(' Try running manually: npm install -g ' + pkgName));
1117
+ }
795
1118
  }
796
1119
  else {
797
1120
  console.log(` [dry-run] Would run: npm install -g ${pkgName}`);
798
1121
  }
799
1122
  }
800
1123
  if (wsl) {
801
- console.log(`Installing in WSL: ${pkgName}...`);
1124
+ console.log(`Installing in WSL: ${pkgName}@${pkgVersion}...`);
802
1125
  if (!dryRun) {
803
- const wslResult = runCommand('wsl', ['npm', 'install', '-g', pkgName], { cwd });
804
- if (!wslResult.success) {
805
- console.log(' Warning: WSL install failed (is npm installed in WSL?)');
1126
+ const wslResult = runCommand('wsl', ['npm', 'install', '-g', `${pkgName}@latest`], { cwd, silent: false });
1127
+ if (wslResult.success) {
1128
+ // Verify WSL installation
1129
+ const verifyResult = runCommand('wsl', ['npm', 'list', '-g', '--depth=0', pkgName], { cwd, silent: true });
1130
+ if (verifyResult.success) {
1131
+ console.log(colors.green(`✓ Installed and verified in WSL: ${pkgName}@${pkgVersion}`));
1132
+ }
1133
+ else {
1134
+ console.log(colors.yellow(`⚠ WSL install appeared successful but verification failed`));
1135
+ }
1136
+ }
1137
+ else {
1138
+ console.error(colors.yellow(' ✗ WSL install failed (is npm installed in WSL?)'));
806
1139
  }
807
1140
  }
808
1141
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.12",
3
+ "version": "1.0.18",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -27,6 +27,7 @@
27
27
  "url": "https://github.com/BobFrankston/npmglobalize.git"
28
28
  },
29
29
  "dependencies": {
30
+ "json5": "^2.2.3",
30
31
  "libnpmversion": "^8.0.3"
31
32
  }
32
33
  }
package/.gitattributes DELETED
@@ -1,10 +0,0 @@
1
- # Force LF line endings for all text files
2
- * text=auto eol=lf
3
-
4
- # Ensure these are always LF
5
- *.ts text eol=lf
6
- *.js text eol=lf
7
- *.json text eol=lf
8
- *.md text eol=lf
9
- *.yml text eol=lf
10
- *.yaml text eol=lf