@bobfrankston/npmglobalize 1.0.12

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