npm-workspaces-publish-tool 0.0.1

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/src/cli.ts ADDED
@@ -0,0 +1,783 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { readFileSync, writeFileSync } from 'fs';
4
+ import { createRequire } from 'module';
5
+ import { dirname, relative, resolve } from 'path';
6
+ import { cwd as getCwd } from 'process';
7
+ import { PackageJson } from 'type-fest';
8
+ import semver from 'semver';
9
+ import {
10
+ createDependencyMap,
11
+ getChangesBetweenRefs,
12
+ getPackageInfos,
13
+ getWorkspaces,
14
+ git,
15
+ PackageInfos,
16
+ } from 'workspace-tools';
17
+ import { execSync } from 'child_process';
18
+
19
+ type Workspace = ReturnType<typeof getWorkspaces>[number];
20
+ type Dependencies = Set<string>;
21
+ type DirtyStatus = 'new' | 'dirty' | 'unchanged';
22
+ type WorkspaceIssue = {
23
+ unstagedFiles: string[];
24
+ stagedFiles: string[];
25
+ untrackedFiles: string[];
26
+ };
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const pkg = require('../package.json');
30
+ const cwd = getCwd();
31
+ const dependancyTypes = [
32
+ 'dependencies',
33
+ 'devDependencies',
34
+ 'peerDependencies',
35
+ 'optionalDependencies',
36
+ ];
37
+
38
+ function getLastTag(cwd: string): string | null {
39
+ const result = git(['tag', '--list', 'v*', '--sort=-v:refname'], { cwd });
40
+ if (!result.success) return null;
41
+ const tags = result.stdout.split('\n').filter(Boolean);
42
+ return tags[0] || null;
43
+ }
44
+
45
+ function calculateWorkspaceInDegree(
46
+ packageInfos: PackageInfos,
47
+ dependencies: Map<string, Set<string>>
48
+ ) {
49
+ const inDegree = new Map<string, number>();
50
+
51
+ for (const pkgName of Object.keys(packageInfos)) {
52
+ inDegree.set(pkgName, 0);
53
+ }
54
+
55
+ for (const [_, deps] of dependencies) {
56
+ for (const dep of deps) {
57
+ inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
58
+ }
59
+ }
60
+
61
+ return inDegree;
62
+ }
63
+
64
+ function getReleaseOrderFromInDegree(
65
+ inDegree: Map<string, number>,
66
+ dependencies: Map<string, Set<string>>
67
+ ): string[] {
68
+ const queue = Array.from(inDegree.entries())
69
+ .filter(([_, count]) => count === 0)
70
+ .map(([pkg]) => pkg);
71
+
72
+ const order: string[] = [];
73
+
74
+ while (queue.length > 0) {
75
+ const current = queue.shift() as string;
76
+ order.push(current);
77
+
78
+ if (dependencies.has(current)) {
79
+ for (const dependent of dependencies.get(current) as Dependencies) {
80
+ const newCount = (inDegree.get(dependent) as number) - 1;
81
+ inDegree.set(dependent, newCount);
82
+ if (newCount === 0) {
83
+ queue.push(dependent);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ return order;
90
+ }
91
+
92
+ function getDirtyMap(
93
+ workspaces: ReturnType<typeof getWorkspaces>,
94
+ lastTag: string | null
95
+ ): Map<string, DirtyStatus> {
96
+ const dirtyMap = new Map<string, DirtyStatus>();
97
+
98
+ const changedFiles = lastTag
99
+ ? getChangesBetweenRefs(lastTag, 'HEAD', [], '', cwd)
100
+ : [];
101
+
102
+ for (const ws of workspaces) {
103
+ const isNew = !lastTag;
104
+ const isDirty = changedFiles.some(
105
+ (f) => f.startsWith(ws.path + '/') || f === ws.path
106
+ );
107
+
108
+ if (isNew) {
109
+ dirtyMap.set(ws.name, 'new');
110
+ } else if (isDirty) {
111
+ dirtyMap.set(ws.name, 'dirty');
112
+ } else {
113
+ dirtyMap.set(ws.name, 'unchanged');
114
+ }
115
+ }
116
+
117
+ return dirtyMap;
118
+ }
119
+
120
+ function getDirtyPackagesVersionChanges(
121
+ workspaces: ReturnType<typeof getWorkspaces>,
122
+ dirtyMap: Map<string, DirtyStatus>,
123
+ lastTag: string | null,
124
+ cwd: string
125
+ ): Map<
126
+ string,
127
+ {
128
+ oldVersion: string | null;
129
+ newVersion: string;
130
+ versionChanged: boolean;
131
+ versionIncremented: boolean;
132
+ }
133
+ > {
134
+ const result = new Map<
135
+ string,
136
+ {
137
+ oldVersion: string | null;
138
+ newVersion: string;
139
+ versionChanged: boolean;
140
+ versionIncremented: boolean;
141
+ }
142
+ >();
143
+
144
+ if (!lastTag) {
145
+ for (const ws of workspaces) {
146
+ const status = dirtyMap.get(ws.name);
147
+ if (status !== 'dirty' && status !== 'new') continue;
148
+
149
+ const pkgPath = resolve(cwd, ws.path, 'package.json');
150
+ let currentPkg: PackageJson;
151
+ try {
152
+ currentPkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
153
+ } catch (err) {
154
+ throw new Error(
155
+ `Failed to parse current package.json for package "${ws.name}" at path "${pkgPath}": ${err}`
156
+ );
157
+ }
158
+
159
+ const newVersion = currentPkg.version as string;
160
+ result.set(ws.name, {
161
+ oldVersion: null,
162
+ newVersion,
163
+ versionChanged: true,
164
+ versionIncremented: true,
165
+ });
166
+ }
167
+ return result;
168
+ }
169
+
170
+ for (const ws of workspaces) {
171
+ const status = dirtyMap.get(ws.name);
172
+ if (status !== 'dirty' && status !== 'new') continue;
173
+
174
+ const pkgPath = resolve(cwd, ws.path, 'package.json');
175
+
176
+ let currentPkg: PackageJson;
177
+ try {
178
+ currentPkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
179
+ } catch (err) {
180
+ throw new Error(
181
+ `Failed to parse current package.json for package "${ws.name}" at path "${pkgPath}": ${err}`
182
+ );
183
+ }
184
+
185
+ const newVersion = currentPkg.version as string;
186
+
187
+ if (status === 'new') {
188
+ result.set(ws.name, {
189
+ oldVersion: null,
190
+ newVersion,
191
+ versionChanged: true,
192
+ versionIncremented: true,
193
+ });
194
+ continue;
195
+ }
196
+
197
+ const previousPkgRaw = git(
198
+ ['show', `${lastTag}:${ws.path}/package.json`],
199
+ { cwd }
200
+ );
201
+
202
+ if (!previousPkgRaw.success) {
203
+ result.set(ws.name, {
204
+ oldVersion: null,
205
+ newVersion,
206
+ versionChanged: true,
207
+ versionIncremented: true,
208
+ });
209
+ continue;
210
+ }
211
+
212
+ let previousPkg: PackageJson;
213
+ try {
214
+ previousPkg = JSON.parse(previousPkgRaw.stdout);
215
+ } catch (err) {
216
+ throw new Error(
217
+ `Failed to parse package.json from git at tag "${lastTag}" for package "${ws.name}": ${err}`
218
+ );
219
+ }
220
+
221
+ const oldVersion = previousPkg.version as string;
222
+ const versionChanged = newVersion !== oldVersion;
223
+ const versionIncremented = semver.gt(newVersion, oldVersion);
224
+
225
+ result.set(ws.name, {
226
+ oldVersion,
227
+ newVersion,
228
+ versionChanged,
229
+ versionIncremented,
230
+ });
231
+ }
232
+
233
+ return result;
234
+ }
235
+
236
+ function checkForUnpushedCommits(cwd: string) {
237
+ const branchResult = git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
238
+ if (!branchResult.success) {
239
+ console.error('ERROR: Could not determine current branch.');
240
+ return false;
241
+ }
242
+
243
+ const currentBranch = branchResult.stdout.trim();
244
+ if (currentBranch === 'HEAD') {
245
+ console.error(
246
+ 'ERROR: You are in detached HEAD state. Cannot check for unpushed commits.'
247
+ );
248
+ return false;
249
+ }
250
+
251
+ const upstreamResult = git(['rev-parse', '--abbrev-ref', '@{u}'], { cwd });
252
+ if (!upstreamResult.success) {
253
+ console.error(
254
+ `ERROR: Current branch '${currentBranch}' has no upstream set.`
255
+ );
256
+ console.error(
257
+ 'Please set upstream with: git branch --set-upstream-to=origin/<branch>'
258
+ );
259
+ return false;
260
+ }
261
+
262
+ const upstream = upstreamResult.stdout.trim();
263
+ const unpushed = git(['log', `${upstream}..HEAD`, '--oneline'], { cwd });
264
+
265
+ if (unpushed.stdout.trim() !== '') {
266
+ console.error('ERROR: You have committed but unpushed changes:');
267
+ console.error(unpushed.stdout);
268
+ console.error(
269
+ `Please push your changes to '${upstream}' before publishing.`
270
+ );
271
+ return false;
272
+ }
273
+
274
+ return true;
275
+ }
276
+
277
+ function verifyCleanGitStatus(
278
+ workspaces: { name: string; path: string }[],
279
+ dirtyMap: Map<string, 'new' | 'dirty' | 'unchanged'>,
280
+ cwd: string
281
+ ): boolean {
282
+ const changedFilesRaw = git(['diff', '--name-only', 'HEAD'], { cwd })
283
+ .stdout.trim()
284
+ .split('\n')
285
+ .filter(Boolean);
286
+
287
+ const stagedFilesRaw = git(['diff', '--cached', '--name-only'], { cwd })
288
+ .stdout.trim()
289
+ .split('\n')
290
+ .filter(Boolean);
291
+
292
+ const unstagedFilesRaw = git(['diff', '--name-only'], { cwd })
293
+ .stdout.trim()
294
+ .split('\n')
295
+ .filter(Boolean);
296
+
297
+ const status = git(['status', '--porcelain'], { cwd })
298
+ .stdout.trim()
299
+ .split('\n')
300
+ .filter(Boolean);
301
+ const untrackedFilesRaw = status
302
+ .filter((line) => line.startsWith('??'))
303
+ .map((line) => line.slice(3));
304
+
305
+ const workspaceIssues = new Map<string, WorkspaceIssue>();
306
+
307
+ for (const ws of workspaces) {
308
+ workspaceIssues.set(ws.name, {
309
+ unstagedFiles: [],
310
+ stagedFiles: [],
311
+ untrackedFiles: [],
312
+ });
313
+ }
314
+
315
+ function findWorkspaceForFile(filePath: string) {
316
+ const absoluteFilePath = resolve(cwd, filePath).replace(/\\/g, '/');
317
+
318
+ for (const ws of workspaces) {
319
+ const wsPathNormalized = resolve(cwd, ws.path)
320
+ .replace(/\\/g, '/')
321
+ .replace(/\/$/, '');
322
+
323
+ if (
324
+ absoluteFilePath === wsPathNormalized ||
325
+ absoluteFilePath.startsWith(wsPathNormalized + '/')
326
+ ) {
327
+ return ws;
328
+ }
329
+ }
330
+
331
+ return undefined;
332
+ }
333
+
334
+ const changedFiles = new Set(changedFilesRaw);
335
+ const stagedFiles = new Set(stagedFilesRaw);
336
+ const unstagedFiles = new Set(unstagedFilesRaw);
337
+
338
+ for (const filePath of changedFiles) {
339
+ const ws = findWorkspaceForFile(filePath);
340
+ if (!ws) continue;
341
+
342
+ const status = dirtyMap.get(ws.name);
343
+ if (status !== 'new' && status !== 'dirty') continue;
344
+
345
+ const issues = workspaceIssues.get(ws.name) as WorkspaceIssue;
346
+
347
+ if (unstagedFiles.has(filePath)) {
348
+ issues.unstagedFiles.push(filePath);
349
+ } else if (stagedFiles.has(filePath)) {
350
+ issues.stagedFiles.push(filePath);
351
+ }
352
+ }
353
+
354
+ for (const filePath of untrackedFilesRaw) {
355
+ const ws = findWorkspaceForFile(filePath);
356
+ if (!ws) continue;
357
+ if (
358
+ dirtyMap.get(ws.name) !== 'new' &&
359
+ dirtyMap.get(ws.name) !== 'dirty'
360
+ )
361
+ continue;
362
+
363
+ (workspaceIssues.get(ws.name) as WorkspaceIssue).untrackedFiles.push(
364
+ filePath
365
+ );
366
+ }
367
+
368
+ let hasCriticalIssues = false;
369
+
370
+ for (const [wsName, issues] of workspaceIssues.entries()) {
371
+ const { unstagedFiles, stagedFiles, untrackedFiles } = issues;
372
+ const status = dirtyMap.get(wsName);
373
+
374
+ // Skip workspaces without issues
375
+ if (
376
+ unstagedFiles.length === 0 &&
377
+ stagedFiles.length === 0 &&
378
+ untrackedFiles.length === 0
379
+ ) {
380
+ continue;
381
+ }
382
+
383
+ let workspaceHasCriticalIssues = false;
384
+ const workspaceWarnings: string[] = [];
385
+
386
+ if (unstagedFiles.length > 0) {
387
+ workspaceHasCriticalIssues = true;
388
+ }
389
+
390
+ if (stagedFiles.length > 0) {
391
+ workspaceHasCriticalIssues = true;
392
+ }
393
+
394
+ // Handle untracked files
395
+ if (untrackedFiles.length > 0) {
396
+ let hasCriticalUntracked = false;
397
+
398
+ for (const f of untrackedFiles) {
399
+ if (f.includes('package.json')) {
400
+ hasCriticalUntracked = true;
401
+ }
402
+ }
403
+
404
+ if (status === 'new') {
405
+ // All untracked files are critical for new workspaces
406
+ workspaceHasCriticalIssues = true;
407
+ } else if (hasCriticalUntracked) {
408
+ // Untracked package.json is always critical
409
+ workspaceHasCriticalIssues = true;
410
+ } else if (status === 'dirty') {
411
+ // Non-package.json untracked files are warnings
412
+ workspaceWarnings.push(...untrackedFiles);
413
+ }
414
+ }
415
+
416
+ if (workspaceHasCriticalIssues) {
417
+ hasCriticalIssues = true;
418
+ console.error(
419
+ `Workspace '${wsName}' has critical issues preventing publish:`
420
+ );
421
+
422
+ if (unstagedFiles.length > 0) {
423
+ console.error(' āŒ Unstaged files:');
424
+ for (const f of unstagedFiles) {
425
+ console.error(` - ${f}`);
426
+ }
427
+ }
428
+
429
+ if (stagedFiles.length > 0) {
430
+ console.error(' āŒ Staged but uncommitted files:');
431
+ for (const f of stagedFiles) {
432
+ console.error(` - ${f}`);
433
+ }
434
+ }
435
+
436
+ if (untrackedFiles.length > 0) {
437
+ console.error(' āŒ Critical untracked files:');
438
+ for (const f of untrackedFiles) {
439
+ // Only show package.json files as critical
440
+ if (f.includes('package.json')) {
441
+ console.error(` - ${f} (must be committed)`);
442
+ } else if (status === 'new') {
443
+ console.error(
444
+ ` - ${f} (new workspace must commit all files)`
445
+ );
446
+ }
447
+ }
448
+ }
449
+ }
450
+
451
+ // Show non-critical warnings
452
+ if (workspaceWarnings.length > 0) {
453
+ console.warn(`āš ļø Workspace '${wsName}' has non-critical issues:`);
454
+ console.warn(
455
+ ' Untracked files (allowed but should be in .gitignore):'
456
+ );
457
+ for (const f of workspaceWarnings) {
458
+ console.warn(` - ${f}`);
459
+ }
460
+ }
461
+ }
462
+
463
+ const rootPackageJsonPath = 'package.json';
464
+ if (changedFiles.has(rootPackageJsonPath)) {
465
+ if (unstagedFiles.has(rootPackageJsonPath)) {
466
+ console.error(
467
+ `āŒ Root package.json has unstaged changes. Please stage or discard them.`
468
+ );
469
+ hasCriticalIssues = true;
470
+ }
471
+ if (stagedFiles.has(rootPackageJsonPath)) {
472
+ console.error(
473
+ `āŒ Root package.json is staged but not committed. Please commit it.`
474
+ );
475
+ hasCriticalIssues = true;
476
+ }
477
+ }
478
+
479
+ if (hasCriticalIssues) {
480
+ console.error(
481
+ 'Please commit or stash the above changes before publishing.'
482
+ );
483
+ return false;
484
+ }
485
+
486
+ if (!checkForUnpushedCommits(cwd)) {
487
+ return false;
488
+ }
489
+
490
+ return true;
491
+ }
492
+
493
+ function validatePublish() {
494
+ const lastMonoRepoTag = getLastTag(cwd);
495
+ const workspaces = getWorkspaces(cwd);
496
+ const packageInfos = getPackageInfos(cwd);
497
+ const { dependencies } = createDependencyMap(packageInfos);
498
+ const inDegree = calculateWorkspaceInDegree(packageInfos, dependencies);
499
+ const releaseOrder = getReleaseOrderFromInDegree(inDegree, dependencies);
500
+ const dirtyMap = getDirtyMap(workspaces, lastMonoRepoTag);
501
+ const dirtyVersionChanges = getDirtyPackagesVersionChanges(
502
+ workspaces,
503
+ dirtyMap,
504
+ lastMonoRepoTag,
505
+ cwd
506
+ );
507
+
508
+ console.log('šŸ—ļø Building packages...');
509
+ for (const pkgName of releaseOrder) {
510
+ const pkgInfo = packageInfos[pkgName];
511
+ if (!pkgInfo.scripts?.build) continue;
512
+
513
+ const pkgDir = dirname(pkgInfo.packageJsonPath);
514
+ console.log(`\nšŸ—ļø Building ${pkgName}...`);
515
+ try {
516
+ execSync('npm run build', {
517
+ cwd: pkgDir,
518
+ stdio: 'inherit',
519
+ env: { ...process.env, FORCE_COLOR: '1' },
520
+ });
521
+ } catch (error) {
522
+ console.error(`āŒ Build failed for ${pkgName}:`, error);
523
+ process.exit(1);
524
+ }
525
+ }
526
+
527
+ console.log('#ļøāƒ£ Validating versions...\n');
528
+
529
+ let hasError = false;
530
+
531
+ for (const [
532
+ pkgName,
533
+ { oldVersion, newVersion, versionChanged, versionIncremented },
534
+ ] of dirtyVersionChanges.entries()) {
535
+ if (!semver.valid(newVersion)) {
536
+ console.error(
537
+ `āŒ Package "${pkgName}" has invalid version: "${newVersion}"`
538
+ );
539
+ hasError = true;
540
+ continue;
541
+ }
542
+
543
+ if (!versionChanged) {
544
+ console.error(
545
+ `āŒ Package "${pkgName}" was modified but version unchanged (${newVersion})`
546
+ );
547
+ hasError = true;
548
+
549
+ const ws = workspaces.find((w) => w.name === pkgName) as Workspace;
550
+ const changedFiles = lastMonoRepoTag
551
+ ? getChangesBetweenRefs(
552
+ lastMonoRepoTag,
553
+ 'HEAD',
554
+ [],
555
+ ws.path,
556
+ cwd
557
+ )
558
+ : [];
559
+
560
+ if (changedFiles.length > 0) {
561
+ console.error(` Changed files in "${pkgName}":`);
562
+ for (const file of changedFiles) {
563
+ console.error(` - ${relative(ws.path, file)}`);
564
+ }
565
+ }
566
+ continue;
567
+ }
568
+
569
+ if (oldVersion && !versionIncremented) {
570
+ console.error(
571
+ `āŒ Package "${pkgName}" version not incremented: ${oldVersion} -> ${newVersion}`
572
+ );
573
+ hasError = true;
574
+ }
575
+ }
576
+
577
+ if (hasError) {
578
+ console.error('\nFix the above issues before publishing.\n');
579
+ process.exit(1);
580
+ }
581
+
582
+ console.log('šŸ—ƒļø Validating git status...\n');
583
+
584
+ if (!verifyCleanGitStatus(workspaces, dirtyMap, cwd)) {
585
+ process.exit(1);
586
+ }
587
+
588
+ console.log('šŸ“ Publish summary:\n');
589
+ const packagesToRelease: string[] = [];
590
+
591
+ for (const pkgName of releaseOrder) {
592
+ const status = dirtyMap.get(pkgName);
593
+ const versionInfo = dirtyVersionChanges.get(pkgName);
594
+
595
+ if (status === 'new') {
596
+ packagesToRelease.push(pkgName);
597
+ const ws = workspaces.find((w) => w.name === pkgName);
598
+ if (!ws) continue;
599
+ const currentVersion = JSON.parse(
600
+ readFileSync(resolve(cwd, ws.path, 'package.json'), 'utf8')
601
+ ).version;
602
+ console.log(`šŸ†• ${pkgName} @ ${currentVersion} (new)`);
603
+ } else if (status === 'dirty' && versionInfo?.versionIncremented) {
604
+ packagesToRelease.push(pkgName);
605
+ console.log(
606
+ `ā¬†ļø ${pkgName}: ${versionInfo.oldVersion} → ${versionInfo.newVersion}`
607
+ );
608
+ } else {
609
+ const ws = workspaces.find((w) => w.name === pkgName);
610
+ if (!ws) continue;
611
+ const currentVersion = JSON.parse(
612
+ readFileSync(resolve(cwd, ws.path, 'package.json'), 'utf8')
613
+ ).version;
614
+ console.log(`āœ”ļø ${pkgName} @ ${currentVersion} (unchanged)`);
615
+ }
616
+ }
617
+
618
+ return { releaseOrder, packageInfos, packagesToRelease };
619
+ }
620
+
621
+ function replaceWorkspaceDepsWithVersions(
622
+ packageInfos: PackageInfos,
623
+ packagesToUpdate: string[]
624
+ ): Map<string, string> {
625
+ const originals = new Map<string, string>();
626
+
627
+ for (const pkgName of packagesToUpdate) {
628
+ const pkgInfo = packageInfos[pkgName];
629
+ const packageJsonPath = pkgInfo.packageJsonPath;
630
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
631
+
632
+ originals.set(pkgName, JSON.stringify(packageJson));
633
+
634
+ let changed = false;
635
+
636
+ for (const depType of dependancyTypes) {
637
+ const deps = packageJson[depType];
638
+ if (!deps) continue;
639
+
640
+ for (const [depName, currentSpec] of Object.entries<string>(deps)) {
641
+ if (currentSpec === '*' && packageInfos[depName]) {
642
+ const newVersion =
643
+ packageInfos[depName].packageJson.version;
644
+ deps[depName] = newVersion;
645
+ changed = true;
646
+ console.log(
647
+ `šŸ†™ Updated ${pkgName}'s ${depType} for ${depName} to ${newVersion}`
648
+ );
649
+ }
650
+ }
651
+ }
652
+
653
+ if (changed) {
654
+ writeFileSync(
655
+ packageJsonPath,
656
+ JSON.stringify(packageJson, null, 2)
657
+ );
658
+ }
659
+ }
660
+
661
+ return originals;
662
+ }
663
+
664
+ function restoreWorkspaceDepsToStar(
665
+ packageInfos: PackageInfos,
666
+ packagesToRestore: string[],
667
+ originals: Map<string, string>
668
+ ) {
669
+ for (const pkgName of packagesToRestore) {
670
+ const original = originals.get(pkgName);
671
+ if (original) {
672
+ const pkgInfo = packageInfos[pkgName];
673
+ writeFileSync(pkgInfo.packageJsonPath, original);
674
+ console.log(`ā™»ļø Restored dependencies for ${pkgName}`);
675
+ }
676
+ }
677
+ }
678
+
679
+ function runNpmPublishInReleaseOrder(
680
+ releaseOrder: string[],
681
+ packageInfos: PackageInfos,
682
+ packagesToRelease: string[]
683
+ ) {
684
+ for (const packageName of releaseOrder) {
685
+ if (!packagesToRelease.includes(packageName)) continue;
686
+
687
+ const pkgInfo = packageInfos[packageName];
688
+ const pkgDir = dirname(pkgInfo.packageJsonPath);
689
+ console.log(`\nšŸ“¦ Publishing ${packageName} from ${pkgDir}...`);
690
+ try {
691
+ execSync('npm publish', {
692
+ cwd: pkgDir,
693
+ stdio: 'inherit',
694
+ env: { ...process.env, FORCE_COLOR: '1' },
695
+ });
696
+ console.log(`āœ… Successfully published ${packageName}`);
697
+ } catch (error) {
698
+ console.error(`āŒ Failed to publish ${packageName}:`, error);
699
+ throw error;
700
+ }
701
+ }
702
+ }
703
+
704
+ function printHeader() {
705
+ console.log(`
706
+ _ _ _ _
707
+ ( ) ( )(_) ( )
708
+ ____ _ _ _ ___ ___ _ _ | |_ | | _ __ | |_
709
+ ( __ )( V V )(___)( o \\( U )( o \\( )( )(_' ( _ )
710
+ /_\\/_\\ \\_^_/ / __//___\\/___//_\\/_\\/__)/_\\||
711
+ |_|
712
+ `);
713
+ console.log(
714
+ `šŸš€ ${pkg.name} v${pkg.version} - Npm Monorepo Publishing Suite\n`
715
+ );
716
+ }
717
+
718
+ program.name(pkg.name).description(pkg.description).version(pkg.version);
719
+
720
+ program
721
+ .description('Publish packages')
722
+ .option('--dry-run', 'Run without making changes')
723
+ .action((options) => {
724
+ printHeader();
725
+ const { packageInfos, releaseOrder, packagesToRelease } =
726
+ validatePublish();
727
+ const dryRun = options.dryRun;
728
+
729
+ if (packagesToRelease.length === 0) {
730
+ console.log('  ̄\\_(惄)_/ ̄ No packages to publish');
731
+ return;
732
+ }
733
+
734
+ let originalPackageJsons: Map<string, string> | null = null;
735
+
736
+ try {
737
+ if (!dryRun) {
738
+ console.log(
739
+ '\nšŸ†™ Updating workspace dependencies to exact versions...'
740
+ );
741
+ originalPackageJsons = replaceWorkspaceDepsWithVersions(
742
+ packageInfos,
743
+ packagesToRelease
744
+ );
745
+ } else {
746
+ console.log(
747
+ '\n[dry-run] šŸ†™ Would update workspace dependencies to exact versions'
748
+ );
749
+ }
750
+
751
+ if (!dryRun) {
752
+ console.log('\nšŸ“¦ Publishing packages...');
753
+ runNpmPublishInReleaseOrder(
754
+ releaseOrder,
755
+ packageInfos,
756
+ packagesToRelease
757
+ );
758
+ console.log('\nšŸŽ‰ All packages published successfully!');
759
+ } else {
760
+ console.log('\n[dry-run] šŸ“¦ Would publish packages in order:');
761
+ packagesToRelease.forEach((pkg) => console.log(`- ${pkg}`));
762
+ }
763
+ } catch (error) {
764
+ console.error('\n āŒ Publishing failed:', error);
765
+ process.exit(1);
766
+ } finally {
767
+ if (!dryRun && originalPackageJsons) {
768
+ console.log('\nā™»ļø Restoring workspace dependencies...');
769
+ restoreWorkspaceDepsToStar(
770
+ packageInfos,
771
+ packagesToRelease,
772
+ originalPackageJsons
773
+ );
774
+ console.log('ā™»ļø Restoration complete.');
775
+ } else if (dryRun) {
776
+ console.log(
777
+ '\n[dry-run] ā™»ļø Would restore workspace dependencies'
778
+ );
779
+ }
780
+ }
781
+ });
782
+
783
+ program.parse(process.argv);