@bobfrankston/npmglobalize 1.0.199 → 1.0.201

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/git.js DELETED
@@ -1,748 +0,0 @@
1
- /**
2
- * npmglobalize — Git operations, command execution, and push protection.
3
- * Depends on: types (leaf), config (leaf)
4
- */
5
- import fs from 'fs';
6
- import path from 'path';
7
- import { execSync } from 'child_process';
8
- import { spawnSafe, colors } from './types.js';
9
- // ── Group 1: Low-level utilities (private) ──────────────────────────
10
- /**
11
- * Remove 'nul' files from a directory tree (Windows reserved name issue).
12
- * These files break git and npm on Windows. Uses \\?\ prefix to bypass name validation.
13
- */
14
- export function removeNulFiles(dir, visited = new Set()) {
15
- const resolved = path.resolve(dir);
16
- if (visited.has(resolved))
17
- return 0;
18
- visited.add(resolved);
19
- let count = 0;
20
- try {
21
- const entries = fs.readdirSync(dir, { withFileTypes: true });
22
- for (const entry of entries) {
23
- const full = path.join(dir, entry.name);
24
- if (entry.isDirectory() && !entry.isSymbolicLink() && entry.name !== 'node_modules') {
25
- count += removeNulFiles(full, visited);
26
- }
27
- else if (entry.name === 'nul') {
28
- try {
29
- fs.unlinkSync(path.join('\\\\?\\', path.resolve(full)));
30
- count++;
31
- }
32
- catch {
33
- // Ignore deletion errors
34
- }
35
- }
36
- }
37
- }
38
- catch {
39
- // Ignore read errors (permissions, etc.)
40
- }
41
- return count;
42
- }
43
- /** Repair a corrupted git index by rebuilding it.
44
- * Handles "invalid object" / "Error building trees" errors caused by
45
- * stale entries in .git/index referencing missing objects. */
46
- export function repairGitIndex(cwd) {
47
- const indexPath = path.join(cwd, '.git', 'index');
48
- try {
49
- console.log(colors.yellow('Detected corrupted git index — rebuilding...'));
50
- // Delete the corrupted index
51
- if (fs.existsSync(indexPath)) {
52
- fs.unlinkSync(indexPath);
53
- }
54
- // Rebuild from HEAD (reset index to match last commit)
55
- const resetResult = spawnSafe('git', ['reset'], {
56
- encoding: 'utf-8',
57
- stdio: 'pipe',
58
- cwd,
59
- env: process.env
60
- });
61
- if (resetResult.status !== 0) {
62
- // If no commits yet, just proceed — git add -A will build a fresh index
63
- const stderr = resetResult.stderr || '';
64
- if (!stderr.includes('does not have any commits')) {
65
- console.error(colors.red('Failed to reset git index:'), stderr.trim());
66
- return false;
67
- }
68
- }
69
- // Re-stage all files
70
- const addResult = spawnSafe('git', ['add', '-A'], {
71
- encoding: 'utf-8',
72
- stdio: 'pipe',
73
- cwd,
74
- env: process.env
75
- });
76
- if (addResult.status !== 0) {
77
- console.error(colors.red('Failed to re-add files after index rebuild:'), (addResult.stderr || '').trim());
78
- return false;
79
- }
80
- console.log(colors.green('✓ Git index rebuilt successfully'));
81
- return true;
82
- }
83
- catch (err) {
84
- console.error(colors.red('Failed to repair git index:'), err.message);
85
- return false;
86
- }
87
- }
88
- /** Parse file paths from git add "Permission denied" / "unable to index file" errors.
89
- * Matches lines like: error: open("data/PingDB.mdf"): Permission denied
90
- * and: error: unable to index file 'data/PingDB.mdf' */
91
- export function parseDeniedFiles(errText) {
92
- const files = [];
93
- for (const line of errText.split('\n')) {
94
- // error: open("path"): Permission denied
95
- let m = line.match(/error:\s*open\(["'](.+?)["']\):\s*Permission denied/i);
96
- if (m) {
97
- files.push(m[1]);
98
- continue;
99
- }
100
- // error: unable to index file 'path'
101
- m = line.match(/error:\s*unable to index file\s+['"](.+?)['"]/i);
102
- if (m) {
103
- files.push(m[1]);
104
- continue;
105
- }
106
- }
107
- return files;
108
- }
109
- // ── Group 2: Command execution ──────────────────────────────────────
110
- /** Run a command and return success status */
111
- export function runCommand(cmd, args, options = {}) {
112
- const { silent = false, verbose = false, showCommand = false, cwd } = options;
113
- try {
114
- // Use shell:true for npm/gh commands which need it on Windows
115
- const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh';
116
- // Show command for important actions (install, etc.) or in verbose mode
117
- if (!silent && (showCommand || verbose)) {
118
- console.log(colors.cyan(`> ${cmd} ${args.join(' ')}`));
119
- }
120
- const result = spawnSafe(cmd, args, {
121
- encoding: 'utf-8',
122
- stdio: silent ? 'pipe' : 'inherit',
123
- cwd,
124
- env: process.env, // Always inherit environment variables
125
- shell: needsShell // Only use shell for npm commands
126
- });
127
- // For non-silent commands, we can't capture output when using 'inherit'
128
- // So we return empty string for output, but the user sees it in the terminal
129
- if (!silent) {
130
- return {
131
- success: result.status === 0,
132
- output: '',
133
- stderr: ''
134
- };
135
- }
136
- const output = result.stdout || '';
137
- const stderr = result.stderr || '';
138
- return {
139
- success: result.status === 0,
140
- output: output,
141
- stderr: stderr
142
- };
143
- }
144
- catch (error) {
145
- return { success: false, output: '', stderr: error.message };
146
- }
147
- }
148
- /** Extract GitHub repo identifier from repository field */
149
- export function getGitHubRepo(pkg) {
150
- if (!pkg.repository)
151
- return null;
152
- const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository.url;
153
- if (!repo)
154
- return null;
155
- // Handle various GitHub URL formats
156
- // git+https://github.com/owner/repo.git
157
- // https://github.com/owner/repo
158
- // github:owner/repo
159
- // owner/repo
160
- const match = repo.match(/github\.com[\/:]([^\/]+\/[^\/\.]+)/);
161
- if (match) {
162
- return match[1]; // Returns "owner/repo"
163
- }
164
- // Already in owner/repo format
165
- if (/^[^\/]+\/[^\/]+$/.test(repo)) {
166
- return repo;
167
- }
168
- return null;
169
- }
170
- /** Run a command and throw on failure */
171
- export function runCommandOrThrow(cmd, args, options = {}) {
172
- const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh';
173
- const result = spawnSafe(cmd, args, {
174
- encoding: 'utf-8',
175
- stdio: 'pipe',
176
- cwd: options.cwd,
177
- env: process.env,
178
- shell: needsShell
179
- });
180
- const stdout = result.stdout || '';
181
- const stderr = result.stderr || '';
182
- const output = stdout + stderr;
183
- if (result.status !== 0) {
184
- // Detect git safe.directory ownership issue and auto-fix
185
- if (cmd === 'git' && stderr.includes('dubious ownership')) {
186
- console.log(colors.yellow('Git "dubious ownership" error — directory owner SID differs from current user.'));
187
- console.log(colors.yellow('Adding safe.directory \'*\' to global git config...'));
188
- const fix = spawnSafe('git', ['config', '--global', '--add', 'safe.directory', '*'], {
189
- encoding: 'utf-8',
190
- stdio: 'pipe',
191
- shell: true
192
- });
193
- if (fix.status === 0) {
194
- console.log(colors.green('✓ Fixed. Retrying...'));
195
- // Retry the original command
196
- const retry = spawnSafe(cmd, args, {
197
- encoding: 'utf-8',
198
- stdio: 'pipe',
199
- cwd: options.cwd,
200
- env: process.env,
201
- shell: needsShell
202
- });
203
- if (retry.status === 0) {
204
- return (retry.stdout || '') + (retry.stderr || '');
205
- }
206
- // Retry also failed
207
- const retryStderr = retry.stderr || '';
208
- if (retryStderr.trim()) {
209
- console.error(colors.red(retryStderr.trim()));
210
- }
211
- throw new Error(`Command failed after safe.directory fix: ${cmd} ${args.join(' ')}`);
212
- }
213
- throw new Error('Failed to add safe.directory to git config.');
214
- }
215
- // Detect corrupted git index and auto-repair
216
- if (cmd === 'git' && (stderr.includes('invalid object') || stderr.includes('Error building trees'))) {
217
- if (options.cwd && repairGitIndex(options.cwd)) {
218
- // Retry the original command
219
- const retry = spawnSafe(cmd, args, {
220
- encoding: 'utf-8',
221
- stdio: 'pipe',
222
- cwd: options.cwd,
223
- env: process.env,
224
- shell: needsShell
225
- });
226
- if (retry.status === 0) {
227
- return (retry.stdout || '') + (retry.stderr || '');
228
- }
229
- const retryStderr = retry.stderr || '';
230
- if (retryStderr.trim()) {
231
- console.error(colors.red(retryStderr.trim()));
232
- }
233
- throw new Error(`Command failed after git index repair: ${cmd} ${args.join(' ')}`);
234
- }
235
- }
236
- // Show the error output
237
- if (stderr.trim()) {
238
- console.error(colors.red(stderr.trim()));
239
- }
240
- if (stdout.trim()) {
241
- console.log(stdout.trim());
242
- }
243
- throw new Error(`Command failed with exit code ${result.status}: ${cmd} ${args.join(' ')}`);
244
- }
245
- return output;
246
- }
247
- // ── Group 3: Git status ─────────────────────────────────────────────
248
- export function getGitStatus(cwd) {
249
- const status = {
250
- isRepo: false,
251
- hasRemote: false,
252
- hasUncommitted: false,
253
- hasUnpushed: false,
254
- hasMergeConflict: false,
255
- isDetachedHead: false,
256
- currentBranch: '',
257
- remoteBranch: '',
258
- isBehindRemote: false
259
- };
260
- // Check if git repo
261
- const gitDir = path.join(cwd, '.git');
262
- if (!fs.existsSync(gitDir)) {
263
- return status;
264
- }
265
- status.isRepo = true;
266
- // Check for merge conflicts
267
- const mergeHead = path.join(gitDir, 'MERGE_HEAD');
268
- status.hasMergeConflict = fs.existsSync(mergeHead);
269
- // Get branch info
270
- try {
271
- const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
272
- status.currentBranch = branch;
273
- status.isDetachedHead = branch === 'HEAD';
274
- }
275
- catch (error) {
276
- // Ignore - might be no commits yet
277
- }
278
- // Check for remote
279
- try {
280
- const remote = execSync('git remote', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
281
- status.hasRemote = remote.length > 0;
282
- }
283
- catch (error) {
284
- // Ignore
285
- }
286
- // Check for uncommitted changes
287
- try {
288
- const statusOutput = execSync('git status --porcelain', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
289
- status.hasUncommitted = statusOutput.trim().length > 0;
290
- }
291
- catch (error) {
292
- // Ignore
293
- }
294
- // Check for unpushed commits
295
- if (status.hasRemote && !status.isDetachedHead && status.currentBranch) {
296
- try {
297
- const unpushed = execSync(`git log origin/${status.currentBranch}..HEAD --oneline`, {
298
- cwd,
299
- encoding: 'utf-8',
300
- stdio: ['pipe', 'pipe', 'ignore']
301
- }).trim();
302
- status.hasUnpushed = unpushed.length > 0;
303
- }
304
- catch (error) {
305
- // Ignore - might not have tracking branch
306
- }
307
- // Check if local is behind remote
308
- try {
309
- const behind = execSync(`git log HEAD..origin/${status.currentBranch} --oneline`, {
310
- cwd,
311
- encoding: 'utf-8',
312
- stdio: ['pipe', 'pipe', 'ignore']
313
- }).trim();
314
- status.isBehindRemote = behind.length > 0;
315
- }
316
- catch (error) {
317
- // Ignore - might not have tracking branch
318
- }
319
- }
320
- return status;
321
- }
322
- /** Validate package.json for release */
323
- export function validatePackageJson(pkg) {
324
- const errors = [];
325
- const warnings = [];
326
- if (!pkg.repository) {
327
- errors.push('Missing repository field - required for npm packages');
328
- }
329
- if (!pkg.description) {
330
- warnings.push('Missing description field');
331
- }
332
- if (!pkg.name) {
333
- errors.push('Missing name field');
334
- }
335
- if (!pkg.version) {
336
- errors.push('Missing version field');
337
- }
338
- // Print warnings
339
- for (const w of warnings) {
340
- console.log(` Warning: ${w}`);
341
- }
342
- return errors;
343
- }
344
- // ── Group 4: Git tags ───────────────────────────────────────────────
345
- /** Get the latest git tag (if any) */
346
- export function getLatestGitTag(cwd) {
347
- try {
348
- const result = spawnSafe('git', ['describe', '--tags', '--abbrev=0'], {
349
- encoding: 'utf-8',
350
- stdio: 'pipe',
351
- cwd
352
- });
353
- if (result.status === 0 && result.stdout) {
354
- return result.stdout.trim();
355
- }
356
- return null;
357
- }
358
- catch (error) {
359
- return null;
360
- }
361
- }
362
- /** Check if a git tag exists */
363
- export function gitTagExists(cwd, tag) {
364
- try {
365
- const result = spawnSafe('git', ['tag', '-l', tag], {
366
- encoding: 'utf-8',
367
- stdio: 'pipe',
368
- cwd
369
- });
370
- return result.status === 0 && result.stdout.trim() === tag;
371
- }
372
- catch (error) {
373
- return false;
374
- }
375
- }
376
- /** Delete a git tag */
377
- export function deleteGitTag(cwd, tag) {
378
- try {
379
- const result = spawnSafe('git', ['tag', '-d', tag], {
380
- encoding: 'utf-8',
381
- stdio: 'pipe',
382
- cwd
383
- });
384
- return result.status === 0;
385
- }
386
- catch (error) {
387
- return false;
388
- }
389
- }
390
- /** Get all git tags */
391
- export function getAllGitTags(cwd) {
392
- try {
393
- const result = spawnSafe('git', ['tag', '-l'], {
394
- encoding: 'utf-8',
395
- stdio: 'pipe',
396
- cwd
397
- });
398
- if (result.status === 0 && result.stdout) {
399
- return result.stdout.trim().split('\n').filter(t => t.trim());
400
- }
401
- return [];
402
- }
403
- catch (error) {
404
- return [];
405
- }
406
- }
407
- /** Parse version from tag (e.g., 'v1.2.3' -> [1, 2, 3]) */
408
- export function parseVersionTag(tag) {
409
- const match = tag.match(/^v?(\d+)\.(\d+)\.(\d+)/);
410
- if (!match)
411
- return null;
412
- return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
413
- }
414
- /** Compare two version arrays (returns -1 if a < b, 0 if equal, 1 if a > b) */
415
- export function compareVersions(a, b) {
416
- for (let i = 0; i < 3; i++) {
417
- if (a[i] < b[i])
418
- return -1;
419
- if (a[i] > b[i])
420
- return 1;
421
- }
422
- return 0;
423
- }
424
- /** Fix version/tag mismatches */
425
- export function fixVersionTagMismatch(cwd, pkg, verbose = false) {
426
- const pkgVersion = pkg.version;
427
- if (!pkgVersion) {
428
- return false; // No version in package.json
429
- }
430
- const currentVersion = parseVersionTag(pkgVersion);
431
- if (!currentVersion) {
432
- console.error(colors.yellow(`Warning: Could not parse package.json version: ${pkgVersion}`));
433
- return false;
434
- }
435
- const allTags = getAllGitTags(cwd);
436
- const tagsToDelete = [];
437
- // Find all tags that are >= current package.json version
438
- for (const tag of allTags) {
439
- const tagVersion = parseVersionTag(tag);
440
- if (tagVersion && compareVersions(tagVersion, currentVersion) >= 0) {
441
- tagsToDelete.push(tag);
442
- }
443
- }
444
- if (tagsToDelete.length === 0) {
445
- if (verbose) {
446
- console.log(`No conflicting tags found (package.json is at v${pkgVersion})`);
447
- }
448
- return false;
449
- }
450
- // Silently clean up tags unless verbose mode
451
- if (verbose) {
452
- console.log(colors.yellow(`\nVersion/tag mismatch detected:`));
453
- console.log(` package.json version: ${pkgVersion}`);
454
- console.log(` Conflicting tags found: ${tagsToDelete.join(', ')}`);
455
- console.log(colors.yellow('Deleting conflicting tags...'));
456
- }
457
- let deletedAny = false;
458
- for (const tag of tagsToDelete) {
459
- if (deleteGitTag(cwd, tag)) {
460
- if (verbose) {
461
- console.log(colors.green(` ✓ Deleted tag ${tag}`));
462
- }
463
- deletedAny = true;
464
- }
465
- else {
466
- console.error(colors.red(` ✗ Failed to delete tag ${tag}`));
467
- }
468
- }
469
- return deletedAny;
470
- }
471
- // ── Group 5: npm install helpers (private) ──────────────────────────
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}`);
490
- for (let i = 0; i < maxAttempts; i++) {
491
- const result = spawnSafe('npm', ['view', `${pkgName}@${version}`, 'version'], {
492
- shell: process.platform === 'win32',
493
- stdio: ['pipe', 'pipe', 'pipe'],
494
- encoding: 'utf-8'
495
- });
496
- if (result.status === 0 && result.stdout?.trim() === version) {
497
- process.stdout.write(' ready\n');
498
- return true;
499
- }
500
- process.stdout.write('.');
501
- sleepSync(interval);
502
- }
503
- process.stdout.write(' timed out\n');
504
- return false;
505
- }
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;
513
- let result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
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);
517
- result = runCommand('npm', ['install', '-g', pkgSpec], { cwd, silent: false, showCommand: true });
518
- }
519
- return result;
520
- }
521
- // ── Group 6: Push protection ────────────────────────────────────────
522
- /** Detect OAuth credentials type from a specific JSON file path.
523
- * Returns 'installed' (public app ID, safe) or 'web' (real secret) or null. */
524
- function detectCredentialsTypeFromFile(filePath) {
525
- if (!fs.existsSync(filePath))
526
- return null;
527
- try {
528
- const content = fs.readFileSync(filePath, 'utf-8');
529
- const parsed = JSON.parse(content);
530
- if (parsed.installed)
531
- return 'installed';
532
- if (parsed.web)
533
- return 'web';
534
- return null; // Unknown format (e.g., Microsoft OAuth)
535
- }
536
- catch {
537
- return null;
538
- }
539
- }
540
- /** Detect OAuth credentials.json type: "installed" (public app ID, safe to include) or "web" (has real secret, must ignore).
541
- * Returns null if no credentials.json exists or it can't be parsed. */
542
- export function detectCredentialsType(cwd) {
543
- return detectCredentialsTypeFromFile(path.join(cwd, 'credentials.json'));
544
- }
545
- /** Parse GitHub push protection (GH013) error output and extract secret details + unblock URLs. */
546
- export function parsePushProtection(errorOutput, cwd) {
547
- const result = { detected: false, secrets: [], allInstalledOAuth: false };
548
- if (!errorOutput)
549
- return result;
550
- // Strip 'remote: ' prefixes for easier parsing
551
- const cleaned = errorOutput.replace(/^remote:\s*/gm, '');
552
- if (!cleaned.includes('GH013') && !cleaned.toLowerCase().includes('push protection')) {
553
- return result;
554
- }
555
- result.detected = true;
556
- // Match secret blocks using a single regex across any dash characters
557
- // Pattern: "-- Type Name ---..." then "path: file:line" then "unblock-secret/id" URL
558
- // Handles em-dash (—), en-dash (–), and regular dash (-)
559
- 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;
560
- let match;
561
- while ((match = secretRegex.exec(cleaned)) !== null) {
562
- result.secrets.push({
563
- type: match[1].trim(),
564
- file: match[2].trim(),
565
- unblockUrl: match[3].trim()
566
- });
567
- }
568
- // If regex didn't match (encoding issues), fall back to finding unblock URLs + paths
569
- if (result.secrets.length === 0) {
570
- const urlRegex = /(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/g;
571
- const pathRegex = /path:\s+(\S+?)(?::\d+)?\s/g;
572
- const urls = [];
573
- const files = [];
574
- let m;
575
- while ((m = urlRegex.exec(cleaned)) !== null)
576
- urls.push(m[1].trim());
577
- while ((m = pathRegex.exec(cleaned)) !== null)
578
- files.push(m[1].trim());
579
- // Try to detect secret type from text
580
- const hasOAuth = cleaned.toLowerCase().includes('oauth');
581
- const secretType = hasOAuth ? 'Google OAuth Credential' : 'Secret';
582
- for (let i = 0; i < Math.max(urls.length, files.length); i++) {
583
- result.secrets.push({
584
- type: secretType,
585
- file: files[i] || '',
586
- unblockUrl: urls[i] || ''
587
- });
588
- }
589
- }
590
- // Check if all detected secrets are from Google OAuth installed apps (safe to include)
591
- if (result.secrets.length > 0) {
592
- result.allInstalledOAuth = result.secrets.every(s => {
593
- if (!s.file)
594
- return false;
595
- // Check by type name OR by inspecting the actual credential file
596
- const credType = detectCredentialsTypeFromFile(path.join(cwd, s.file));
597
- if (credType === 'installed')
598
- return true;
599
- // Also match if the type name mentions OAuth (even if file check failed)
600
- return s.type.toLowerCase().includes('oauth') && credType !== 'web';
601
- });
602
- }
603
- return result;
604
- }
605
- /** Try to auto-bypass push protection for installed OAuth secrets via gh API.
606
- * Returns true if all bypasses succeeded and push should be retried. */
607
- function tryAutoBypassPushProtection(ppInfo, cwd) {
608
- if (!ppInfo.allInstalledOAuth || ppInfo.secrets.length === 0)
609
- return false;
610
- // Check if gh CLI is available
611
- const ghCheck = runCommand('gh', ['auth', 'status'], { cwd, silent: true });
612
- if (!ghCheck.success)
613
- return false;
614
- // Extract repo owner/name from unblock URLs or git remote
615
- let repo = '';
616
- for (const s of ppInfo.secrets) {
617
- if (s.unblockUrl) {
618
- const repoMatch = s.unblockUrl.match(/github\.com\/([^/]+\/[^/]+)\//);
619
- if (repoMatch) {
620
- repo = repoMatch[1];
621
- break;
622
- }
623
- }
624
- }
625
- if (!repo)
626
- return false;
627
- let allBypassed = true;
628
- for (const s of ppInfo.secrets) {
629
- if (!s.unblockUrl) {
630
- allBypassed = false;
631
- continue;
632
- }
633
- // Extract placeholder_id from unblock URL (last path segment)
634
- const idMatch = s.unblockUrl.match(/\/unblock-secret\/(\S+)/);
635
- if (!idMatch) {
636
- allBypassed = false;
637
- continue;
638
- }
639
- const placeholderId = idMatch[1];
640
- console.log(colors.cyan(` Bypassing push protection for: ${s.type} (false_positive — installed app ID)...`));
641
- const bypassResult = runCommand('gh', [
642
- 'api', `repos/${repo}/secret-scanning/push-protection-bypasses`,
643
- '-X', 'POST',
644
- '-f', 'reason=false_positive',
645
- '-f', `placeholder_id=${placeholderId}`
646
- ], { cwd, silent: true });
647
- if (!bypassResult.success) {
648
- allBypassed = false;
649
- }
650
- }
651
- return allBypassed;
652
- }
653
- /** Display push protection guidance based on the type of secrets detected. */
654
- export function showPushProtectionGuidance(ppInfo) {
655
- console.error('');
656
- console.error(colors.yellow('GitHub Push Protection blocked the push — secrets detected in commits.'));
657
- if (ppInfo.allInstalledOAuth) {
658
- console.error(colors.cyan('These are Google OAuth credentials for a desktop/installed app.'));
659
- console.error(colors.cyan('The "client_secret" is just a public app identifier, not a real secret.'));
660
- console.error(colors.cyan('(Google docs: "the client_secret is obviously not treated as a secret")'));
661
- console.error('');
662
- console.error('To unblock, visit each URL below and allow the secret:');
663
- }
664
- else {
665
- console.error('');
666
- console.error('To unblock, review each secret and visit the URL to allow or remove it:');
667
- }
668
- for (const s of ppInfo.secrets) {
669
- console.error(` ${s.type}: ${s.file}`);
670
- if (s.unblockUrl) {
671
- console.error(` ${colors.cyan(s.unblockUrl)}`);
672
- }
673
- }
674
- console.error('');
675
- console.error('After unblocking, re-run the push or re-run npmglobalize.');
676
- }
677
- /** Push to git with push-protection detection and auto-bypass for installed OAuth.
678
- * Returns true if push succeeded (possibly after auto-bypass). */
679
- export function pushWithProtection(cwd, verbose) {
680
- let pushResult = runCommand('git', ['push'], { cwd, silent: true });
681
- if (pushResult.success) {
682
- if (verbose)
683
- console.log(colors.green(' ✓ Pushed to remote'));
684
- return true;
685
- }
686
- // Check for "Everything up-to-date" — git sometimes returns non-zero
687
- // alongside transient errors even though there's nothing to push
688
- const combined = (pushResult.output + ' ' + pushResult.stderr).toLowerCase();
689
- if (combined.includes('everything up-to-date')) {
690
- if (verbose)
691
- console.log(colors.green(' ✓ Already up-to-date'));
692
- return true;
693
- }
694
- // No upstream branch — auto-set and retry
695
- if (combined.includes('no upstream branch') || combined.includes('has no upstream')) {
696
- if (verbose)
697
- console.log(colors.yellow(' No upstream branch — setting upstream...'));
698
- const branchResult = runCommand('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, silent: true });
699
- const branch = branchResult.output.trim() || 'master';
700
- pushResult = runCommand('git', ['push', '--set-upstream', 'origin', branch], { cwd, silent: true });
701
- if (pushResult.success) {
702
- if (verbose)
703
- console.log(colors.green(' ✓ Pushed to remote (set upstream)'));
704
- return true;
705
- }
706
- }
707
- // Transient network errors (HTTP 408, RPC failed, unexpected disconnect) — retry once
708
- if (/rpc failed|curl \d+|unexpected disconnect|hung up unexpectedly/i.test(pushResult.stderr)) {
709
- console.log(colors.yellow('Git push failed with transient error — retrying...'));
710
- pushResult = runCommand('git', ['push'], { cwd, silent: true });
711
- if (pushResult.success) {
712
- if (verbose)
713
- console.log(colors.green(' ✓ Pushed to remote (retry)'));
714
- return true;
715
- }
716
- const retryCombo = (pushResult.output + ' ' + pushResult.stderr).toLowerCase();
717
- if (retryCombo.includes('everything up-to-date')) {
718
- if (verbose)
719
- console.log(colors.green(' ✓ Already up-to-date'));
720
- return true;
721
- }
722
- }
723
- const ppInfo = parsePushProtection(pushResult.stderr, cwd);
724
- if (!ppInfo.detected) {
725
- console.error(colors.red('Git push failed:'));
726
- if (pushResult.stderr)
727
- console.error(pushResult.stderr);
728
- return false;
729
- }
730
- // Try auto-bypass for installed OAuth credentials
731
- if (ppInfo.allInstalledOAuth && tryAutoBypassPushProtection(ppInfo, cwd)) {
732
- console.log(colors.green(' ✓ Auto-bypassed push protection (installed OAuth — not a real secret)'));
733
- const retryPush = runCommand('git', ['push'], { cwd, silent: true });
734
- if (retryPush.success) {
735
- if (verbose)
736
- console.log(colors.green(' ✓ Pushed to remote'));
737
- return true;
738
- }
739
- console.error(colors.yellow('Push still failed after bypass:'));
740
- if (retryPush.stderr)
741
- console.error(retryPush.stderr);
742
- return false;
743
- }
744
- // Can't auto-bypass — show manual guidance
745
- showPushProtectionGuidance(ppInfo);
746
- return false;
747
- }
748
- //# sourceMappingURL=git.js.map