@bobfrankston/npmglobalize 1.0.114 → 1.0.116
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 +26 -11
- package/lib.js +148 -98
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,16 @@ npmglobalize --fix # Runs npm audit fix
|
|
|
127
127
|
npmglobalize --no-fix
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### 🔑 OAuth Credentials Handling
|
|
131
|
+
|
|
132
|
+
`npmglobalize` automatically detects `credentials.json` files and handles them based on OAuth app type:
|
|
133
|
+
|
|
134
|
+
- **Desktop/installed apps** (`"installed"` key in JSON): The `client_secret` is just a public app registration ID, not a real secret. Google's own docs state: *"the client_secret is obviously not treated as a secret."* These files are kept in the repo — `!credentials.json` is added to `.gitignore`/`.npmignore` to override any broader ignore patterns.
|
|
135
|
+
|
|
136
|
+
- **Web apps** (`"web"` key in JSON): The `client_secret` is a real secret. These files are automatically added to `.gitignore`/`.npmignore` to prevent accidental exposure.
|
|
137
|
+
|
|
138
|
+
This distinction also drives the **push protection auto-bypass**: when GitHub blocks a push because it detects an OAuth client secret, `npmglobalize` checks the credential file type. For installed apps, it auto-bypasses (marking as `false_positive`) since the credential is public by design.
|
|
139
|
+
|
|
130
140
|
### 🔄 File Reference Management
|
|
131
141
|
|
|
132
142
|
**Default behavior** (restore file: references after publish):
|
|
@@ -165,6 +175,10 @@ npmglobalize --rebase # Auto-rebase if behind remote
|
|
|
165
175
|
|
|
166
176
|
For single-developer projects, this safely pulls remote changes before publishing.
|
|
167
177
|
|
|
178
|
+
**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.
|
|
179
|
+
|
|
180
|
+
**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.
|
|
181
|
+
|
|
168
182
|
**Note:** This tool is designed for single-developer, single-branch workflows where automatic rebase and tag cleanup are safe operations.
|
|
169
183
|
|
|
170
184
|
### 📂 Git Repository Setup
|
|
@@ -390,17 +404,18 @@ npmglobalize --dry-run # See what would happen
|
|
|
390
404
|
## How It Works
|
|
391
405
|
|
|
392
406
|
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. **
|
|
407
|
+
2. **Checks** if current version is on npm (recovers from failed publishes)
|
|
408
|
+
3. **Updates dependencies** (if `--update-deps`)
|
|
409
|
+
4. **Publishes file: dependencies** (if needed)
|
|
410
|
+
5. **Backs up** original file: references to `.dependencies`
|
|
411
|
+
6. **Converts** `file:` → npm version references
|
|
412
|
+
7. **Commits** changes
|
|
413
|
+
8. **Bumps** version (using npm version) — skipped if recovering a failed publish
|
|
414
|
+
9. **Publishes** to npm
|
|
415
|
+
10. **Pushes** to git (with push-protection detection and auto-bypass)
|
|
416
|
+
11. **Installs** globally (if `--install`)
|
|
417
|
+
12. **Restores** file: references (if `--files`, default)
|
|
418
|
+
13. **Runs audit** (shows security status)
|
|
404
419
|
|
|
405
420
|
## Operational Details
|
|
406
421
|
|
package/lib.js
CHANGED
|
@@ -1440,30 +1440,51 @@ function parsePushProtection(errorOutput, cwd) {
|
|
|
1440
1440
|
return result;
|
|
1441
1441
|
}
|
|
1442
1442
|
result.detected = true;
|
|
1443
|
-
//
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1443
|
+
// Match secret blocks using a single regex across any dash characters
|
|
1444
|
+
// Pattern: "-- Type Name ---..." then "path: file:line" then "unblock-secret/id" URL
|
|
1445
|
+
// Handles em-dash (—), en-dash (–), and regular dash (-)
|
|
1446
|
+
const secretRegex = /[-\u2014\u2013]{2,}\s+(.+?)\s+[-\u2014\u2013]{2,}[\s\S]*?path:\s+(\S+?)(?::\d+)?\s[\s\S]*?(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/g;
|
|
1447
|
+
let match;
|
|
1448
|
+
while ((match = secretRegex.exec(cleaned)) !== null) {
|
|
1449
|
+
result.secrets.push({
|
|
1450
|
+
type: match[1].trim(),
|
|
1451
|
+
file: match[2].trim(),
|
|
1452
|
+
unblockUrl: match[3].trim()
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
// If regex didn't match (encoding issues), fall back to finding unblock URLs + paths
|
|
1456
|
+
if (result.secrets.length === 0) {
|
|
1457
|
+
const urlRegex = /(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/g;
|
|
1458
|
+
const pathRegex = /path:\s+(\S+?)(?::\d+)?\s/g;
|
|
1459
|
+
const urls = [];
|
|
1460
|
+
const files = [];
|
|
1461
|
+
let m;
|
|
1462
|
+
while ((m = urlRegex.exec(cleaned)) !== null)
|
|
1463
|
+
urls.push(m[1].trim());
|
|
1464
|
+
while ((m = pathRegex.exec(cleaned)) !== null)
|
|
1465
|
+
files.push(m[1].trim());
|
|
1466
|
+
// Try to detect secret type from text
|
|
1467
|
+
const hasOAuth = cleaned.toLowerCase().includes('oauth');
|
|
1468
|
+
const secretType = hasOAuth ? 'Google OAuth Credential' : 'Secret';
|
|
1469
|
+
for (let i = 0; i < Math.max(urls.length, files.length); i++) {
|
|
1470
|
+
result.secrets.push({
|
|
1471
|
+
type: secretType,
|
|
1472
|
+
file: files[i] || '',
|
|
1473
|
+
unblockUrl: urls[i] || ''
|
|
1474
|
+
});
|
|
1456
1475
|
}
|
|
1457
1476
|
}
|
|
1458
1477
|
// Check if all detected secrets are from Google OAuth installed apps (safe to include)
|
|
1459
1478
|
if (result.secrets.length > 0) {
|
|
1460
1479
|
result.allInstalledOAuth = result.secrets.every(s => {
|
|
1461
|
-
if (!s.type.toLowerCase().includes('oauth'))
|
|
1462
|
-
return false;
|
|
1463
1480
|
if (!s.file)
|
|
1464
1481
|
return false;
|
|
1482
|
+
// Check by type name OR by inspecting the actual credential file
|
|
1465
1483
|
const credType = detectCredentialsTypeFromFile(path.join(cwd, s.file));
|
|
1466
|
-
|
|
1484
|
+
if (credType === 'installed')
|
|
1485
|
+
return true;
|
|
1486
|
+
// Also match if the type name mentions OAuth (even if file check failed)
|
|
1487
|
+
return s.type.toLowerCase().includes('oauth') && credType !== 'web';
|
|
1467
1488
|
});
|
|
1468
1489
|
}
|
|
1469
1490
|
return result;
|
|
@@ -3012,103 +3033,127 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3012
3033
|
// Check if there are changes to commit or a custom message
|
|
3013
3034
|
// Skip this check for first publish (currentAccess null) or just-initialized repos
|
|
3014
3035
|
const isFirstPublish = !currentAccess;
|
|
3036
|
+
let skipVersionBump = false;
|
|
3015
3037
|
if (!currentGitStatus.hasUncommitted && !message && !justInitialized && !isFirstPublish) {
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3038
|
+
// Check if the current version is actually on npm (it might have failed to publish previously)
|
|
3039
|
+
const versionCheck = spawnSafe('npm', ['view', `${pkg.name}@${pkg.version}`, 'version'], {
|
|
3040
|
+
shell: process.platform === 'win32',
|
|
3041
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3042
|
+
encoding: 'utf-8'
|
|
3043
|
+
});
|
|
3044
|
+
const versionIsOnNpm = versionCheck.status === 0 && versionCheck.stdout?.trim() === pkg.version;
|
|
3045
|
+
if (!versionIsOnNpm && currentAccess) {
|
|
3046
|
+
// Version not on npm — previous publish/push likely failed
|
|
3047
|
+
// Skip version bump but fall through to publish+push
|
|
3048
|
+
console.log('');
|
|
3049
|
+
console.log(colors.yellow(`${pkg.name}@${pkg.version} is NOT on npm (previous publish may have failed).`));
|
|
3050
|
+
console.log(colors.yellow('Republishing current version...'));
|
|
3051
|
+
skipVersionBump = true;
|
|
3022
3052
|
}
|
|
3023
3053
|
else {
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
if (
|
|
3030
|
-
console.log(
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
if (
|
|
3043
|
-
if (
|
|
3044
|
-
console.log(
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3054
|
+
// Version is on npm — truly nothing to publish
|
|
3055
|
+
console.log('');
|
|
3056
|
+
if (currentAccess === 'public') {
|
|
3057
|
+
console.log(`${pkg.name}@${pkg.version} is already PUBLIC on npm. No changes to publish.`);
|
|
3058
|
+
}
|
|
3059
|
+
else if (currentAccess === 'restricted') {
|
|
3060
|
+
console.log(`${pkg.name}@${pkg.version} is PRIVATE on npm. No changes to publish.`);
|
|
3061
|
+
}
|
|
3062
|
+
else {
|
|
3063
|
+
console.log('No changes to commit and no custom message specified.');
|
|
3064
|
+
}
|
|
3065
|
+
console.log(colors.dim(' Use -m "message" to force a version bump and republish.'));
|
|
3066
|
+
// Push any unpushed commits (e.g., from a previous failed push)
|
|
3067
|
+
if (currentGitStatus.hasRemote && currentGitStatus.hasUnpushed && !dryRun) {
|
|
3068
|
+
console.log(colors.yellow('Pushing unpushed commits...'));
|
|
3069
|
+
pushWithProtection(cwd, verbose);
|
|
3070
|
+
}
|
|
3071
|
+
// If install/link flag is set, install globally
|
|
3072
|
+
if (install || link || wsl) {
|
|
3073
|
+
if (verbose) {
|
|
3074
|
+
console.log('');
|
|
3075
|
+
console.log(link ? 'Installing from local directory (link)...' : 'Installing from registry...');
|
|
3076
|
+
}
|
|
3077
|
+
const pkgName = pkg.name;
|
|
3078
|
+
const pkgVersion = pkg.version;
|
|
3079
|
+
if (!pkg.bin && (install || link || wsl)) {
|
|
3080
|
+
const proceed = await offerAddBin(cwd, pkg);
|
|
3081
|
+
if (!proceed) {
|
|
3082
|
+
console.log(colors.yellow('Skipping global install — library packages should not be installed globally.'));
|
|
3083
|
+
console.log(colors.yellow(`To use in projects: npm install ${pkgName}`));
|
|
3052
3084
|
}
|
|
3053
3085
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
});
|
|
3061
|
-
const versionOnNpm = vCheck.status === 0 && vCheck.stdout?.trim() === pkgVersion;
|
|
3062
|
-
if (versionOnNpm) {
|
|
3063
|
-
console.log(`Installing ${pkgName}@${pkgVersion} globally from registry...`);
|
|
3064
|
-
const registryInstallResult = installGlobalWithRetry(`${pkgName}@${pkgVersion}`, cwd);
|
|
3065
|
-
if (registryInstallResult.success) {
|
|
3066
|
-
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
3067
|
-
}
|
|
3068
|
-
else {
|
|
3069
|
-
console.error(colors.red(`✗ Global install failed`));
|
|
3070
|
-
console.error(colors.yellow(` Try running manually: npm install -g ${pkgName}@${pkgVersion}`));
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
else {
|
|
3074
|
-
console.log(colors.yellow(`${pkgName}@${pkgVersion} not found on npm — installing from local directory.`));
|
|
3075
|
-
console.log(colors.dim(' Use -m "message" to publish this version to npm.'));
|
|
3076
|
-
const localResult = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
3077
|
-
if (localResult.success) {
|
|
3078
|
-
console.log(colors.green(`✓ Installed globally from local: ${pkgName}@${pkgVersion}`));
|
|
3086
|
+
if (pkg.bin && (install || link || wsl)) {
|
|
3087
|
+
if (link) {
|
|
3088
|
+
console.log(`Installing ${pkgName} globally from local directory (link)...`);
|
|
3089
|
+
const localInstallResult = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
3090
|
+
if (localInstallResult.success) {
|
|
3091
|
+
console.log(colors.green(`✓ Linked globally: ${pkgName}@${pkgVersion}`));
|
|
3079
3092
|
}
|
|
3080
3093
|
else {
|
|
3081
|
-
console.error(colors.red(`✗
|
|
3094
|
+
console.error(colors.red(`✗ Global link install failed`));
|
|
3082
3095
|
console.error(colors.yellow(' Try running manually: npm install -g .'));
|
|
3083
3096
|
}
|
|
3084
3097
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
const useLocal = link || (() => {
|
|
3089
|
-
const vc = spawnSafe('npm', ['view', `${pkgName}@${pkgVersion}`, 'version'], {
|
|
3098
|
+
else if (install) {
|
|
3099
|
+
// Quick check: does this version actually exist on npm?
|
|
3100
|
+
const vCheck = spawnSafe('npm', ['view', `${pkgName}@${pkgVersion}`, 'version'], {
|
|
3090
3101
|
shell: process.platform === 'win32',
|
|
3091
3102
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3092
3103
|
encoding: 'utf-8'
|
|
3093
3104
|
});
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3105
|
+
const vOnNpm = vCheck.status === 0 && vCheck.stdout?.trim() === pkgVersion;
|
|
3106
|
+
if (vOnNpm) {
|
|
3107
|
+
console.log(`Installing ${pkgName}@${pkgVersion} globally from registry...`);
|
|
3108
|
+
const registryInstallResult = installGlobalWithRetry(`${pkgName}@${pkgVersion}`, cwd);
|
|
3109
|
+
if (registryInstallResult.success) {
|
|
3110
|
+
console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
|
|
3111
|
+
}
|
|
3112
|
+
else {
|
|
3113
|
+
console.error(colors.red(`✗ Global install failed`));
|
|
3114
|
+
console.error(colors.yellow(` Try running manually: npm install -g ${pkgName}@${pkgVersion}`));
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
else {
|
|
3118
|
+
console.log(colors.yellow(`${pkgName}@${pkgVersion} not found on npm — installing from local directory.`));
|
|
3119
|
+
console.log(colors.dim(' Use -m "message" to publish this version to npm.'));
|
|
3120
|
+
const localResult = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
3121
|
+
if (localResult.success) {
|
|
3122
|
+
console.log(colors.green(`✓ Installed globally from local: ${pkgName}@${pkgVersion}`));
|
|
3123
|
+
}
|
|
3124
|
+
else {
|
|
3125
|
+
console.error(colors.red(`✗ Local install failed`));
|
|
3126
|
+
console.error(colors.yellow(' Try running manually: npm install -g .'));
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3103
3129
|
}
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3130
|
+
if (wsl) {
|
|
3131
|
+
// Check if version is on npm for registry-based WSL install
|
|
3132
|
+
const useLocal = link || (() => {
|
|
3133
|
+
const vc = spawnSafe('npm', ['view', `${pkgName}@${pkgVersion}`, 'version'], {
|
|
3134
|
+
shell: process.platform === 'win32',
|
|
3135
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3136
|
+
encoding: 'utf-8'
|
|
3137
|
+
});
|
|
3138
|
+
return !(vc.status === 0 && vc.stdout?.trim() === pkgVersion);
|
|
3139
|
+
})();
|
|
3140
|
+
const wslArgs = useLocal ? ['npm', 'install', '-g', '.'] : ['npm', 'install', '-g', `${pkgName}@${pkgVersion}`];
|
|
3141
|
+
if (!useLocal)
|
|
3142
|
+
waitForNpmVersion(pkgName, pkgVersion);
|
|
3143
|
+
console.log(`Installing ${pkgName} in WSL${useLocal ? ' (local)' : ' from registry'}...`);
|
|
3144
|
+
const wslInstallResult = runCommand('wsl', wslArgs, { cwd, silent: false, showCommand: true });
|
|
3145
|
+
if (wslInstallResult.success) {
|
|
3146
|
+
console.log(colors.green(`✓ Installed in WSL: ${pkgName}@${pkgVersion}`));
|
|
3147
|
+
}
|
|
3148
|
+
else {
|
|
3149
|
+
console.error(colors.red(`✗ WSL install failed`));
|
|
3150
|
+
console.error(colors.yellow(' Try running manually in WSL: npm install -g ' + (useLocal ? '.' : pkgName)));
|
|
3151
|
+
}
|
|
3107
3152
|
}
|
|
3108
3153
|
}
|
|
3109
3154
|
}
|
|
3155
|
+
return true;
|
|
3110
3156
|
}
|
|
3111
|
-
return true;
|
|
3112
3157
|
}
|
|
3113
3158
|
// Ensure node_modules is in .gitignore before any git operations
|
|
3114
3159
|
if (currentGitStatus.isRepo && !dryRun) {
|
|
@@ -3199,9 +3244,14 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3199
3244
|
console.log(' Pulled latest from remote');
|
|
3200
3245
|
}
|
|
3201
3246
|
}
|
|
3202
|
-
// Version bump
|
|
3203
|
-
|
|
3204
|
-
|
|
3247
|
+
// Version bump (skip if republishing a version that's already bumped locally)
|
|
3248
|
+
if (skipVersionBump) {
|
|
3249
|
+
console.log(`Skipping version bump — ${pkg.version} already set locally.`);
|
|
3250
|
+
}
|
|
3251
|
+
else {
|
|
3252
|
+
console.log(`Bumping version (${bump})...`);
|
|
3253
|
+
}
|
|
3254
|
+
if (!dryRun && !skipVersionBump) {
|
|
3205
3255
|
// Temporarily disable postversion hook to prevent double-publishing or push
|
|
3206
3256
|
// (push is handled separately with push-protection detection)
|
|
3207
3257
|
const pkg = readPackageJson(cwd);
|
|
@@ -3411,7 +3461,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3411
3461
|
}
|
|
3412
3462
|
}
|
|
3413
3463
|
}
|
|
3414
|
-
else {
|
|
3464
|
+
else if (!skipVersionBump) {
|
|
3415
3465
|
console.log(` [dry-run] Would run: npm version ${bump}`);
|
|
3416
3466
|
}
|
|
3417
3467
|
// Publish
|