@bobfrankston/npmglobalize 1.0.113 → 1.0.115
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/README.md +16 -11
- package/lib.js +308 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,6 +165,10 @@ npmglobalize --rebase # Auto-rebase if behind remote
|
|
|
165
165
|
|
|
166
166
|
For single-developer projects, this safely pulls remote changes before publishing.
|
|
167
167
|
|
|
168
|
+
**Failed publish recovery**: If a previous run bumped the version locally but the publish or push failed (e.g., network error, push protection), `npmglobalize` detects this automatically on the next run. It checks whether the current version actually exists on npm — if not, it republishes without re-bumping the version. Unpushed commits from a failed push are also pushed automatically.
|
|
169
|
+
|
|
170
|
+
**GitHub Push Protection (GH013)**: When GitHub's secret scanning blocks a push, `npmglobalize` detects the error and checks whether the flagged secrets are actually safe. For Google OAuth desktop/installed app credentials (where the "client_secret" is just a public app identifier, not a real secret), it automatically bypasses push protection via the GitHub API (`gh` CLI required) and retries the push. For other secret types, it displays the unblock URLs with guidance.
|
|
171
|
+
|
|
168
172
|
**Note:** This tool is designed for single-developer, single-branch workflows where automatic rebase and tag cleanup are safe operations.
|
|
169
173
|
|
|
170
174
|
### 📂 Git Repository Setup
|
|
@@ -390,17 +394,18 @@ npmglobalize --dry-run # See what would happen
|
|
|
390
394
|
## How It Works
|
|
391
395
|
|
|
392
396
|
1. **Validates** package.json and git status
|
|
393
|
-
2. **
|
|
394
|
-
3. **
|
|
395
|
-
4. **
|
|
396
|
-
5. **
|
|
397
|
-
6. **
|
|
398
|
-
7. **
|
|
399
|
-
8. **
|
|
400
|
-
9. **
|
|
401
|
-
10. **
|
|
402
|
-
11. **
|
|
403
|
-
12. **
|
|
397
|
+
2. **Checks** if current version is on npm (recovers from failed publishes)
|
|
398
|
+
3. **Updates dependencies** (if `--update-deps`)
|
|
399
|
+
4. **Publishes file: dependencies** (if needed)
|
|
400
|
+
5. **Backs up** original file: references to `.dependencies`
|
|
401
|
+
6. **Converts** `file:` → npm version references
|
|
402
|
+
7. **Commits** changes
|
|
403
|
+
8. **Bumps** version (using npm version) — skipped if recovering a failed publish
|
|
404
|
+
9. **Publishes** to npm
|
|
405
|
+
10. **Pushes** to git (with push-protection detection and auto-bypass)
|
|
406
|
+
11. **Installs** globally (if `--install`)
|
|
407
|
+
12. **Restores** file: references (if `--files`, default)
|
|
408
|
+
13. **Runs audit** (shows security status)
|
|
404
409
|
|
|
405
410
|
## Operational Details
|
|
406
411
|
|
package/lib.js
CHANGED
|
@@ -1406,14 +1406,13 @@ function getSecurityNpmignorePatterns() {
|
|
|
1406
1406
|
function lineHasPattern(lines, pattern) {
|
|
1407
1407
|
return lines.some(line => line === pattern || line === pattern.replace('/', ''));
|
|
1408
1408
|
}
|
|
1409
|
-
/** Detect OAuth credentials
|
|
1410
|
-
* Returns
|
|
1411
|
-
function
|
|
1412
|
-
|
|
1413
|
-
if (!fs.existsSync(credPath))
|
|
1409
|
+
/** Detect OAuth credentials type from a specific JSON file path.
|
|
1410
|
+
* Returns 'installed' (public app ID, safe) or 'web' (real secret) or null. */
|
|
1411
|
+
function detectCredentialsTypeFromFile(filePath) {
|
|
1412
|
+
if (!fs.existsSync(filePath))
|
|
1414
1413
|
return null;
|
|
1415
1414
|
try {
|
|
1416
|
-
const content = fs.readFileSync(
|
|
1415
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1417
1416
|
const parsed = JSON.parse(content);
|
|
1418
1417
|
if (parsed.installed)
|
|
1419
1418
|
return 'installed';
|
|
@@ -1425,6 +1424,156 @@ function detectCredentialsType(cwd) {
|
|
|
1425
1424
|
return null;
|
|
1426
1425
|
}
|
|
1427
1426
|
}
|
|
1427
|
+
/** Detect OAuth credentials.json type: "installed" (public app ID, safe to include) or "web" (has real secret, must ignore).
|
|
1428
|
+
* Returns null if no credentials.json exists or it can't be parsed. */
|
|
1429
|
+
function detectCredentialsType(cwd) {
|
|
1430
|
+
return detectCredentialsTypeFromFile(path.join(cwd, 'credentials.json'));
|
|
1431
|
+
}
|
|
1432
|
+
/** Parse GitHub push protection (GH013) error output and extract secret details + unblock URLs. */
|
|
1433
|
+
function parsePushProtection(errorOutput, cwd) {
|
|
1434
|
+
const result = { detected: false, secrets: [], allInstalledOAuth: false };
|
|
1435
|
+
if (!errorOutput)
|
|
1436
|
+
return result;
|
|
1437
|
+
// Strip 'remote: ' prefixes for easier parsing
|
|
1438
|
+
const cleaned = errorOutput.replace(/^remote:\s*/gm, '');
|
|
1439
|
+
if (!cleaned.includes('GH013') && !cleaned.toLowerCase().includes('push protection')) {
|
|
1440
|
+
return result;
|
|
1441
|
+
}
|
|
1442
|
+
result.detected = true;
|
|
1443
|
+
// Split on secret-type headers: "—— Secret Type ————..."
|
|
1444
|
+
const blocks = cleaned.split(/\u2014\u2014\s+/).slice(1);
|
|
1445
|
+
for (const block of blocks) {
|
|
1446
|
+
const typeMatch = block.match(/^(.+?)[\s\u2014]+/);
|
|
1447
|
+
const type = typeMatch ? typeMatch[1].trim() : '';
|
|
1448
|
+
if (!type)
|
|
1449
|
+
continue;
|
|
1450
|
+
const pathMatch = block.match(/path:\s+(\S+?)(?::(\d+))?\s/);
|
|
1451
|
+
const file = pathMatch ? pathMatch[1].trim() : '';
|
|
1452
|
+
const urlMatch = block.match(/(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/);
|
|
1453
|
+
const unblockUrl = urlMatch ? urlMatch[1].trim() : '';
|
|
1454
|
+
if (type && (file || unblockUrl)) {
|
|
1455
|
+
result.secrets.push({ type, file, unblockUrl });
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
// Check if all detected secrets are from Google OAuth installed apps (safe to include)
|
|
1459
|
+
if (result.secrets.length > 0) {
|
|
1460
|
+
result.allInstalledOAuth = result.secrets.every(s => {
|
|
1461
|
+
if (!s.type.toLowerCase().includes('oauth'))
|
|
1462
|
+
return false;
|
|
1463
|
+
if (!s.file)
|
|
1464
|
+
return false;
|
|
1465
|
+
const credType = detectCredentialsTypeFromFile(path.join(cwd, s.file));
|
|
1466
|
+
return credType === 'installed';
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return result;
|
|
1470
|
+
}
|
|
1471
|
+
/** Try to auto-bypass push protection for installed OAuth secrets via gh API.
|
|
1472
|
+
* Returns true if all bypasses succeeded and push should be retried. */
|
|
1473
|
+
function tryAutoBypassPushProtection(ppInfo, cwd) {
|
|
1474
|
+
if (!ppInfo.allInstalledOAuth || ppInfo.secrets.length === 0)
|
|
1475
|
+
return false;
|
|
1476
|
+
// Check if gh CLI is available
|
|
1477
|
+
const ghCheck = runCommand('gh', ['auth', 'status'], { cwd, silent: true });
|
|
1478
|
+
if (!ghCheck.success)
|
|
1479
|
+
return false;
|
|
1480
|
+
// Extract repo owner/name from unblock URLs or git remote
|
|
1481
|
+
let repo = '';
|
|
1482
|
+
for (const s of ppInfo.secrets) {
|
|
1483
|
+
if (s.unblockUrl) {
|
|
1484
|
+
const repoMatch = s.unblockUrl.match(/github\.com\/([^/]+\/[^/]+)\//);
|
|
1485
|
+
if (repoMatch) {
|
|
1486
|
+
repo = repoMatch[1];
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (!repo)
|
|
1492
|
+
return false;
|
|
1493
|
+
let allBypassed = true;
|
|
1494
|
+
for (const s of ppInfo.secrets) {
|
|
1495
|
+
if (!s.unblockUrl) {
|
|
1496
|
+
allBypassed = false;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
// Extract placeholder_id from unblock URL (last path segment)
|
|
1500
|
+
const idMatch = s.unblockUrl.match(/\/unblock-secret\/(\S+)/);
|
|
1501
|
+
if (!idMatch) {
|
|
1502
|
+
allBypassed = false;
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const placeholderId = idMatch[1];
|
|
1506
|
+
console.log(colors.cyan(` Bypassing push protection for: ${s.type} (false_positive — installed app ID)...`));
|
|
1507
|
+
const bypassResult = runCommand('gh', [
|
|
1508
|
+
'api', `repos/${repo}/secret-scanning/push-protection-bypasses`,
|
|
1509
|
+
'-X', 'POST',
|
|
1510
|
+
'-f', 'reason=false_positive',
|
|
1511
|
+
'-f', `placeholder_id=${placeholderId}`
|
|
1512
|
+
], { cwd, silent: true });
|
|
1513
|
+
if (!bypassResult.success) {
|
|
1514
|
+
allBypassed = false;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return allBypassed;
|
|
1518
|
+
}
|
|
1519
|
+
/** Display push protection guidance based on the type of secrets detected. */
|
|
1520
|
+
function showPushProtectionGuidance(ppInfo) {
|
|
1521
|
+
console.error('');
|
|
1522
|
+
console.error(colors.yellow('GitHub Push Protection blocked the push — secrets detected in commits.'));
|
|
1523
|
+
if (ppInfo.allInstalledOAuth) {
|
|
1524
|
+
console.error(colors.cyan('These are Google OAuth credentials for a desktop/installed app.'));
|
|
1525
|
+
console.error(colors.cyan('The "client_secret" is just a public app identifier, not a real secret.'));
|
|
1526
|
+
console.error(colors.cyan('(Google docs: "the client_secret is obviously not treated as a secret")'));
|
|
1527
|
+
console.error('');
|
|
1528
|
+
console.error('To unblock, visit each URL below and allow the secret:');
|
|
1529
|
+
}
|
|
1530
|
+
else {
|
|
1531
|
+
console.error('');
|
|
1532
|
+
console.error('To unblock, review each secret and visit the URL to allow or remove it:');
|
|
1533
|
+
}
|
|
1534
|
+
for (const s of ppInfo.secrets) {
|
|
1535
|
+
console.error(` ${s.type}: ${s.file}`);
|
|
1536
|
+
if (s.unblockUrl) {
|
|
1537
|
+
console.error(` ${colors.cyan(s.unblockUrl)}`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
console.error('');
|
|
1541
|
+
console.error('After unblocking, re-run the push or re-run npmglobalize.');
|
|
1542
|
+
}
|
|
1543
|
+
/** Push to git with push-protection detection and auto-bypass for installed OAuth.
|
|
1544
|
+
* Returns true if push succeeded (possibly after auto-bypass). */
|
|
1545
|
+
function pushWithProtection(cwd, verbose) {
|
|
1546
|
+
const pushResult = runCommand('git', ['push'], { cwd, silent: true });
|
|
1547
|
+
if (pushResult.success) {
|
|
1548
|
+
if (verbose)
|
|
1549
|
+
console.log(colors.green(' ✓ Pushed to remote'));
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
const ppInfo = parsePushProtection(pushResult.stderr, cwd);
|
|
1553
|
+
if (!ppInfo.detected) {
|
|
1554
|
+
console.error(colors.red('Git push failed:'));
|
|
1555
|
+
if (pushResult.stderr)
|
|
1556
|
+
console.error(pushResult.stderr);
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
// Try auto-bypass for installed OAuth credentials
|
|
1560
|
+
if (ppInfo.allInstalledOAuth && tryAutoBypassPushProtection(ppInfo, cwd)) {
|
|
1561
|
+
console.log(colors.green(' ✓ Auto-bypassed push protection (installed OAuth — not a real secret)'));
|
|
1562
|
+
const retryPush = runCommand('git', ['push'], { cwd, silent: true });
|
|
1563
|
+
if (retryPush.success) {
|
|
1564
|
+
if (verbose)
|
|
1565
|
+
console.log(colors.green(' ✓ Pushed to remote'));
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
console.error(colors.yellow('Push still failed after bypass:'));
|
|
1569
|
+
if (retryPush.stderr)
|
|
1570
|
+
console.error(retryPush.stderr);
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
// Can't auto-bypass — show manual guidance
|
|
1574
|
+
showPushProtectionGuidance(ppInfo);
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1428
1577
|
/** Ensure credentials.json is handled correctly in ignore files based on OAuth type.
|
|
1429
1578
|
* "installed" apps: client_secret is just a public app registration ID — must be INCLUDED.
|
|
1430
1579
|
* "web" apps: client_secret is a real secret — must be IGNORED. */
|
|
@@ -2824,7 +2973,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2824
2973
|
runCommand('git', ['add', 'package.json'], { cwd });
|
|
2825
2974
|
runCommand('git', ['commit', '-m', 'Restore file: dependencies'], { cwd });
|
|
2826
2975
|
if (currentGitStatus.hasRemote) {
|
|
2827
|
-
|
|
2976
|
+
pushWithProtection(cwd, verbose);
|
|
2828
2977
|
}
|
|
2829
2978
|
}
|
|
2830
2979
|
}
|
|
@@ -2852,7 +3001,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2852
3001
|
runCommand('git', ['add', 'package.json'], { cwd });
|
|
2853
3002
|
runCommand('git', ['commit', '-m', 'Restore file: dependencies'], { cwd });
|
|
2854
3003
|
if (currentGitStatus.hasRemote) {
|
|
2855
|
-
|
|
3004
|
+
pushWithProtection(cwd, verbose);
|
|
2856
3005
|
}
|
|
2857
3006
|
}
|
|
2858
3007
|
}
|
|
@@ -2863,103 +3012,127 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
2863
3012
|
// Check if there are changes to commit or a custom message
|
|
2864
3013
|
// Skip this check for first publish (currentAccess null) or just-initialized repos
|
|
2865
3014
|
const isFirstPublish = !currentAccess;
|
|
3015
|
+
let skipVersionBump = false;
|
|
2866
3016
|
if (!currentGitStatus.hasUncommitted && !message && !justInitialized && !isFirstPublish) {
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3017
|
+
// Check if the current version is actually on npm (it might have failed to publish previously)
|
|
3018
|
+
const versionCheck = spawnSafe('npm', ['view', `${pkg.name}@${pkg.version}`, 'version'], {
|
|
3019
|
+
shell: process.platform === 'win32',
|
|
3020
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3021
|
+
encoding: 'utf-8'
|
|
3022
|
+
});
|
|
3023
|
+
const versionIsOnNpm = versionCheck.status === 0 && versionCheck.stdout?.trim() === pkg.version;
|
|
3024
|
+
if (!versionIsOnNpm && currentAccess) {
|
|
3025
|
+
// Version not on npm — previous publish/push likely failed
|
|
3026
|
+
// Skip version bump but fall through to publish+push
|
|
3027
|
+
console.log('');
|
|
3028
|
+
console.log(colors.yellow(`${pkg.name}@${pkg.version} is NOT on npm (previous publish may have failed).`));
|
|
3029
|
+
console.log(colors.yellow('Republishing current version...'));
|
|
3030
|
+
skipVersionBump = true;
|
|
2873
3031
|
}
|
|
2874
3032
|
else {
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
if (
|
|
2881
|
-
console.log(
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
if (
|
|
2894
|
-
if (
|
|
2895
|
-
console.log(
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
3033
|
+
// Version is on npm — truly nothing to publish
|
|
3034
|
+
console.log('');
|
|
3035
|
+
if (currentAccess === 'public') {
|
|
3036
|
+
console.log(`${pkg.name}@${pkg.version} is already PUBLIC on npm. No changes to publish.`);
|
|
3037
|
+
}
|
|
3038
|
+
else if (currentAccess === 'restricted') {
|
|
3039
|
+
console.log(`${pkg.name}@${pkg.version} is PRIVATE on npm. No changes to publish.`);
|
|
3040
|
+
}
|
|
3041
|
+
else {
|
|
3042
|
+
console.log('No changes to commit and no custom message specified.');
|
|
3043
|
+
}
|
|
3044
|
+
console.log(colors.dim(' Use -m "message" to force a version bump and republish.'));
|
|
3045
|
+
// Push any unpushed commits (e.g., from a previous failed push)
|
|
3046
|
+
if (currentGitStatus.hasRemote && currentGitStatus.hasUnpushed && !dryRun) {
|
|
3047
|
+
console.log(colors.yellow('Pushing unpushed commits...'));
|
|
3048
|
+
pushWithProtection(cwd, verbose);
|
|
3049
|
+
}
|
|
3050
|
+
// If install/link flag is set, install globally
|
|
3051
|
+
if (install || link || wsl) {
|
|
3052
|
+
if (verbose) {
|
|
3053
|
+
console.log('');
|
|
3054
|
+
console.log(link ? 'Installing from local directory (link)...' : 'Installing from registry...');
|
|
3055
|
+
}
|
|
3056
|
+
const pkgName = pkg.name;
|
|
3057
|
+
const pkgVersion = pkg.version;
|
|
3058
|
+
if (!pkg.bin && (install || link || wsl)) {
|
|
3059
|
+
const proceed = await offerAddBin(cwd, pkg);
|
|
3060
|
+
if (!proceed) {
|
|
3061
|
+
console.log(colors.yellow('Skipping global install — library packages should not be installed globally.'));
|
|
3062
|
+
console.log(colors.yellow(`To use in projects: npm install ${pkgName}`));
|
|
2903
3063
|
}
|
|
2904
3064
|
}
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
});
|
|
2912
|
-
const versionOnNpm = vCheck.status === 0 && vCheck.stdout?.trim() === pkgVersion;
|
|
2913
|
-
if (versionOnNpm) {
|
|
2914
|
-
console.log(`Installing ${pkgName}@${pkgVersion} globally from registry...`);
|
|
2915
|
-
const registryInstallResult = installGlobalWithRetry(`${pkgName}@${pkgVersion}`, cwd);
|
|
2916
|
-
if (registryInstallResult.success) {
|
|
2917
|
-
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
3065
|
+
if (pkg.bin && (install || link || wsl)) {
|
|
3066
|
+
if (link) {
|
|
3067
|
+
console.log(`Installing ${pkgName} globally from local directory (link)...`);
|
|
3068
|
+
const localInstallResult = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
3069
|
+
if (localInstallResult.success) {
|
|
3070
|
+
console.log(colors.green(`✓ Linked globally: ${pkgName}@${pkgVersion}`));
|
|
2918
3071
|
}
|
|
2919
3072
|
else {
|
|
2920
|
-
console.error(colors.red(`✗ Global install failed`));
|
|
2921
|
-
console.error(colors.yellow(` Try running manually: npm install -g ${pkgName}@${pkgVersion}`));
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
else {
|
|
2925
|
-
console.log(colors.yellow(`${pkgName}@${pkgVersion} not found on npm — installing from local directory.`));
|
|
2926
|
-
console.log(colors.dim(' Use -m "message" to publish this version to npm.'));
|
|
2927
|
-
const localResult = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
2928
|
-
if (localResult.success) {
|
|
2929
|
-
console.log(colors.green(`✓ Installed globally from local: ${pkgName}@${pkgVersion}`));
|
|
2930
|
-
}
|
|
2931
|
-
else {
|
|
2932
|
-
console.error(colors.red(`✗ Local install failed`));
|
|
3073
|
+
console.error(colors.red(`✗ Global link install failed`));
|
|
2933
3074
|
console.error(colors.yellow(' Try running manually: npm install -g .'));
|
|
2934
3075
|
}
|
|
2935
3076
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
const useLocal = link || (() => {
|
|
2940
|
-
const vc = spawnSafe('npm', ['view', `${pkgName}@${pkgVersion}`, 'version'], {
|
|
3077
|
+
else if (install) {
|
|
3078
|
+
// Quick check: does this version actually exist on npm?
|
|
3079
|
+
const vCheck = spawnSafe('npm', ['view', `${pkgName}@${pkgVersion}`, 'version'], {
|
|
2941
3080
|
shell: process.platform === 'win32',
|
|
2942
3081
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2943
3082
|
encoding: 'utf-8'
|
|
2944
3083
|
});
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3084
|
+
const vOnNpm = vCheck.status === 0 && vCheck.stdout?.trim() === pkgVersion;
|
|
3085
|
+
if (vOnNpm) {
|
|
3086
|
+
console.log(`Installing ${pkgName}@${pkgVersion} globally from registry...`);
|
|
3087
|
+
const registryInstallResult = installGlobalWithRetry(`${pkgName}@${pkgVersion}`, cwd);
|
|
3088
|
+
if (registryInstallResult.success) {
|
|
3089
|
+
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
3090
|
+
}
|
|
3091
|
+
else {
|
|
3092
|
+
console.error(colors.red(`✗ Global install failed`));
|
|
3093
|
+
console.error(colors.yellow(` Try running manually: npm install -g ${pkgName}@${pkgVersion}`));
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
else {
|
|
3097
|
+
console.log(colors.yellow(`${pkgName}@${pkgVersion} not found on npm — installing from local directory.`));
|
|
3098
|
+
console.log(colors.dim(' Use -m "message" to publish this version to npm.'));
|
|
3099
|
+
const localResult = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
3100
|
+
if (localResult.success) {
|
|
3101
|
+
console.log(colors.green(`✓ Installed globally from local: ${pkgName}@${pkgVersion}`));
|
|
3102
|
+
}
|
|
3103
|
+
else {
|
|
3104
|
+
console.error(colors.red(`✗ Local install failed`));
|
|
3105
|
+
console.error(colors.yellow(' Try running manually: npm install -g .'));
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
2954
3108
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
3109
|
+
if (wsl) {
|
|
3110
|
+
// Check if version is on npm for registry-based WSL install
|
|
3111
|
+
const useLocal = link || (() => {
|
|
3112
|
+
const vc = spawnSafe('npm', ['view', `${pkgName}@${pkgVersion}`, 'version'], {
|
|
3113
|
+
shell: process.platform === 'win32',
|
|
3114
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3115
|
+
encoding: 'utf-8'
|
|
3116
|
+
});
|
|
3117
|
+
return !(vc.status === 0 && vc.stdout?.trim() === pkgVersion);
|
|
3118
|
+
})();
|
|
3119
|
+
const wslArgs = useLocal ? ['npm', 'install', '-g', '.'] : ['npm', 'install', '-g', `${pkgName}@${pkgVersion}`];
|
|
3120
|
+
if (!useLocal)
|
|
3121
|
+
waitForNpmVersion(pkgName, pkgVersion);
|
|
3122
|
+
console.log(`Installing ${pkgName} in WSL${useLocal ? ' (local)' : ' from registry'}...`);
|
|
3123
|
+
const wslInstallResult = runCommand('wsl', wslArgs, { cwd, silent: false, showCommand: true });
|
|
3124
|
+
if (wslInstallResult.success) {
|
|
3125
|
+
console.log(colors.green(`✓ Installed in WSL: ${pkgName}@${pkgVersion}`));
|
|
3126
|
+
}
|
|
3127
|
+
else {
|
|
3128
|
+
console.error(colors.red(`✗ WSL install failed`));
|
|
3129
|
+
console.error(colors.yellow(' Try running manually in WSL: npm install -g ' + (useLocal ? '.' : pkgName)));
|
|
3130
|
+
}
|
|
2958
3131
|
}
|
|
2959
3132
|
}
|
|
2960
3133
|
}
|
|
3134
|
+
return true;
|
|
2961
3135
|
}
|
|
2962
|
-
return true;
|
|
2963
3136
|
}
|
|
2964
3137
|
// Ensure node_modules is in .gitignore before any git operations
|
|
2965
3138
|
if (currentGitStatus.isRepo && !dryRun) {
|
|
@@ -3050,16 +3223,22 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3050
3223
|
console.log(' Pulled latest from remote');
|
|
3051
3224
|
}
|
|
3052
3225
|
}
|
|
3053
|
-
// Version bump
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3226
|
+
// Version bump (skip if republishing a version that's already bumped locally)
|
|
3227
|
+
if (skipVersionBump) {
|
|
3228
|
+
console.log(`Skipping version bump — ${pkg.version} already set locally.`);
|
|
3229
|
+
}
|
|
3230
|
+
else {
|
|
3231
|
+
console.log(`Bumping version (${bump})...`);
|
|
3232
|
+
}
|
|
3233
|
+
if (!dryRun && !skipVersionBump) {
|
|
3234
|
+
// Temporarily disable postversion hook to prevent double-publishing or push
|
|
3235
|
+
// (push is handled separately with push-protection detection)
|
|
3057
3236
|
const pkg = readPackageJson(cwd);
|
|
3058
3237
|
const originalPostversion = pkg.scripts?.postversion;
|
|
3059
3238
|
let postversDisabled = false;
|
|
3060
|
-
if (originalPostversion && originalPostversion.includes('npm publish')) {
|
|
3239
|
+
if (originalPostversion && (originalPostversion.includes('npm publish') || originalPostversion.includes('git push'))) {
|
|
3061
3240
|
if (verbose) {
|
|
3062
|
-
console.log('Temporarily disabling postversion hook
|
|
3241
|
+
console.log('Temporarily disabling postversion hook (npmglobalize handles publish/push)...');
|
|
3063
3242
|
}
|
|
3064
3243
|
delete pkg.scripts.postversion;
|
|
3065
3244
|
writePackageJson(cwd, pkg);
|
|
@@ -3224,6 +3403,35 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3224
3403
|
console.error(' • Disk space or permissions issues');
|
|
3225
3404
|
console.error(colors.yellow('\nTry running with --verbose or check git status manually'));
|
|
3226
3405
|
}
|
|
3406
|
+
else if (combinedOutput.includes('gh013') || combinedOutput.includes('push protection') || combinedOutput.includes('push declined due to repository rule')) {
|
|
3407
|
+
// GitHub push protection blocked a push (from postversion script)
|
|
3408
|
+
const ppInfo = parsePushProtection(error.stderr || error.stdout || error.message || '', cwd);
|
|
3409
|
+
if (ppInfo.detected) {
|
|
3410
|
+
showPushProtectionGuidance(ppInfo);
|
|
3411
|
+
}
|
|
3412
|
+
else {
|
|
3413
|
+
console.error(colors.yellow('\nGitHub push protection blocked the push.'));
|
|
3414
|
+
console.error(colors.yellow('Check the output above for unblock URLs.'));
|
|
3415
|
+
}
|
|
3416
|
+
console.log(colors.yellow('Version and tag were created locally. Continuing with publish...'));
|
|
3417
|
+
autoFixed = true;
|
|
3418
|
+
}
|
|
3419
|
+
// If version/tag exist locally despite error, postversion script likely failed
|
|
3420
|
+
// (e.g., push protection with inherited stdio that we couldn't capture)
|
|
3421
|
+
if (!autoFixed) {
|
|
3422
|
+
try {
|
|
3423
|
+
const postPkg = readPackageJson(cwd);
|
|
3424
|
+
const tagCheck = runCommand('git', ['tag', '-l', `v${postPkg.version}`], { cwd, silent: true });
|
|
3425
|
+
if (tagCheck.success && tagCheck.output.trim() === `v${postPkg.version}`) {
|
|
3426
|
+
console.log(colors.yellow('\nVersion and tag created locally — postversion script may have failed.'));
|
|
3427
|
+
console.log(colors.yellow('Continuing — push will be attempted separately.'));
|
|
3428
|
+
autoFixed = true;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
catch {
|
|
3432
|
+
// ignore — fall through to normal error handling
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3227
3435
|
if (!autoFixed) {
|
|
3228
3436
|
if (!force) {
|
|
3229
3437
|
return false;
|
|
@@ -3232,7 +3440,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3232
3440
|
}
|
|
3233
3441
|
}
|
|
3234
3442
|
}
|
|
3235
|
-
else {
|
|
3443
|
+
else if (!skipVersionBump) {
|
|
3236
3444
|
console.log(` [dry-run] Would run: npm version ${bump}`);
|
|
3237
3445
|
}
|
|
3238
3446
|
// Publish
|
|
@@ -3433,14 +3641,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3433
3641
|
else {
|
|
3434
3642
|
console.log(` [dry-run] Would run: npm publish ${quiet ? '--quiet' : ''}`);
|
|
3435
3643
|
}
|
|
3436
|
-
// Push to git
|
|
3644
|
+
// Push to git (with push-protection detection and auto-bypass)
|
|
3437
3645
|
if (currentGitStatus.hasRemote) {
|
|
3438
3646
|
if (verbose) {
|
|
3439
3647
|
console.log('Pushing to git...');
|
|
3440
3648
|
}
|
|
3441
3649
|
if (!dryRun) {
|
|
3442
|
-
|
|
3443
|
-
|
|
3650
|
+
if (pushWithProtection(cwd, verbose)) {
|
|
3651
|
+
const tagResult = runCommand('git', ['push', '--tags'], { cwd, silent: true });
|
|
3652
|
+
if (verbose && tagResult.success)
|
|
3653
|
+
console.log(colors.green(' ✓ Pushed tags'));
|
|
3654
|
+
}
|
|
3444
3655
|
}
|
|
3445
3656
|
else {
|
|
3446
3657
|
console.log(' [dry-run] Would push to git');
|
|
@@ -3540,7 +3751,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3540
3751
|
runCommand('git', ['add', 'package.json'], { cwd });
|
|
3541
3752
|
runCommand('git', ['commit', '-m', 'Restore file: dependencies'], { cwd });
|
|
3542
3753
|
if (currentGitStatus.hasRemote) {
|
|
3543
|
-
|
|
3754
|
+
pushWithProtection(cwd, verbose);
|
|
3544
3755
|
}
|
|
3545
3756
|
}
|
|
3546
3757
|
}
|