@bobfrankston/npmglobalize 1.0.12 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib.ts DELETED
@@ -1,993 +0,0 @@
1
- /**
2
- * npmglobalize - Transform file: dependencies to npm versions for publishing
3
- */
4
-
5
- import fs from 'fs';
6
- import path from 'path';
7
- import { execSync, spawnSync } from 'child_process';
8
- import readline from 'readline';
9
- import libversion from 'libnpmversion';
10
-
11
- /** ANSI color codes */
12
- const colors = {
13
- red: (text: string) => `\x1b[31m${text}\x1b[0m`,
14
- yellow: (text: string) => `\x1b[33m${text}\x1b[0m`,
15
- green: (text: string) => `\x1b[32m${text}\x1b[0m`,
16
- };
17
-
18
- /** Options for the globalize operation */
19
- export interface GlobalizeOptions {
20
- /** Bump type: patch (default), minor, major */
21
- bump?: 'patch' | 'minor' | 'major';
22
- /** Just transform, don't publish */
23
- applyOnly?: boolean;
24
- /** Restore from .dependencies */
25
- cleanup?: boolean;
26
- /** Global install after publish */
27
- install?: boolean;
28
- /** Also install in WSL */
29
- wsl?: boolean;
30
- /** Continue despite git errors */
31
- force?: boolean;
32
- /** Keep file: paths after publish (default true) */
33
- files?: boolean;
34
- /** Show what would happen */
35
- dryRun?: boolean;
36
- /** Suppress npm warnings (default true) */
37
- quiet?: boolean;
38
- /** Show verbose output */
39
- verbose?: boolean;
40
- /** Initialize git/npm if needed */
41
- init?: boolean;
42
- /** Git visibility: private (default) or public */
43
- gitVisibility?: 'private' | 'public';
44
- /** npm visibility: public (default) or private */
45
- npmVisibility?: 'private' | 'public';
46
- /** Custom commit message */
47
- message?: string;
48
- }
49
-
50
- const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
51
-
52
- /** Read and parse package.json from a directory */
53
- export function readPackageJson(dir: string): any {
54
- const pkgPath = path.join(dir, 'package.json');
55
- if (!fs.existsSync(pkgPath)) {
56
- throw new Error(`package.json not found: ${pkgPath}`);
57
- }
58
- return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
59
- }
60
-
61
- /** Read .globalize.jsonc config file */
62
- export function readConfig(dir: string): Partial<GlobalizeOptions> {
63
- const configPath = path.join(dir, '.globalize.jsonc');
64
- if (!fs.existsSync(configPath)) {
65
- return {};
66
- }
67
- try {
68
- const content = fs.readFileSync(configPath, 'utf-8');
69
- // Strip comments for JSON parsing (simple implementation)
70
- const jsonContent = content
71
- .split('\n')
72
- .map(line => line.replace(/\/\/.*$/, '').trim())
73
- .filter(line => line.length > 0)
74
- .join('\n');
75
- return JSON.parse(jsonContent);
76
- } catch (error: any) {
77
- console.warn(`Warning: Could not parse .globalize.jsonc config: ${error.message}`);
78
- return {};
79
- }
80
- }
81
-
82
- /** Write .globalize.jsonc config file */
83
- export function writeConfig(dir: string, config: Partial<GlobalizeOptions>): void {
84
- const configPath = path.join(dir, '.globalize.jsonc');
85
-
86
- // Build content with comments
87
- const lines = [
88
- '{',
89
- ' // Only set options that differ from defaults',
90
- ' // Defaults: bump=patch, files=true, quiet=true, gitVisibility=private, npmVisibility=public',
91
- ''
92
- ];
93
-
94
- // Add configured values
95
- for (const [key, value] of Object.entries(config)) {
96
- const jsonValue = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
97
- lines.push(` "${key}": ${jsonValue},`);
98
- }
99
-
100
- // Add commented examples for other options
101
- lines.push('');
102
- lines.push(' // Available options:');
103
- lines.push(' // "bump": "patch" | "minor" | "major"');
104
- lines.push(' // "install": true | false // Auto-install globally after publish');
105
- lines.push(' // "wsl": true | false // Also install in WSL');
106
- lines.push(' // "files": true | false // Keep file: paths after publish');
107
- lines.push(' // "force": true | false // Continue despite git errors');
108
- lines.push(' // "quiet": true | false // Suppress npm warnings');
109
- lines.push(' // "verbose": true | false // Show detailed output');
110
- lines.push(' // "gitVisibility": "private" | "public"');
111
- lines.push(' // "npmVisibility": "private" | "public"');
112
- lines.push('}');
113
-
114
- fs.writeFileSync(configPath, lines.join('\n') + '\n');
115
- }
116
-
117
- /** Write package.json to a directory */
118
- export function writePackageJson(dir: string, pkg: any): void {
119
- const pkgPath = path.join(dir, 'package.json');
120
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
121
- }
122
-
123
- /** Resolve a file: path to absolute path */
124
- export function resolveFilePath(fileRef: string, baseDir: string): string {
125
- const relativePath = fileRef.replace(/^file:/, '');
126
- return path.resolve(baseDir, relativePath);
127
- }
128
-
129
- /** Check if a dependency value is a file: reference */
130
- export function isFileRef(value: string): boolean {
131
- return typeof value === 'string' && value.startsWith('file:');
132
- }
133
-
134
- /** Get all file: dependencies from package.json */
135
- export function getFileRefs(pkg: any): Map<string, { key: string; name: string; value: string }> {
136
- const refs = new Map<string, { key: string; name: string; value: string }>();
137
- for (const key of DEP_KEYS) {
138
- if (!pkg[key]) continue;
139
- for (const [name, value] of Object.entries(pkg[key])) {
140
- if (isFileRef(value as string)) {
141
- refs.set(`${key}:${name}`, { key, name, value: value as string });
142
- }
143
- }
144
- }
145
- return refs;
146
- }
147
-
148
- /** Transform file: dependencies to npm versions */
149
- export function transformDeps(pkg: any, baseDir: string, verbose: boolean = false): boolean {
150
- let transformed = false;
151
-
152
- for (const key of DEP_KEYS) {
153
- if (!pkg[key]) continue;
154
-
155
- const dotKey = '.' + key;
156
- const hasFileRefs = Object.values(pkg[key]).some(v => isFileRef(v as string));
157
-
158
- if (!hasFileRefs) continue;
159
-
160
- // If .dependencies already exists, sync any new file: refs to it
161
- if (pkg[dotKey]) {
162
- for (const [name, value] of Object.entries(pkg[key])) {
163
- if (isFileRef(value as string)) {
164
- pkg[dotKey][name] = value;
165
- }
166
- }
167
- } else {
168
- // Backup original
169
- pkg[dotKey] = { ...pkg[key] };
170
- }
171
-
172
- // Transform file: refs to npm versions
173
- for (const [name, value] of Object.entries(pkg[key])) {
174
- if (isFileRef(value as string)) {
175
- const targetPath = resolveFilePath(value as string, baseDir);
176
- try {
177
- const targetPkg = readPackageJson(targetPath);
178
- const npmVersion = '^' + targetPkg.version;
179
- pkg[key][name] = npmVersion;
180
- if (verbose) {
181
- console.log(` ${name}: ${value} -> ${npmVersion}`);
182
- }
183
- transformed = true;
184
- }
185
- catch (error: any) {
186
- throw new Error(`Failed to resolve ${name} at ${targetPath}: ${error.message}`);
187
- }
188
- }
189
- }
190
- }
191
-
192
- return transformed;
193
- }
194
-
195
- /** Restore file: dependencies from .dependencies */
196
- export function restoreDeps(pkg: any, verbose: boolean = false): boolean {
197
- let restored = false;
198
-
199
- for (const key of DEP_KEYS) {
200
- const dotKey = '.' + key;
201
- if (pkg[dotKey]) {
202
- pkg[key] = pkg[dotKey];
203
- delete pkg[dotKey];
204
- restored = true;
205
- if (verbose) {
206
- console.log(` Restored ${key} from ${dotKey}`);
207
- }
208
- }
209
- }
210
-
211
- return restored;
212
- }
213
-
214
- /** Check if .dependencies exist (already transformed) */
215
- export function hasBackup(pkg: any): boolean {
216
- return DEP_KEYS.some(key => pkg['.' + key]);
217
- }
218
-
219
- /** Run a command and return success status */
220
- export function runCommand(cmd: string, args: string[], options: { silent?: boolean; cwd?: string } = {}): { success: boolean; output: string; stderr: string } {
221
- const { silent = false, cwd } = options;
222
- try {
223
- const result = spawnSync(cmd, args, {
224
- encoding: 'utf-8',
225
- stdio: silent ? 'pipe' : 'inherit',
226
- cwd,
227
- shell: true // Use shell for better Windows compatibility and output capture
228
- });
229
-
230
- // For non-silent commands, we can't capture output when using 'inherit'
231
- // So we return empty string for output, but the user sees it in the terminal
232
- if (!silent) {
233
- return {
234
- success: result.status === 0,
235
- output: '',
236
- stderr: ''
237
- };
238
- }
239
-
240
- const output = result.stdout || '';
241
- const stderr = result.stderr || '';
242
- return {
243
- success: result.status === 0,
244
- output: output,
245
- stderr: stderr
246
- };
247
- }
248
- catch (error: any) {
249
- return { success: false, output: '', stderr: error.message };
250
- }
251
- }
252
-
253
- /** Run a command and throw on failure */
254
- export function runCommandOrThrow(cmd: string, args: string[], options: { silent?: boolean; cwd?: string } = {}): string {
255
- // For commands that should throw, we need to capture output, so use silent=true
256
- const captureOptions = { ...options, silent: true };
257
- const result = spawnSync(cmd, args, {
258
- encoding: 'utf-8',
259
- stdio: 'pipe',
260
- cwd: captureOptions.cwd,
261
- shell: false
262
- });
263
-
264
- const stdout = result.stdout || '';
265
- const stderr = result.stderr || '';
266
- const output = stdout + stderr;
267
-
268
- if (result.status !== 0) {
269
- // Show the error output
270
- if (stderr.trim()) {
271
- console.error(colors.red(stderr.trim()));
272
- }
273
- if (stdout.trim()) {
274
- console.log(stdout.trim());
275
- }
276
- throw new Error(`Command failed with exit code ${result.status}: ${cmd} ${args.join(' ')}`);
277
- }
278
-
279
- return output;
280
- }
281
-
282
- /** Git status checks */
283
- export interface GitStatus {
284
- isRepo: boolean;
285
- hasRemote: boolean;
286
- hasUncommitted: boolean;
287
- hasUnpushed: boolean;
288
- hasMergeConflict: boolean;
289
- isDetachedHead: boolean;
290
- currentBranch: string;
291
- remoteBranch: string;
292
- }
293
-
294
- export function getGitStatus(cwd: string): GitStatus {
295
- const status: GitStatus = {
296
- isRepo: false,
297
- hasRemote: false,
298
- hasUncommitted: false,
299
- hasUnpushed: false,
300
- hasMergeConflict: false,
301
- isDetachedHead: false,
302
- currentBranch: '',
303
- remoteBranch: ''
304
- };
305
-
306
- // Check if git repo
307
- const gitDir = path.join(cwd, '.git');
308
- if (!fs.existsSync(gitDir)) {
309
- return status;
310
- }
311
- status.isRepo = true;
312
-
313
- // Check for merge conflicts
314
- const mergeHead = path.join(gitDir, 'MERGE_HEAD');
315
- status.hasMergeConflict = fs.existsSync(mergeHead);
316
-
317
- // Get branch info
318
- try {
319
- const branch = execSync('git rev-parse --abbrev-ref HEAD 2>nul', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
320
- status.currentBranch = branch;
321
- status.isDetachedHead = branch === 'HEAD';
322
- }
323
- catch (error: any) {
324
- // Ignore - might be no commits yet
325
- }
326
-
327
- // Check for remote
328
- try {
329
- const remote = execSync('git remote 2>nul', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
330
- status.hasRemote = remote.length > 0;
331
- }
332
- catch (error: any) {
333
- // Ignore
334
- }
335
-
336
- // Check for uncommitted changes
337
- try {
338
- const statusOutput = execSync('git status --porcelain 2>nul', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
339
- status.hasUncommitted = statusOutput.trim().length > 0;
340
- }
341
- catch (error: any) {
342
- // Ignore
343
- }
344
-
345
- // Check for unpushed commits
346
- if (status.hasRemote && !status.isDetachedHead && status.currentBranch) {
347
- try {
348
- const unpushed = execSync(`git log origin/${status.currentBranch}..HEAD --oneline 2>nul`, {
349
- cwd,
350
- encoding: 'utf-8',
351
- stdio: ['pipe', 'pipe', 'ignore']
352
- }).trim();
353
- status.hasUnpushed = unpushed.length > 0;
354
- }
355
- catch (error: any) {
356
- // Ignore - might not have tracking branch
357
- }
358
- }
359
-
360
- return status;
361
- }
362
-
363
- /** Validate package.json for release */
364
- export function validatePackageJson(pkg: any): string[] {
365
- const errors: string[] = [];
366
- const warnings: string[] = [];
367
-
368
- if (!pkg.repository) {
369
- errors.push('Missing repository field - required for npm packages');
370
- }
371
-
372
- if (!pkg.description) {
373
- warnings.push('Missing description field');
374
- }
375
-
376
- if (!pkg.name) {
377
- errors.push('Missing name field');
378
- }
379
-
380
- if (!pkg.version) {
381
- errors.push('Missing version field');
382
- }
383
-
384
- // Print warnings
385
- for (const w of warnings) {
386
- console.log(` Warning: ${w}`);
387
- }
388
-
389
- return errors;
390
- }
391
-
392
- /** Prompt user for confirmation */
393
- export async function confirm(message: string, defaultYes: boolean = false): Promise<boolean> {
394
- const rl = readline.createInterface({
395
- input: process.stdin,
396
- output: process.stdout
397
- });
398
-
399
- const suffix = defaultYes ? '[Y/n]' : '[y/N]';
400
-
401
- return new Promise((resolve) => {
402
- rl.question(`${message} ${suffix} `, (answer) => {
403
- rl.close();
404
- const a = answer.trim().toLowerCase();
405
- if (a === '') {
406
- resolve(defaultYes);
407
- } else {
408
- resolve(a === 'y' || a === 'yes');
409
- }
410
- });
411
- });
412
- }
413
-
414
- /** Check npm authentication status */
415
- function checkNpmAuth(): { authenticated: boolean; username?: string; error?: string } {
416
- try {
417
- // Use shell:true on Windows for better compatibility
418
- const result = spawnSync('npm', ['whoami'], {
419
- encoding: 'utf-8',
420
- shell: true,
421
- stdio: ['ignore', 'pipe', 'pipe']
422
- });
423
-
424
- if (result.status === 0 && result.stdout && result.stdout.trim()) {
425
- return { authenticated: true, username: result.stdout.trim() };
426
- }
427
-
428
- // Parse error message to determine the issue
429
- const stderr = (result.stderr || '').toLowerCase();
430
- if (stderr.includes('code eneedauth') || stderr.includes('not logged in')) {
431
- return { authenticated: false, error: 'not logged in' };
432
- } else if (stderr.includes('token') && (stderr.includes('expired') || stderr.includes('revoked'))) {
433
- return { authenticated: false, error: 'token expired' };
434
- } else if (stderr) {
435
- return { authenticated: false, error: 'unknown auth error' };
436
- } else {
437
- // npm whoami failed but with no stderr - likely auth issue
438
- return { authenticated: false, error: 'not logged in' };
439
- }
440
- } catch (error: any) {
441
- return { authenticated: false, error: error.message };
442
- }
443
- }
444
-
445
- /** Get authentication setup instructions */
446
- function getAuthInstructions(): string {
447
- const npmrcPath = path.join(process.env.USERPROFILE || process.env.HOME || '~', '.npmrc');
448
- return `
449
- Authentication Options:
450
-
451
- 1. ${colors.yellow('Create a Granular Access Token')} (recommended):
452
- - Go to: https://www.npmjs.com/settings/[username]/tokens
453
- - Click "Generate New Token" → "Granular Access Token"
454
- - Set permissions: ${colors.green('Read and write')} for packages
455
- - Enable: ${colors.green('Bypass 2FA requirement')} (if available)
456
- - Copy the token and save it securely
457
-
458
- 2. ${colors.yellow('Set token via environment variable')}:
459
- - Set: ${colors.green('NPM_TOKEN=npm_xxx...')}
460
- - Or run: ${colors.green('$env:NPM_TOKEN="npm_xxx..."')} (PowerShell)
461
-
462
- 3. ${colors.yellow('Set token in .npmrc')}:
463
- - Edit: ${colors.green(npmrcPath)}
464
- - Add: ${colors.green('//registry.npmjs.org/:_authToken=npm_xxx...')}
465
-
466
- 4. ${colors.yellow('Use classic login')} (may require 2FA):
467
- - Run: ${colors.green('npm login')}
468
- - Follow interactive prompts
469
-
470
- Note: npm now requires either 2FA or a granular token with bypass enabled.
471
- `;
472
- }
473
-
474
- /** Ensure .gitignore exists and includes node_modules */
475
- function ensureGitignore(cwd: string): void {
476
- const gitignorePath = path.join(cwd, '.gitignore');
477
- let content = '';
478
- let needsUpdate = false;
479
-
480
- // Read existing .gitignore if it exists
481
- if (fs.existsSync(gitignorePath)) {
482
- content = fs.readFileSync(gitignorePath, 'utf-8');
483
- // Check if node_modules is already ignored
484
- const lines = content.split('\n').map(l => l.trim());
485
- const hasNodeModules = lines.some(line =>
486
- line === 'node_modules' ||
487
- line === 'node_modules/' ||
488
- line === '/node_modules' ||
489
- line === '/node_modules/'
490
- );
491
- if (!hasNodeModules) {
492
- console.log(colors.yellow(' Warning: node_modules not found in .gitignore, adding it...'));
493
- needsUpdate = true;
494
- }
495
- } else {
496
- console.log(' Creating .gitignore...');
497
- needsUpdate = true;
498
- }
499
-
500
- // Update .gitignore if needed
501
- if (needsUpdate) {
502
- if (!content || content.trim() === '') {
503
- // Create new .gitignore with common patterns
504
- content = `node_modules/
505
- .env*
506
- *cert*/
507
- *certs*/
508
- *.pem
509
- *.key
510
- *.p12
511
- *.pfx
512
- configuration.json
513
- token
514
- tokens
515
- *.token
516
- cruft/
517
- prev/
518
- tests/
519
- *.log
520
- .DS_Store
521
- Thumbs.db
522
- `;
523
- } else {
524
- // Add node_modules to existing .gitignore
525
- if (!content.endsWith('\n')) {
526
- content += '\n';
527
- }
528
- content = 'node_modules/\n' + content;
529
- }
530
- fs.writeFileSync(gitignorePath, content);
531
- console.log(colors.green(' ✓ .gitignore updated'));
532
- }
533
- }
534
-
535
- /** Initialize git repository */
536
- export async function initGit(cwd: string, visibility: 'private' | 'public', dryRun: boolean): Promise<boolean> {
537
- const pkg = readPackageJson(cwd);
538
- const repoName = pkg.name.replace(/^@[^/]+\//, ''); // Remove scope
539
-
540
- console.log('Initializing git repository...');
541
-
542
- // Explicit visibility confirmation
543
- if (visibility === 'public') {
544
- const ok = await confirm('Create PUBLIC git repo - anyone can see your code. Continue?', false);
545
- if (!ok) {
546
- console.log('Aborted.');
547
- return false;
548
- }
549
- } else {
550
- console.log(`Creating PRIVATE git repo '${repoName}' (default)...`);
551
- }
552
-
553
- if (dryRun) {
554
- console.log(' [dry-run] Would run: git init');
555
- console.log(` [dry-run] Would run: gh repo create ${repoName} --${visibility} --source=. --push`);
556
- return true;
557
- }
558
-
559
- // Ensure .gitignore exists and includes node_modules
560
- ensureGitignore(cwd);
561
-
562
- // git init
563
- runCommandOrThrow('git', ['init'], { cwd });
564
- runCommandOrThrow('git', ['add', '-A'], { cwd });
565
- runCommandOrThrow('git', ['commit', '-m', 'Initial commit'], { cwd });
566
-
567
- // Create GitHub repo
568
- const visFlag = visibility === 'private' ? '--private' : '--public';
569
- runCommandOrThrow('gh', ['repo', 'create', repoName, visFlag, '--source=.', '--push'], { cwd });
570
-
571
- // Update package.json with repository field
572
- try {
573
- const remoteUrl = execSync('git remote get-url origin', { cwd, encoding: 'utf-8' }).trim();
574
- pkg.repository = {
575
- type: 'git',
576
- url: remoteUrl
577
- };
578
- writePackageJson(cwd, pkg);
579
- console.log(' Updated package.json with repository field');
580
- }
581
- catch (error: any) {
582
- console.log(' Warning: Could not update repository field');
583
- }
584
-
585
- return true;
586
- }
587
-
588
- /** Main globalize function */
589
- export async function globalize(cwd: string, options: GlobalizeOptions = {}): Promise<boolean> {
590
- const {
591
- bump = 'patch',
592
- applyOnly = false,
593
- cleanup = false,
594
- install = false,
595
- wsl = false,
596
- force = false,
597
- files = true,
598
- dryRun = false,
599
- quiet = true,
600
- verbose = false,
601
- init = false,
602
- gitVisibility = 'private',
603
- npmVisibility = 'public',
604
- message
605
- } = options;
606
-
607
- // Handle cleanup mode
608
- if (cleanup) {
609
- console.log('Restoring dependencies from backup...');
610
- const pkg = readPackageJson(cwd);
611
- if (restoreDeps(pkg, verbose)) {
612
- if (!dryRun) {
613
- writePackageJson(cwd, pkg);
614
- }
615
- console.log('Dependencies restored.');
616
- } else {
617
- console.log('No backup found - nothing to restore.');
618
- }
619
- return true;
620
- }
621
-
622
- // Check npm authentication early (unless dry-run or apply-only)
623
- if (!dryRun && !applyOnly) {
624
- const authStatus = checkNpmAuth();
625
- if (!authStatus.authenticated) {
626
- console.error(colors.red('\n✗ npm authentication required'));
627
- console.error('');
628
- if (authStatus.error === 'token expired') {
629
- console.error('Your npm access token has expired or been revoked.');
630
- console.error('');
631
- console.error('To fix this, run:');
632
- console.error(colors.yellow(' npm logout'));
633
- console.error(colors.yellow(' npm login'));
634
- } else if (authStatus.error === 'not logged in') {
635
- console.error('You are not logged in to npm.');
636
- console.error('');
637
- console.error('To fix this, run:');
638
- console.error(colors.yellow(' npm login'));
639
- } else {
640
- console.error(`Authentication error: ${authStatus.error}`);
641
- console.error('');
642
- console.error('Try running:');
643
- console.error(colors.yellow(' npm whoami'));
644
- }
645
- console.error('');
646
- console.error('After logging in, try running this command again.');
647
- return false;
648
- }
649
- if (verbose) {
650
- console.log(colors.green(`✓ Authenticated as ${authStatus.username}`));
651
- }
652
- }
653
-
654
- // Check git status
655
- const gitStatus = getGitStatus(cwd);
656
-
657
- // Handle init mode
658
- if (!gitStatus.isRepo) {
659
- console.log('No git repository found.');
660
- if (dryRun) {
661
- console.log(' [dry-run] Would initialize git repository');
662
- } else if (!init) {
663
- const ok = await confirm('Initialize git repository?', true);
664
- if (!ok) {
665
- console.log('Aborted. Run with --init to initialize.');
666
- return false;
667
- }
668
- const success = await initGit(cwd, gitVisibility, dryRun);
669
- if (!success) return false;
670
- } else {
671
- const success = await initGit(cwd, gitVisibility, dryRun);
672
- if (!success) return false;
673
- }
674
- } else if (init && !gitStatus.hasRemote) {
675
- // Git repo exists but no remote - create GitHub repo
676
- const success = await initGit(cwd, gitVisibility, dryRun);
677
- if (!success) return false;
678
- }
679
-
680
- // Re-check git status after potential init
681
- const currentGitStatus = getGitStatus(cwd);
682
-
683
- // Validate git state
684
- if (currentGitStatus.hasMergeConflict) {
685
- console.error(colors.red('ERROR: Merge conflict detected. Resolve before releasing.'));
686
- return false;
687
- }
688
-
689
- if (currentGitStatus.isDetachedHead && !force) {
690
- console.error(colors.red('ERROR: Detached HEAD state. Use --force to continue anyway.'));
691
- return false;
692
- }
693
-
694
- if (currentGitStatus.isDetachedHead && force) {
695
- console.log(colors.yellow('Warning: Detached HEAD state - continuing with --force'));
696
- }
697
-
698
- // Read package.json
699
- const pkg = readPackageJson(cwd);
700
-
701
- // Auto-add repository field if git exists but field is missing
702
- if (currentGitStatus.isRepo && currentGitStatus.hasRemote && !pkg.repository) {
703
- try {
704
- const remoteUrl = execSync('git remote get-url origin', { cwd, encoding: 'utf-8' }).trim();
705
- pkg.repository = {
706
- type: 'git',
707
- url: remoteUrl
708
- };
709
- writePackageJson(cwd, pkg);
710
- console.log('Added repository field from git remote.');
711
- }
712
- catch (error: any) {
713
- // Ignore - will be caught by validation
714
- }
715
- }
716
-
717
- // Validate package.json
718
- console.log('Validating package.json...');
719
- const errors = validatePackageJson(pkg);
720
- if (errors.length > 0) {
721
- // Check if missing repository and no remote - suggest init
722
- if (!pkg.repository && !currentGitStatus.hasRemote) {
723
- console.error(colors.red('ERROR: Missing repository field and no git remote configured.'));
724
- console.error(colors.red('Run with --init to set up GitHub repository.'));
725
- return false;
726
- }
727
-
728
- for (const e of errors) {
729
- console.error(colors.red(` ERROR: ${e}`));
730
- }
731
- if (!force) {
732
- return false;
733
- }
734
- console.log(colors.yellow('Continuing with --force despite errors...'));
735
- }
736
-
737
- // Check npm visibility - explicit confirmation
738
- if (npmVisibility === 'private') {
739
- if (!pkg.private) {
740
- const ok = await confirm('Mark npm package PRIVATE - will not publish to npm. Continue?', false);
741
- if (!ok) {
742
- console.log('Aborted.');
743
- return false;
744
- }
745
- pkg.private = true;
746
- }
747
- } else {
748
- // Default is public - just inform
749
- console.log(`Will publish '${pkg.name}' to PUBLIC npm registry (default).`);
750
- }
751
-
752
- // Transform dependencies
753
- console.log('Transforming file: dependencies...');
754
- const alreadyTransformed = hasBackup(pkg);
755
- if (alreadyTransformed) {
756
- console.log(' Already transformed (found .dependencies)');
757
- }
758
-
759
- const transformed = transformDeps(pkg, cwd, verbose);
760
- if (transformed) {
761
- console.log(' Dependencies transformed.');
762
- if (!dryRun) {
763
- writePackageJson(cwd, pkg);
764
- }
765
- } else if (!alreadyTransformed) {
766
- console.log(' No file: dependencies found.');
767
- }
768
-
769
- if (applyOnly) {
770
- console.log('Transform complete (--apply mode).');
771
- return true;
772
- }
773
-
774
- // Skip if private
775
- if (pkg.private) {
776
- console.log('Package is private - skipping npm publish.');
777
- return true;
778
- }
779
-
780
- // Check if there are changes to commit or a custom message
781
- if (!currentGitStatus.hasUncommitted && !message) {
782
- console.log('');
783
- console.log('No changes to commit and no custom message specified.');
784
- console.log('Nothing to release. Use -m "message" to force a release.');
785
- return true;
786
- }
787
-
788
- // Ensure node_modules is in .gitignore before any git operations
789
- if (currentGitStatus.isRepo && !dryRun) {
790
- ensureGitignore(cwd);
791
- }
792
-
793
- // Git operations
794
- if (currentGitStatus.hasUncommitted) {
795
- const commitMsg = message || 'Pre-release commit';
796
- console.log(`Committing changes: ${commitMsg}`);
797
- if (!dryRun) {
798
- runCommand('git', ['add', '-A'], { cwd });
799
- runCommand('git', ['commit', '-m', commitMsg], { cwd });
800
- } else {
801
- console.log(' [dry-run] Would commit changes');
802
- }
803
- }
804
-
805
- // Version bump
806
- console.log(`Bumping version (${bump})...`);
807
- if (!dryRun) {
808
- try {
809
- // Use libnpmversion - it will create commit and tag automatically
810
- const newVersion = await libversion(bump, {
811
- path: cwd
812
- });
813
-
814
- console.log(colors.green(`Version bumped to ${newVersion}`));
815
- } catch (error: any) {
816
- console.error(colors.red('ERROR: Version bump failed:'), error.message);
817
- if (!force) {
818
- return false;
819
- }
820
- console.log(colors.yellow('Continuing with --force...'));
821
- }
822
- } else {
823
- console.log(` [dry-run] Would run: npm version ${bump}`);
824
- }
825
-
826
- // Publish
827
- console.log('Publishing to npm...');
828
- const npmArgs = ['publish'];
829
- if (quiet) {
830
- npmArgs.push('--quiet');
831
- }
832
- if (!dryRun) {
833
- // Run with silent:true to capture the error message
834
- const publishResult = runCommand('npm', npmArgs, { cwd, silent: true });
835
- if (!publishResult.success) {
836
- console.error(colors.red('\nERROR: npm publish failed\n'));
837
-
838
- // Combine stdout and stderr for error analysis
839
- const errorOutput = (publishResult.output + '\n' + publishResult.stderr).toLowerCase();
840
-
841
- // Check for specific error types
842
- if (errorOutput.includes('e403') && (errorOutput.includes('two-factor') || errorOutput.includes('2fa'))) {
843
- console.error(colors.red('⚠ 2FA Required'));
844
- console.error('');
845
- console.error('Your npm account requires two-factor authentication or a');
846
- console.error('granular access token with "bypass 2FA" enabled.');
847
- console.error('');
848
- console.error(colors.yellow('Your options:'));
849
- console.error('');
850
- console.error('1. Enable 2FA on your npm account:');
851
- console.error(colors.green(' https://www.npmjs.com/settings/[username]/tfa'));
852
- console.error('');
853
- console.error('2. Create a granular access token:');
854
- console.error(colors.green(' https://www.npmjs.com/settings/[username]/tokens'));
855
- console.error(' - Select "Granular Access Token"');
856
- console.error(' - Set permissions: Read and write');
857
- console.error(' - Enable "Bypass 2FA requirement"');
858
- console.error(' - Then set via environment variable or .npmrc:');
859
- console.error(colors.green(' $env:NPM_TOKEN="npm_xxx..."'));
860
- console.error(' Or add to ~/.npmrc:');
861
- console.error(colors.green(' //registry.npmjs.org/:_authToken=npm_xxx...'));
862
- console.error('');
863
- } else if (errorOutput.includes('e404') || errorOutput.includes('not found')) {
864
- console.error(colors.yellow('Package not found or access denied.'));
865
- console.error('');
866
- console.error('Possible causes:');
867
- console.error(' • Package name is not available');
868
- console.error(' • You don\'t have permission to publish to this package');
869
- console.error(' • Scope (@username/) requires organization access');
870
- console.error('');
871
- } else if (errorOutput.includes('e402') || errorOutput.includes('payment required')) {
872
- console.error(colors.yellow('Payment required for private packages.'));
873
- console.error('');
874
- console.error('Private scoped packages require a paid npm account.');
875
- console.error('Either upgrade your account or make the package public.');
876
- console.error('');
877
- } else {
878
- // Show the actual error output
879
- if (publishResult.stderr) {
880
- console.error(publishResult.stderr);
881
- }
882
- if (publishResult.output) {
883
- console.error(publishResult.output);
884
- }
885
- console.error('');
886
- console.error(colors.yellow('Common causes:'));
887
- console.error(colors.yellow(' 1. Not logged in - run: npm login'));
888
- console.error(colors.yellow(' 2. Version already published'));
889
- console.error(colors.yellow(' 3. Authentication token expired'));
890
- }
891
-
892
- if (transformed) {
893
- console.log('Run --cleanup to restore file: dependencies');
894
- }
895
- return false;
896
- }
897
- console.log(colors.green('✓ Published to npm'));
898
- } else {
899
- console.log(` [dry-run] Would run: npm publish ${quiet ? '--quiet' : ''}`);
900
- }
901
-
902
- // Push to git
903
- if (currentGitStatus.hasRemote) {
904
- console.log('Pushing to git...');
905
- if (!dryRun) {
906
- runCommand('git', ['push'], { cwd });
907
- runCommand('git', ['push', '--tags'], { cwd });
908
- } else {
909
- console.log(' [dry-run] Would push to git');
910
- }
911
- }
912
-
913
- // Global install
914
- const pkgName = pkg.name;
915
- if (install) {
916
- console.log(`Installing globally: ${pkgName}...`);
917
- if (!dryRun) {
918
- runCommand('npm', ['install', '-g', pkgName], { cwd });
919
- } else {
920
- console.log(` [dry-run] Would run: npm install -g ${pkgName}`);
921
- }
922
- }
923
-
924
- if (wsl) {
925
- console.log(`Installing in WSL: ${pkgName}...`);
926
- if (!dryRun) {
927
- const wslResult = runCommand('wsl', ['npm', 'install', '-g', pkgName], { cwd });
928
- if (!wslResult.success) {
929
- console.log(' Warning: WSL install failed (is npm installed in WSL?)');
930
- }
931
- } else {
932
- console.log(` [dry-run] Would run: wsl npm install -g ${pkgName}`);
933
- }
934
- }
935
-
936
- // Finalize - restore file: paths if --files mode (default)
937
- if (files && transformed) {
938
- console.log('Restoring file: dependencies (--files mode)...');
939
- const finalPkg = readPackageJson(cwd);
940
- restoreDeps(finalPkg, verbose);
941
- if (!dryRun) {
942
- writePackageJson(cwd, finalPkg);
943
- // Commit the restore
944
- runCommand('git', ['add', 'package.json'], { cwd });
945
- runCommand('git', ['commit', '-m', 'Restore file: dependencies'], { cwd });
946
- if (currentGitStatus.hasRemote) {
947
- runCommand('git', ['push'], { cwd });
948
- }
949
- }
950
- } else if (!files) {
951
- console.log('Keeping npm versions (--nofiles mode).');
952
- }
953
-
954
- console.log('Done!');
955
-
956
- // Print summary
957
- console.log('');
958
- console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
959
- console.log(colors.green('✓ Release Summary'));
960
- console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
961
- const finalPkg = readPackageJson(cwd);
962
- console.log(` Package: ${colors.green(finalPkg.name)}`);
963
- console.log(` Version: ${colors.green('v' + finalPkg.version)}`);
964
- console.log(` Published: ${colors.green('✓')}`);
965
- console.log(` Git pushed: ${colors.green('✓')}`);
966
- if (install) {
967
- console.log(` Installed globally: ${colors.green('✓')}`);
968
- }
969
- if (wsl) {
970
- console.log(` Installed in WSL: ${colors.green('✓')}`);
971
- }
972
- if (files && transformed) {
973
- console.log(` Restored file: deps: ${colors.green('✓')}`);
974
- }
975
- console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
976
- console.log('');
977
- console.log(`Run: ${colors.green(finalPkg.name.includes('/') ? finalPkg.name.split('/')[1] : finalPkg.name)}`);
978
- console.log('');
979
-
980
- return true;
981
- }
982
-
983
- export default {
984
- globalize,
985
- transformDeps,
986
- restoreDeps,
987
- readPackageJson,
988
- writePackageJson,
989
- getGitStatus,
990
- validatePackageJson,
991
- confirm,
992
- initGit
993
- };