@geekmidas/cli 1.7.0 → 1.9.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/{HostingerProvider-BiXdHjiq.cjs → HostingerProvider-CEsQbmpY.cjs} +1 -1
  3. package/dist/{HostingerProvider-BiXdHjiq.cjs.map → HostingerProvider-CEsQbmpY.cjs.map} +1 -1
  4. package/dist/{HostingerProvider-402UdK89.mjs → HostingerProvider-DkahM5AP.mjs} +1 -1
  5. package/dist/{HostingerProvider-402UdK89.mjs.map → HostingerProvider-DkahM5AP.mjs.map} +1 -1
  6. package/dist/{LocalStateProvider-BDm7ZqJo.mjs → LocalStateProvider-DXIwWb7k.mjs} +1 -1
  7. package/dist/{LocalStateProvider-BDm7ZqJo.mjs.map → LocalStateProvider-DXIwWb7k.mjs.map} +1 -1
  8. package/dist/{LocalStateProvider-CdspeSVL.cjs → LocalStateProvider-Roi202l7.cjs} +1 -1
  9. package/dist/{LocalStateProvider-CdspeSVL.cjs.map → LocalStateProvider-Roi202l7.cjs.map} +1 -1
  10. package/dist/{Route53Provider-kfJ77LmL.cjs → Route53Provider-BqXeHzuc.cjs} +1 -1
  11. package/dist/{Route53Provider-kfJ77LmL.cjs.map → Route53Provider-BqXeHzuc.cjs.map} +1 -1
  12. package/dist/{Route53Provider-DbBo7Uz5.mjs → Route53Provider-Ckq_n5Be.mjs} +1 -1
  13. package/dist/{Route53Provider-DbBo7Uz5.mjs.map → Route53Provider-Ckq_n5Be.mjs.map} +1 -1
  14. package/dist/{SSMStateProvider-DGrqYll0.cjs → SSMStateProvider-BReQA5re.cjs} +1 -1
  15. package/dist/{SSMStateProvider-DGrqYll0.cjs.map → SSMStateProvider-BReQA5re.cjs.map} +1 -1
  16. package/dist/{SSMStateProvider-DT0WV-E_.mjs → SSMStateProvider-wddd0_-d.mjs} +1 -1
  17. package/dist/{SSMStateProvider-DT0WV-E_.mjs.map → SSMStateProvider-wddd0_-d.mjs.map} +1 -1
  18. package/dist/{backup-provisioner-BIArpmTr.mjs → backup-provisioner-BAExdDtc.mjs} +1 -1
  19. package/dist/{backup-provisioner-BIArpmTr.mjs.map → backup-provisioner-BAExdDtc.mjs.map} +1 -1
  20. package/dist/{backup-provisioner-B5e-F6zX.cjs → backup-provisioner-C8VK63I-.cjs} +1 -1
  21. package/dist/{backup-provisioner-B5e-F6zX.cjs.map → backup-provisioner-C8VK63I-.cjs.map} +1 -1
  22. package/dist/{bundler-DgXsOSxc.mjs → bundler-BxHyDhdt.mjs} +1 -1
  23. package/dist/{bundler-DgXsOSxc.mjs.map → bundler-BxHyDhdt.mjs.map} +1 -1
  24. package/dist/{bundler-tHLLwYuU.cjs → bundler-CuMIfXw5.cjs} +1 -1
  25. package/dist/{bundler-tHLLwYuU.cjs.map → bundler-CuMIfXw5.cjs.map} +1 -1
  26. package/dist/config.d.mts +2 -2
  27. package/dist/{index-C-KxSGGK.d.mts → index-BVNXOydm.d.mts} +2 -2
  28. package/dist/{index-C-KxSGGK.d.mts.map → index-BVNXOydm.d.mts.map} +1 -1
  29. package/dist/index.cjs +1019 -551
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +1017 -549
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/openapi.d.mts +1 -1
  34. package/dist/sync-BOS0jKLn.cjs +93 -0
  35. package/dist/sync-BOS0jKLn.cjs.map +1 -0
  36. package/dist/sync-BnqNNc6O.mjs +3 -0
  37. package/dist/sync-BxFB34zW.cjs +4 -0
  38. package/dist/sync-CHfhmXF3.mjs +76 -0
  39. package/dist/sync-CHfhmXF3.mjs.map +1 -0
  40. package/dist/{types-CZg5iUgD.d.mts → types-eTlj5f2M.d.mts} +1 -1
  41. package/dist/{types-CZg5iUgD.d.mts.map → types-eTlj5f2M.d.mts.map} +1 -1
  42. package/dist/workspace/index.d.mts +2 -2
  43. package/package.json +4 -4
  44. package/src/dev/index.ts +1 -1
  45. package/src/generators/SubscriberGenerator.ts +1 -0
  46. package/src/index.ts +93 -0
  47. package/src/init/index.ts +4 -23
  48. package/src/init/utils.ts +103 -2
  49. package/src/init/versions.ts +1 -1
  50. package/src/secrets/index.ts +20 -1
  51. package/src/secrets/sync.ts +136 -0
  52. package/src/setup/fullstack-secrets.ts +121 -0
  53. package/src/setup/index.ts +212 -0
  54. package/src/test/__tests__/web.spec.ts +1 -1
  55. package/src/upgrade/__tests__/index.spec.ts +354 -0
  56. package/src/upgrade/index.ts +253 -0
@@ -0,0 +1,354 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { mkdir, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { HttpResponse, http } from 'msw';
7
+ import { setupServer } from 'msw/node';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { upgradeCommand } from '../index';
10
+
11
+ vi.mock('node:child_process', () => ({
12
+ execSync: vi.fn(),
13
+ }));
14
+
15
+ const NPM_REGISTRY = 'https://registry.npmjs.org';
16
+
17
+ const server = setupServer();
18
+
19
+ function writePackageJson(dir: string, content: Record<string, unknown>) {
20
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(content, null, 2));
21
+ }
22
+
23
+ describe('upgradeCommand', () => {
24
+ let tempDir: string;
25
+ let originalCwd: string;
26
+
27
+ beforeEach(async () => {
28
+ tempDir = join(tmpdir(), `gkm-upgrade-test-${Date.now()}`);
29
+ await mkdir(tempDir, { recursive: true });
30
+ originalCwd = process.cwd();
31
+ process.chdir(tempDir);
32
+ server.listen({ onUnhandledRequest: 'bypass' });
33
+ vi.mocked(execSync).mockReset();
34
+ });
35
+
36
+ afterEach(async () => {
37
+ process.chdir(originalCwd);
38
+ server.resetHandlers();
39
+ server.close();
40
+ await rm(tempDir, { recursive: true, force: true });
41
+ });
42
+
43
+ it('should report no packages found when none exist', async () => {
44
+ writePackageJson(tempDir, {
45
+ name: 'test-project',
46
+ dependencies: { lodash: '^4.0.0' },
47
+ });
48
+ // Create a lockfile so detectPackageManager finds a root
49
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
50
+
51
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
52
+
53
+ await upgradeCommand();
54
+
55
+ const output = logSpy.mock.calls.flat().join('\n');
56
+ expect(output).toContain('No @geekmidas packages found');
57
+
58
+ logSpy.mockRestore();
59
+ });
60
+
61
+ it('should detect workspace refs and mark them as workspace status', async () => {
62
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
63
+ writePackageJson(tempDir, {
64
+ name: 'test-monorepo',
65
+ workspaces: ['packages/*'],
66
+ dependencies: {
67
+ '@geekmidas/constructs': 'workspace:*',
68
+ },
69
+ });
70
+
71
+ const pkgDir = join(tempDir, 'packages', 'api');
72
+ await mkdir(pkgDir, { recursive: true });
73
+ writePackageJson(pkgDir, {
74
+ name: '@test/api',
75
+ dependencies: {
76
+ '@geekmidas/auth': 'workspace:~',
77
+ },
78
+ });
79
+
80
+ server.use(
81
+ http.get(`${NPM_REGISTRY}/@geekmidas/constructs/latest`, () => {
82
+ return HttpResponse.json({ version: '1.1.1' });
83
+ }),
84
+ http.get(`${NPM_REGISTRY}/@geekmidas/auth/latest`, () => {
85
+ return HttpResponse.json({ version: '1.0.0' });
86
+ }),
87
+ );
88
+
89
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
90
+
91
+ await upgradeCommand();
92
+
93
+ const output = logSpy.mock.calls.flat().join('\n');
94
+ expect(output).toContain('workspace');
95
+ expect(output).toContain('All @geekmidas packages are up to date');
96
+
97
+ logSpy.mockRestore();
98
+ });
99
+
100
+ it('should identify packages that need upgrade', async () => {
101
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
102
+ writePackageJson(tempDir, {
103
+ name: 'test-project',
104
+ dependencies: {
105
+ '@geekmidas/constructs': '^1.0.0',
106
+ '@geekmidas/auth': '~1.0.0',
107
+ },
108
+ });
109
+
110
+ server.use(
111
+ http.get(`${NPM_REGISTRY}/@geekmidas/constructs/latest`, () => {
112
+ return HttpResponse.json({ version: '1.2.0' });
113
+ }),
114
+ http.get(`${NPM_REGISTRY}/@geekmidas/auth/latest`, () => {
115
+ return HttpResponse.json({ version: '1.1.0' });
116
+ }),
117
+ );
118
+
119
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
120
+
121
+ await upgradeCommand({ dryRun: true });
122
+
123
+ const output = logSpy.mock.calls.flat().join('\n');
124
+ expect(output).toContain('⬆ upgrade');
125
+ expect(output).toContain('2 package(s) can be upgraded');
126
+ expect(output).toContain('--dry-run: No changes made');
127
+ expect(output).toContain('npm update');
128
+
129
+ logSpy.mockRestore();
130
+ });
131
+
132
+ it('should report up-to-date when versions match', async () => {
133
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
134
+ writePackageJson(tempDir, {
135
+ name: 'test-project',
136
+ dependencies: {
137
+ '@geekmidas/errors': '1.0.0',
138
+ },
139
+ });
140
+
141
+ server.use(
142
+ http.get(`${NPM_REGISTRY}/@geekmidas/errors/latest`, () => {
143
+ return HttpResponse.json({ version: '1.0.0' });
144
+ }),
145
+ );
146
+
147
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
148
+
149
+ await upgradeCommand();
150
+
151
+ const output = logSpy.mock.calls.flat().join('\n');
152
+ expect(output).toContain('✓ up-to-date');
153
+ expect(output).toContain('All @geekmidas packages are up to date');
154
+
155
+ logSpy.mockRestore();
156
+ });
157
+
158
+ it('should execute upgrade command when not dry-run', async () => {
159
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
160
+ writePackageJson(tempDir, {
161
+ name: 'test-project',
162
+ dependencies: {
163
+ '@geekmidas/errors': '^1.0.0',
164
+ },
165
+ });
166
+
167
+ server.use(
168
+ http.get(`${NPM_REGISTRY}/@geekmidas/errors/latest`, () => {
169
+ return HttpResponse.json({ version: '2.0.0' });
170
+ }),
171
+ );
172
+
173
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
174
+
175
+ await upgradeCommand();
176
+
177
+ expect(execSync).toHaveBeenCalledWith(
178
+ expect.stringContaining('npm update @geekmidas/errors'),
179
+ expect.objectContaining({ stdio: 'inherit' }),
180
+ );
181
+
182
+ logSpy.mockRestore();
183
+ });
184
+
185
+ it('should use pnpm update -r when pnpm is detected', async () => {
186
+ writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
187
+ writePackageJson(tempDir, {
188
+ name: 'test-project',
189
+ dependencies: {
190
+ '@geekmidas/logger': '^1.0.0',
191
+ },
192
+ });
193
+
194
+ server.use(
195
+ http.get(`${NPM_REGISTRY}/@geekmidas/logger/latest`, () => {
196
+ return HttpResponse.json({ version: '2.0.0' });
197
+ }),
198
+ );
199
+
200
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
201
+
202
+ await upgradeCommand();
203
+
204
+ expect(execSync).toHaveBeenCalledWith(
205
+ expect.stringContaining('pnpm update -r @geekmidas/logger --latest'),
206
+ expect.anything(),
207
+ );
208
+
209
+ logSpy.mockRestore();
210
+ });
211
+
212
+ it('should scan all workspace packages in pnpm workspace', async () => {
213
+ writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
214
+ writeFileSync(
215
+ join(tempDir, 'pnpm-workspace.yaml'),
216
+ 'packages:\n - "packages/*"\n - "apps/*"\n',
217
+ );
218
+ writePackageJson(tempDir, {
219
+ name: 'test-monorepo',
220
+ devDependencies: {
221
+ '@geekmidas/cli': '^1.0.0',
222
+ },
223
+ });
224
+
225
+ const apiDir = join(tempDir, 'apps', 'api');
226
+ await mkdir(apiDir, { recursive: true });
227
+ writePackageJson(apiDir, {
228
+ name: '@test/api',
229
+ dependencies: {
230
+ '@geekmidas/constructs': '^1.0.0',
231
+ },
232
+ });
233
+
234
+ const libDir = join(tempDir, 'packages', 'shared');
235
+ await mkdir(libDir, { recursive: true });
236
+ writePackageJson(libDir, {
237
+ name: '@test/shared',
238
+ dependencies: {
239
+ '@geekmidas/errors': '^1.0.0',
240
+ },
241
+ });
242
+
243
+ server.use(
244
+ http.get(`${NPM_REGISTRY}/@geekmidas/cli/latest`, () => {
245
+ return HttpResponse.json({ version: '2.0.0' });
246
+ }),
247
+ http.get(`${NPM_REGISTRY}/@geekmidas/constructs/latest`, () => {
248
+ return HttpResponse.json({ version: '2.0.0' });
249
+ }),
250
+ http.get(`${NPM_REGISTRY}/@geekmidas/errors/latest`, () => {
251
+ return HttpResponse.json({ version: '2.0.0' });
252
+ }),
253
+ );
254
+
255
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
256
+
257
+ await upgradeCommand({ dryRun: true });
258
+
259
+ const output = logSpy.mock.calls.flat().join('\n');
260
+ expect(output).toContain('Found 3 package(s) in workspace');
261
+ expect(output).toContain('3 package(s) can be upgraded');
262
+
263
+ logSpy.mockRestore();
264
+ });
265
+
266
+ it('should handle npm registry errors gracefully', async () => {
267
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
268
+ writePackageJson(tempDir, {
269
+ name: 'test-project',
270
+ dependencies: {
271
+ '@geekmidas/nonexistent': '^1.0.0',
272
+ },
273
+ });
274
+
275
+ server.use(
276
+ http.get(`${NPM_REGISTRY}/@geekmidas/nonexistent/latest`, () => {
277
+ return new HttpResponse(null, { status: 404 });
278
+ }),
279
+ );
280
+
281
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
282
+
283
+ await upgradeCommand();
284
+
285
+ const output = logSpy.mock.calls.flat().join('\n');
286
+ expect(output).toContain('unknown');
287
+
288
+ logSpy.mockRestore();
289
+ });
290
+
291
+ it('should throw when execSync fails', async () => {
292
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
293
+ writePackageJson(tempDir, {
294
+ name: 'test-project',
295
+ dependencies: {
296
+ '@geekmidas/errors': '^1.0.0',
297
+ },
298
+ });
299
+
300
+ server.use(
301
+ http.get(`${NPM_REGISTRY}/@geekmidas/errors/latest`, () => {
302
+ return HttpResponse.json({ version: '2.0.0' });
303
+ }),
304
+ );
305
+
306
+ vi.mocked(execSync).mockImplementation(() => {
307
+ throw new Error('command failed');
308
+ });
309
+
310
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
311
+
312
+ await expect(upgradeCommand()).rejects.toThrow('Package upgrade failed');
313
+
314
+ logSpy.mockRestore();
315
+ });
316
+
317
+ it('should scan deps, devDeps, and peerDeps', async () => {
318
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
319
+ writePackageJson(tempDir, {
320
+ name: 'test-project',
321
+ dependencies: {
322
+ '@geekmidas/constructs': '^1.0.0',
323
+ },
324
+ devDependencies: {
325
+ '@geekmidas/testkit': '^1.0.0',
326
+ },
327
+ peerDependencies: {
328
+ '@geekmidas/logger': '^1.0.0',
329
+ },
330
+ });
331
+
332
+ server.use(
333
+ http.get(`${NPM_REGISTRY}/@geekmidas/constructs/latest`, () => {
334
+ return HttpResponse.json({ version: '2.0.0' });
335
+ }),
336
+ http.get(`${NPM_REGISTRY}/@geekmidas/testkit/latest`, () => {
337
+ return HttpResponse.json({ version: '2.0.0' });
338
+ }),
339
+ http.get(`${NPM_REGISTRY}/@geekmidas/logger/latest`, () => {
340
+ return HttpResponse.json({ version: '2.0.0' });
341
+ }),
342
+ );
343
+
344
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
345
+
346
+ await upgradeCommand({ dryRun: true });
347
+
348
+ const output = logSpy.mock.calls.flat().join('\n');
349
+ expect(output).toContain('Checking 3 unique @geekmidas package(s)');
350
+ expect(output).toContain('3 package(s) can be upgraded');
351
+
352
+ logSpy.mockRestore();
353
+ });
354
+ });
@@ -0,0 +1,253 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import {
4
+ detectPackageManager,
5
+ findWorkspacePackages,
6
+ type PackageManager,
7
+ } from '../init/utils.js';
8
+
9
+ const logger = console;
10
+
11
+ export interface UpgradeOptions {
12
+ dryRun?: boolean;
13
+ }
14
+
15
+ interface FoundDependency {
16
+ packageName: string;
17
+ currentVersion: string;
18
+ depType: 'dependencies' | 'devDependencies' | 'peerDependencies';
19
+ packageJsonPath: string;
20
+ workspaceName: string;
21
+ }
22
+
23
+ interface UpgradeInfo extends FoundDependency {
24
+ latestVersion: string;
25
+ isWorkspaceRef: boolean;
26
+ needsUpgrade: boolean;
27
+ }
28
+
29
+ export async function upgradeCommand(
30
+ options: UpgradeOptions = {},
31
+ ): Promise<void> {
32
+ const cwd = process.cwd();
33
+
34
+ logger.log('\n📦 Scanning workspace for @geekmidas packages...\n');
35
+
36
+ const pm = detectPackageManager(cwd);
37
+ logger.log(` Package manager: ${pm}`);
38
+
39
+ const packageJsonPaths = findWorkspacePackages(cwd, pm);
40
+ logger.log(` Found ${packageJsonPaths.length} package(s) in workspace\n`);
41
+
42
+ const dependencies = scanForGeekmidasDeps(packageJsonPaths);
43
+
44
+ if (dependencies.length === 0) {
45
+ logger.log(' No @geekmidas packages found.\n');
46
+ return;
47
+ }
48
+
49
+ const uniquePackages = [...new Set(dependencies.map((d) => d.packageName))];
50
+ logger.log(
51
+ ` Checking ${uniquePackages.length} unique @geekmidas package(s) on npm...\n`,
52
+ );
53
+
54
+ const latestVersions = await fetchLatestVersions(uniquePackages);
55
+
56
+ const upgradeInfos = dependencies.map((dep) =>
57
+ resolveUpgradeInfo(dep, latestVersions),
58
+ );
59
+
60
+ printUpgradeTable(upgradeInfos);
61
+
62
+ const upgradable = upgradeInfos.filter(
63
+ (info) => info.needsUpgrade && !info.isWorkspaceRef,
64
+ );
65
+
66
+ if (upgradable.length === 0) {
67
+ logger.log('\n All @geekmidas packages are up to date!\n');
68
+ return;
69
+ }
70
+
71
+ logger.log(`\n ${upgradable.length} package(s) can be upgraded.\n`);
72
+
73
+ if (options.dryRun) {
74
+ logger.log(' --dry-run: No changes made.\n');
75
+ printUpgradeCommands(upgradable, pm);
76
+ return;
77
+ }
78
+
79
+ executeUpgrade(upgradable, pm, cwd);
80
+
81
+ logger.log('\n ✅ Upgrade complete! Run your tests to verify.\n');
82
+ }
83
+
84
+ function scanForGeekmidasDeps(packageJsonPaths: string[]): FoundDependency[] {
85
+ const results: FoundDependency[] = [];
86
+ const depTypes = [
87
+ 'dependencies',
88
+ 'devDependencies',
89
+ 'peerDependencies',
90
+ ] as const;
91
+
92
+ for (const pkgJsonPath of packageJsonPaths) {
93
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
94
+ const workspaceName = pkg.name ?? pkgJsonPath;
95
+
96
+ for (const depType of depTypes) {
97
+ const deps = pkg[depType];
98
+ if (!deps) continue;
99
+
100
+ for (const [name, version] of Object.entries(deps)) {
101
+ if (!name.startsWith('@geekmidas/')) continue;
102
+
103
+ results.push({
104
+ packageName: name,
105
+ currentVersion: version as string,
106
+ depType,
107
+ packageJsonPath: pkgJsonPath,
108
+ workspaceName,
109
+ });
110
+ }
111
+ }
112
+ }
113
+
114
+ return results;
115
+ }
116
+
117
+ async function fetchLatestVersions(
118
+ packageNames: string[],
119
+ ): Promise<Map<string, string>> {
120
+ const versions = new Map<string, string>();
121
+
122
+ const results = await Promise.allSettled(
123
+ packageNames.map(async (name) => {
124
+ const res = await fetch(`https://registry.npmjs.org/${name}/latest`);
125
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
126
+ const data = (await res.json()) as { version: string };
127
+ return { name, version: data.version };
128
+ }),
129
+ );
130
+
131
+ for (const result of results) {
132
+ if (result.status === 'fulfilled') {
133
+ versions.set(result.value.name, result.value.version);
134
+ }
135
+ }
136
+
137
+ return versions;
138
+ }
139
+
140
+ function resolveUpgradeInfo(
141
+ dep: FoundDependency,
142
+ latestVersions: Map<string, string>,
143
+ ): UpgradeInfo {
144
+ const isWorkspaceRef = dep.currentVersion.startsWith('workspace:');
145
+ const latestVersion = latestVersions.get(dep.packageName) ?? 'unknown';
146
+
147
+ const currentBare = dep.currentVersion.replace(/^[\^~>=<]*/g, '');
148
+ const needsUpgrade =
149
+ !isWorkspaceRef &&
150
+ latestVersion !== 'unknown' &&
151
+ currentBare !== latestVersion;
152
+
153
+ return {
154
+ ...dep,
155
+ latestVersion,
156
+ isWorkspaceRef,
157
+ needsUpgrade,
158
+ };
159
+ }
160
+
161
+ function printUpgradeTable(infos: UpgradeInfo[]): void {
162
+ const byPackage = new Map<string, UpgradeInfo>();
163
+ for (const info of infos) {
164
+ if (!byPackage.has(info.packageName)) {
165
+ byPackage.set(info.packageName, info);
166
+ }
167
+ }
168
+
169
+ const nameWidth = 33;
170
+ const verWidth = 14;
171
+ const statusWidth = 14;
172
+
173
+ const hr = ` ${'─'.repeat(nameWidth + verWidth * 2 + statusWidth + 5)}`;
174
+
175
+ logger.log(hr);
176
+ logger.log(
177
+ ` ${'Package'.padEnd(nameWidth)} ${'Current'.padEnd(verWidth)} ${'Latest'.padEnd(verWidth)} ${'Status'.padEnd(statusWidth)}`,
178
+ );
179
+ logger.log(hr);
180
+
181
+ for (const [, info] of byPackage) {
182
+ const name = info.packageName.padEnd(nameWidth);
183
+ const current = info.currentVersion.padEnd(verWidth);
184
+ const latest = info.latestVersion.padEnd(verWidth);
185
+
186
+ let status: string;
187
+ if (info.isWorkspaceRef) {
188
+ status = 'workspace';
189
+ } else if (info.needsUpgrade) {
190
+ status = '⬆ upgrade';
191
+ } else {
192
+ status = '✓ up-to-date';
193
+ }
194
+
195
+ logger.log(` ${name} ${current} ${latest} ${status}`);
196
+ }
197
+
198
+ logger.log(hr);
199
+ }
200
+
201
+ function printUpgradeCommands(
202
+ upgradable: UpgradeInfo[],
203
+ pm: PackageManager,
204
+ ): void {
205
+ logger.log(' Commands that would be run:\n');
206
+
207
+ const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
208
+ const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
209
+ logger.log(` ${cmd}\n`);
210
+ }
211
+
212
+ function getWorkspaceUpgradeCommand(
213
+ pm: PackageManager,
214
+ packages: string[],
215
+ ): string {
216
+ const pkgList = packages.join(' ');
217
+
218
+ switch (pm) {
219
+ case 'pnpm':
220
+ return `pnpm update -r ${pkgList} --latest`;
221
+ case 'yarn':
222
+ return `yarn upgrade ${pkgList}`;
223
+ case 'bun':
224
+ return `bun update ${pkgList}`;
225
+ case 'npm':
226
+ return `npm update ${pkgList} --workspaces`;
227
+ default:
228
+ return `npm update ${pkgList}`;
229
+ }
230
+ }
231
+
232
+ function executeUpgrade(
233
+ upgradable: UpgradeInfo[],
234
+ pm: PackageManager,
235
+ cwd: string,
236
+ ): void {
237
+ const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
238
+
239
+ const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
240
+ logger.log(` Running: ${cmd}\n`);
241
+
242
+ try {
243
+ execSync(cmd, {
244
+ cwd,
245
+ stdio: 'inherit',
246
+ timeout: 120_000,
247
+ });
248
+ } catch {
249
+ throw new Error(
250
+ 'Package upgrade failed. Check the output above for details.',
251
+ );
252
+ }
253
+ }