@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/README.md +84 -15
- package/cli.js +5 -0
- package/lib.js +5 -5
- package/package.json +3 -3
- package/lib/config.d.ts +0 -23
- package/lib/config.js +0 -163
- package/lib/deps.d.ts +0 -42
- package/lib/deps.js +0 -298
- package/lib/git.d.ts +0 -91
- package/lib/git.js +0 -748
- package/lib/ignore.d.ts +0 -24
- package/lib/ignore.js +0 -346
- package/lib/npm.d.ts +0 -33
- package/lib/npm.js +0 -306
- package/lib/types.d.ts +0 -135
- package/lib/types.js +0 -80
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
|