workspace-utils 1.0.0 → 1.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.
Files changed (49) hide show
  1. package/dist/index.js +15460 -0
  2. package/dist/package.json +57 -0
  3. package/package.json +4 -1
  4. package/.github/workflows/mdbook.yml +0 -64
  5. package/.prettierignore +0 -22
  6. package/.prettierrc +0 -13
  7. package/docs/book.toml +0 -10
  8. package/docs/src/SUMMARY.md +0 -24
  9. package/docs/src/commands/build.md +0 -110
  10. package/docs/src/commands/dev.md +0 -118
  11. package/docs/src/commands/overview.md +0 -239
  12. package/docs/src/commands/run.md +0 -153
  13. package/docs/src/configuration.md +0 -249
  14. package/docs/src/examples.md +0 -567
  15. package/docs/src/installation.md +0 -148
  16. package/docs/src/introduction.md +0 -117
  17. package/docs/src/quick-start.md +0 -278
  18. package/docs/src/troubleshooting.md +0 -533
  19. package/index.ts +0 -84
  20. package/src/commands/build.ts +0 -158
  21. package/src/commands/dev.ts +0 -192
  22. package/src/commands/run.test.ts +0 -329
  23. package/src/commands/run.ts +0 -118
  24. package/src/core/dependency-graph.ts +0 -262
  25. package/src/core/process-runner.ts +0 -355
  26. package/src/core/workspace.test.ts +0 -404
  27. package/src/core/workspace.ts +0 -228
  28. package/src/package-managers/bun.test.ts +0 -209
  29. package/src/package-managers/bun.ts +0 -79
  30. package/src/package-managers/detector.test.ts +0 -199
  31. package/src/package-managers/detector.ts +0 -111
  32. package/src/package-managers/index.ts +0 -10
  33. package/src/package-managers/npm.ts +0 -79
  34. package/src/package-managers/pnpm.ts +0 -101
  35. package/src/package-managers/types.ts +0 -42
  36. package/src/utils/output.ts +0 -301
  37. package/src/utils/package-utils.ts +0 -243
  38. package/tests/bun-workspace/apps/web-app/package.json +0 -18
  39. package/tests/bun-workspace/bun.lockb +0 -0
  40. package/tests/bun-workspace/package.json +0 -18
  41. package/tests/bun-workspace/packages/shared-utils/package.json +0 -15
  42. package/tests/bun-workspace/packages/ui-components/package.json +0 -17
  43. package/tests/npm-workspace/package-lock.json +0 -0
  44. package/tests/npm-workspace/package.json +0 -18
  45. package/tests/npm-workspace/packages/core/package.json +0 -15
  46. package/tests/pnpm-workspace/package.json +0 -14
  47. package/tests/pnpm-workspace/packages/utils/package.json +0 -15
  48. package/tests/pnpm-workspace/pnpm-workspace.yaml +0 -3
  49. package/tsconfig.json +0 -29
@@ -1,158 +0,0 @@
1
- import pc from 'picocolors';
2
- import { WorkspaceParser } from '../core/workspace.ts';
3
- import {
4
- buildDependencyGraph,
5
- validatePackagesHaveScript,
6
- prepareCommandExecution,
7
- } from '../utils/package-utils.ts';
8
- import { ProcessRunner } from '../core/process-runner.ts';
9
- import { Output } from '../utils/output.ts';
10
-
11
- interface BuildCommandOptions {
12
- filter?: string;
13
- concurrency?: string;
14
- skipUnchanged?: boolean;
15
- }
16
-
17
- export async function buildCommand(options: BuildCommandOptions): Promise<void> {
18
- try {
19
- Output.build('Building packages in dependency order...\n');
20
-
21
- // Parse workspace
22
- const parser = new WorkspaceParser();
23
- const workspace = await parser.parseWorkspace();
24
-
25
- Output.dim(`Workspace root: ${workspace.root}`, 'folder');
26
- Output.dim(`Found ${workspace.packages.length} packages\n`, 'package');
27
-
28
- // Filter packages if pattern provided
29
- let targetPackages = workspace.packages;
30
- if (options.filter) {
31
- targetPackages = parser.filterPackages(workspace.packages, options.filter);
32
- Output.log(
33
- `Filtered to ${targetPackages.length} packages matching "${options.filter}"`,
34
- 'magnifying',
35
- 'yellow'
36
- );
37
- }
38
-
39
- // Validate packages have the build script
40
- const { valid: packagesWithBuild, invalid: packagesWithoutBuild } = validatePackagesHaveScript(
41
- targetPackages,
42
- 'build'
43
- );
44
-
45
- if (packagesWithoutBuild.length > 0) {
46
- Output.warning(`The following packages don't have a "build" script:`);
47
- packagesWithoutBuild.forEach(pkg => {
48
- Output.listItem(pkg.name);
49
- });
50
- console.log();
51
- }
52
-
53
- if (packagesWithBuild.length === 0) {
54
- Output.error('No packages found with a "build" script.');
55
- process.exit(1);
56
- }
57
-
58
- // Build dependency graph
59
- Output.log('Building dependency graph...', 'chart', 'blue');
60
- const dependencyGraph = buildDependencyGraph(packagesWithBuild);
61
-
62
- // Filter graph to only include packages that need to be built
63
- const packageNames = packagesWithBuild.map(pkg => pkg.name);
64
- const filteredGraph = dependencyGraph.filterGraph(packageNames);
65
-
66
- // Get build batches (topological order)
67
- let buildBatches;
68
- try {
69
- buildBatches = filteredGraph.getBuildBatches();
70
- } catch (error) {
71
- Output.error(
72
- `Dependency cycle detected: ${error instanceof Error ? error.message : String(error)}`
73
- );
74
- Output.tip('Check for circular dependencies between packages.');
75
- process.exit(1);
76
- }
77
-
78
- Output.success(`Build order determined: ${buildBatches.length} batches`);
79
-
80
- // Display build plan
81
- console.log(pc.blue(`\n${Output.getSymbol('books')} Build Plan:`));
82
- buildBatches.forEach((batch, index) => {
83
- Output.listItem(`Batch ${index + 1}: ${batch.join(', ')}`);
84
- });
85
- console.log();
86
-
87
- const concurrency = parseInt(options.concurrency || '4', 10);
88
-
89
- Output.log(`Package manager: ${workspace.packageManager.name}`, 'wrench', 'blue');
90
- Output.log(`Batch concurrency: ${concurrency}`, 'lightning', 'blue');
91
- console.log();
92
-
93
- // Prepare commands organized by batches
94
- const packageMap = new Map(packagesWithBuild.map(pkg => [pkg.name, pkg]));
95
- const commandBatches = buildBatches.map(batch => {
96
- return batch
97
- .map(packageName => packageMap.get(packageName))
98
- .filter((pkg): pkg is NonNullable<typeof pkg> => pkg !== undefined)
99
- .map(pkg => {
100
- const commands = prepareCommandExecution([pkg], 'build', workspace.packageManager);
101
- return commands[0];
102
- })
103
- .filter((cmd): cmd is NonNullable<typeof cmd> => cmd !== undefined);
104
- });
105
-
106
- // Execute builds in batches
107
- const startTime = Date.now();
108
- const allResults = await ProcessRunner.runBatches(commandBatches, concurrency);
109
- const totalDuration = Date.now() - startTime;
110
-
111
- // Print final summary
112
- const successful = allResults.filter(r => r.success);
113
- const failed = allResults.filter(r => !r.success);
114
-
115
- Output.buildSummary(successful.length, failed.length, totalDuration);
116
-
117
- if (failed.length > 0) {
118
- console.log(pc.red('\nFailed packages:'));
119
- failed.forEach(f => {
120
- Output.listItem(`${f.packageName} (exit code ${f.exitCode})`);
121
- });
122
- }
123
-
124
- if (successful.length > 0) {
125
- const avgDuration = Math.round(
126
- successful.reduce((sum, r) => sum + r.duration, 0) / successful.length
127
- );
128
- Output.dim(`Average package build time: ${Output.formatDuration(avgDuration)}`, 'chart');
129
- }
130
-
131
- // Show dependency chain info
132
- const rootPackages = filteredGraph.getRootPackages();
133
- const leafPackages = filteredGraph.getLeafPackages();
134
-
135
- if (rootPackages.length > 0) {
136
- Output.dim(`Root packages (no dependencies): ${rootPackages.join(', ')}`, 'seedling');
137
- }
138
-
139
- if (leafPackages.length > 0) {
140
- Output.dim(`Leaf packages (no dependents): ${leafPackages.join(', ')}`, 'leaf');
141
- }
142
-
143
- // Exit with error code if any builds failed
144
- if (failed.length > 0) {
145
- Output.log('\nBuild failed due to package failures.', 'fire', 'red');
146
- process.exit(1);
147
- } else {
148
- Output.celebrate('\nAll packages built successfully!');
149
- }
150
- } catch (error) {
151
- Output.log(
152
- `Build error: ${error instanceof Error ? error.message : String(error)}`,
153
- 'fire',
154
- 'red'
155
- );
156
- process.exit(1);
157
- }
158
- }
@@ -1,192 +0,0 @@
1
- import pc from 'picocolors';
2
- import { WorkspaceParser } from '../core/workspace.ts';
3
- import { validatePackagesHaveScript, prepareCommandExecution } from '../utils/package-utils.ts';
4
- import { ProcessRunner } from '../core/process-runner.ts';
5
- import { Output } from '../utils/output.ts';
6
-
7
- interface DevCommandOptions {
8
- filter?: string;
9
- concurrency?: string;
10
- }
11
-
12
- export async function devCommand(options: DevCommandOptions): Promise<void> {
13
- try {
14
- Output.dev('Starting development servers with live log streaming...\n');
15
-
16
- // Parse workspace
17
- const parser = new WorkspaceParser();
18
- const workspace = await parser.parseWorkspace();
19
-
20
- Output.dim(`Workspace root: ${workspace.root}`, 'folder');
21
- Output.dim(`Found ${workspace.packages.length} packages\n`, 'package');
22
-
23
- // Filter packages if pattern provided
24
- let targetPackages = workspace.packages;
25
- if (options.filter) {
26
- targetPackages = parser.filterPackages(workspace.packages, options.filter);
27
- Output.log(
28
- `Filtered to ${targetPackages.length} packages matching "${options.filter}"`,
29
- 'magnifying',
30
- 'yellow'
31
- );
32
- }
33
-
34
- // Validate packages have the dev script
35
- const { valid: packagesWithDev, invalid: packagesWithoutDev } = validatePackagesHaveScript(
36
- targetPackages,
37
- 'dev'
38
- );
39
-
40
- if (packagesWithoutDev.length > 0) {
41
- Output.warning(`The following packages don't have a "dev" script:`);
42
- packagesWithoutDev.forEach(pkg => {
43
- Output.listItem(pkg.name);
44
- });
45
- console.log();
46
- }
47
-
48
- if (packagesWithDev.length === 0) {
49
- Output.error('No packages found with a "dev" script.');
50
- process.exit(1);
51
- }
52
-
53
- Output.success(`Starting dev servers for ${packagesWithDev.length} packages:`);
54
- packagesWithDev.forEach(pkg => {
55
- const color = ProcessRunner.getPackageColor(pkg.name);
56
- const getColorFn = (colorName: string) => {
57
- switch (colorName) {
58
- case 'red':
59
- return pc.red;
60
- case 'green':
61
- return pc.green;
62
- case 'yellow':
63
- return pc.yellow;
64
- case 'blue':
65
- return pc.blue;
66
- case 'magenta':
67
- return pc.magenta;
68
- case 'cyan':
69
- return pc.cyan;
70
- case 'gray':
71
- return pc.gray;
72
- case 'redBright':
73
- return pc.redBright;
74
- case 'greenBright':
75
- return pc.greenBright;
76
- case 'yellowBright':
77
- return pc.yellowBright;
78
- case 'blueBright':
79
- return pc.blueBright;
80
- case 'magentaBright':
81
- return pc.magentaBright;
82
- case 'cyanBright':
83
- return pc.cyanBright;
84
- default:
85
- return pc.white;
86
- }
87
- };
88
- const colorFn = getColorFn(color);
89
- console.log(` • ${colorFn(pkg.name)}`);
90
- });
91
- console.log();
92
-
93
- const concurrency = parseInt(options.concurrency || '4', 10);
94
-
95
- Output.log(`Package manager: ${workspace.packageManager.name}`, 'wrench', 'blue');
96
- Output.log(
97
- `Running ${Math.min(packagesWithDev.length, concurrency)} dev servers simultaneously`,
98
- 'lightning',
99
- 'blue'
100
- );
101
- Output.tip('Use Ctrl+C to stop all development servers\n');
102
-
103
- // Prepare command execution with enhanced logging for dev mode
104
- const commands = prepareCommandExecution(packagesWithDev, 'dev', workspace.packageManager).map(
105
- cmd => ({
106
- ...cmd,
107
- logOptions: {
108
- ...cmd.logOptions,
109
- showTimestamp: false, // Disable timestamps for dev mode
110
- },
111
- })
112
- );
113
-
114
- // Set up graceful shutdown
115
- let isShuttingDown = false;
116
- const shutdown = () => {
117
- if (isShuttingDown) return;
118
- isShuttingDown = true;
119
-
120
- Output.log('\n\nShutting down development servers...', 'warning', 'yellow');
121
- Output.dim('This may take a moment to gracefully stop all processes.\n');
122
-
123
- // Force exit after a timeout
124
- setTimeout(() => {
125
- Output.log('Timeout reached, forcing exit...', 'clock', 'red');
126
- process.exit(0);
127
- }, 5000);
128
- };
129
-
130
- process.on('SIGINT', shutdown);
131
- process.on('SIGTERM', shutdown);
132
-
133
- Output.log('Starting development servers...\n', 'movie', 'green');
134
- Output.separator();
135
-
136
- // Execute all dev commands in parallel (they're meant to run indefinitely)
137
- const startTime = Date.now();
138
-
139
- try {
140
- // For dev mode, we don't wait for completion since these are long-running processes
141
- const promises = commands.map(cmd =>
142
- ProcessRunner.runCommand(cmd.command, cmd.args, cmd.options, cmd.logOptions).catch(
143
- error => {
144
- Output.log(`Error in ${cmd.logOptions.prefix}: ${error}`, 'fire', 'red');
145
- return {
146
- success: false,
147
- exitCode: 1,
148
- packageName: cmd.logOptions.prefix,
149
- command: `${cmd.command} ${cmd.args.join(' ')}`,
150
- duration: Date.now() - startTime,
151
- };
152
- }
153
- )
154
- );
155
-
156
- // Wait for all processes to start
157
- await Promise.allSettled(promises.slice(0, Math.min(concurrency, promises.length)));
158
-
159
- // If we reach here, some processes may have exited unexpectedly
160
- if (!isShuttingDown) {
161
- Output.warning('\nSome development servers may have stopped unexpectedly.');
162
- Output.dim('Check the logs above for any error messages.\n');
163
- }
164
- } catch (error) {
165
- if (!isShuttingDown) {
166
- Output.log(
167
- `Development server error: ${error instanceof Error ? error.message : String(error)}`,
168
- 'fire',
169
- 'red'
170
- );
171
- }
172
- }
173
-
174
- const totalDuration = Date.now() - startTime;
175
-
176
- if (!isShuttingDown) {
177
- Output.log(
178
- `\nDevelopment session lasted: ${Output.formatDuration(totalDuration)}`,
179
- 'chart',
180
- 'blue'
181
- );
182
- Output.success('All development servers have stopped.');
183
- }
184
- } catch (error) {
185
- Output.log(
186
- `Dev command error: ${error instanceof Error ? error.message : String(error)}`,
187
- 'fire',
188
- 'red'
189
- );
190
- process.exit(1);
191
- }
192
- }
@@ -1,329 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
- import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
3
- import { join } from 'path';
4
- import { runCommand } from './run.ts';
5
- import { spyOn } from 'bun:test';
6
-
7
- describe('runCommand', () => {
8
- const testDir = join(process.cwd(), 'test-temp-run');
9
-
10
- beforeEach(() => {
11
- // Clean up test directory if it exists
12
- if (existsSync(testDir)) {
13
- rmSync(testDir, { recursive: true, force: true });
14
- }
15
- mkdirSync(testDir, { recursive: true });
16
-
17
- // Change to test directory
18
- process.chdir(testDir);
19
- });
20
-
21
- afterEach(() => {
22
- // Change back to original directory
23
- process.chdir(process.cwd().replace('/test-temp-run', ''));
24
-
25
- // Clean up test directory
26
- if (existsSync(testDir)) {
27
- rmSync(testDir, { recursive: true, force: true });
28
- }
29
- });
30
-
31
- describe('Bun workspace', () => {
32
- beforeEach(() => {
33
- // Create Bun workspace
34
- writeFileSync(join(testDir, 'bun.lockb'), '');
35
- writeFileSync(
36
- join(testDir, 'package.json'),
37
- JSON.stringify({
38
- name: 'test-workspace',
39
- workspaces: ['packages/*'],
40
- })
41
- );
42
-
43
- // Create test packages
44
- mkdirSync(join(testDir, 'packages', 'pkg1'), { recursive: true });
45
- writeFileSync(
46
- join(testDir, 'packages', 'pkg1', 'package.json'),
47
- JSON.stringify({
48
- name: '@test/pkg1',
49
- version: '1.0.0',
50
- scripts: {
51
- test: 'echo "Testing pkg1"',
52
- build: 'echo "Building pkg1"',
53
- lint: 'echo "Linting pkg1"',
54
- },
55
- })
56
- );
57
-
58
- mkdirSync(join(testDir, 'packages', 'pkg2'), { recursive: true });
59
- writeFileSync(
60
- join(testDir, 'packages', 'pkg2', 'package.json'),
61
- JSON.stringify({
62
- name: '@test/pkg2',
63
- version: '1.0.0',
64
- scripts: {
65
- test: 'echo "Testing pkg2"',
66
- build: 'echo "Building pkg2"',
67
- },
68
- })
69
- );
70
- });
71
-
72
- it('should run test script in parallel by default', async () => {
73
- const consoleSpy = spyOn(console, 'log');
74
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
75
- throw new Error('process.exit called');
76
- });
77
-
78
- const options = {};
79
-
80
- try {
81
- await runCommand('test', options);
82
- } catch {
83
- // Expected to throw due to process.exit mock
84
- }
85
-
86
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('🔧 Package manager: bun'));
87
- expect(consoleSpy).toHaveBeenCalledWith(
88
- expect.stringContaining('⚡ Execution mode: parallel (concurrency: 4)')
89
- );
90
-
91
- consoleSpy.mockRestore();
92
- processExitSpy.mockRestore();
93
- });
94
-
95
- it('should run script sequentially when --sequential flag is used', async () => {
96
- const consoleSpy = spyOn(console, 'log');
97
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
98
- throw new Error('process.exit called');
99
- });
100
-
101
- const options = { sequential: true };
102
-
103
- try {
104
- await runCommand('test', options);
105
- } catch {
106
- // Expected to throw due to process.exit mock
107
- }
108
-
109
- expect(consoleSpy).toHaveBeenCalledWith(
110
- expect.stringContaining('⚡ Execution mode: sequential')
111
- );
112
-
113
- consoleSpy.mockRestore();
114
- processExitSpy.mockRestore();
115
- });
116
-
117
- it('should filter packages when filter option is provided', async () => {
118
- const consoleSpy = spyOn(console, 'log');
119
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
120
- throw new Error('process.exit called');
121
- });
122
-
123
- const options = { filter: '*pkg1*' };
124
-
125
- try {
126
- await runCommand('test', options);
127
- } catch {
128
- // Expected to throw due to process.exit mock
129
- }
130
-
131
- expect(consoleSpy).toHaveBeenCalledWith(
132
- expect.stringContaining('🔍 Filtered to 1 packages matching "*pkg1*"')
133
- );
134
- expect(consoleSpy).toHaveBeenCalledWith(
135
- expect.stringContaining('✅ Running "test" in 1 packages:')
136
- );
137
-
138
- consoleSpy.mockRestore();
139
- processExitSpy.mockRestore();
140
- });
141
-
142
- it('should handle packages without the specified script', async () => {
143
- const consoleSpy = spyOn(console, 'log');
144
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
145
- throw new Error('process.exit called');
146
- });
147
-
148
- const options = {};
149
-
150
- try {
151
- await runCommand('lint', options);
152
- } catch {
153
- // Expected to throw due to process.exit mock
154
- }
155
-
156
- expect(consoleSpy).toHaveBeenCalledWith(
157
- expect.stringContaining('⚠️ The following packages don\'t have the "lint" script:')
158
- );
159
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('• @test/pkg2'));
160
-
161
- consoleSpy.mockRestore();
162
- processExitSpy.mockRestore();
163
- });
164
-
165
- it('should exit with error when no packages have the script', async () => {
166
- const consoleSpy = spyOn(console, 'log');
167
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
168
- throw new Error('process.exit called');
169
- });
170
-
171
- const options = {};
172
-
173
- try {
174
- await runCommand('nonexistent', options);
175
- } catch (error) {
176
- expect((error as Error).message).toBe('process.exit called');
177
- }
178
-
179
- expect(consoleSpy).toHaveBeenCalledWith(
180
- expect.stringContaining('❌ No packages found with the "nonexistent" script.')
181
- );
182
- expect(processExitSpy).toHaveBeenCalledWith(1);
183
-
184
- consoleSpy.mockRestore();
185
- processExitSpy.mockRestore();
186
- });
187
-
188
- it('should use custom concurrency when specified', async () => {
189
- const consoleSpy = spyOn(console, 'log');
190
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
191
- throw new Error('process.exit called');
192
- });
193
-
194
- const options = { concurrency: '8' };
195
-
196
- try {
197
- await runCommand('test', options);
198
- } catch {
199
- // Expected to throw due to process.exit mock
200
- }
201
-
202
- expect(consoleSpy).toHaveBeenCalledWith(
203
- expect.stringContaining('⚡ Execution mode: parallel (concurrency: 8)')
204
- );
205
-
206
- consoleSpy.mockRestore();
207
- processExitSpy.mockRestore();
208
- });
209
- });
210
-
211
- describe('pnpm workspace', () => {
212
- beforeEach(() => {
213
- // Create pnpm workspace
214
- writeFileSync(join(testDir, 'pnpm-lock.yaml'), 'lockfileVersion: 6.0');
215
- writeFileSync(join(testDir, 'pnpm-workspace.yaml'), 'packages:\n - packages/*');
216
-
217
- // Create test package
218
- mkdirSync(join(testDir, 'packages', 'utils'), { recursive: true });
219
- writeFileSync(
220
- join(testDir, 'packages', 'utils', 'package.json'),
221
- JSON.stringify({
222
- name: '@test/utils',
223
- version: '1.0.0',
224
- scripts: {
225
- test: 'echo "Testing utils with pnpm"',
226
- },
227
- })
228
- );
229
- });
230
-
231
- it('should detect pnpm as package manager', async () => {
232
- const consoleSpy = spyOn(console, 'log');
233
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
234
- throw new Error('process.exit called');
235
- });
236
-
237
- const options = {};
238
-
239
- try {
240
- await runCommand('test', options);
241
- } catch {
242
- // Expected to throw due to process.exit mock
243
- }
244
-
245
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('🔧 Package manager: pnpm'));
246
-
247
- consoleSpy.mockRestore();
248
- processExitSpy.mockRestore();
249
- });
250
- });
251
-
252
- describe('npm workspace', () => {
253
- beforeEach(() => {
254
- // Create npm workspace
255
- writeFileSync(join(testDir, 'package-lock.json'), '{}');
256
- writeFileSync(
257
- join(testDir, 'package.json'),
258
- JSON.stringify({
259
- name: 'test-workspace',
260
- workspaces: ['libs/*'],
261
- })
262
- );
263
-
264
- // Create test package
265
- mkdirSync(join(testDir, 'libs', 'core'), { recursive: true });
266
- writeFileSync(
267
- join(testDir, 'libs', 'core', 'package.json'),
268
- JSON.stringify({
269
- name: 'core',
270
- version: '1.0.0',
271
- scripts: {
272
- test: 'echo "Testing core with npm"',
273
- },
274
- })
275
- );
276
- });
277
-
278
- it('should detect npm as package manager', async () => {
279
- const consoleSpy = spyOn(console, 'log');
280
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
281
- throw new Error('process.exit called');
282
- });
283
-
284
- const options = {};
285
-
286
- try {
287
- await runCommand('test', options);
288
- } catch {
289
- // Expected to throw due to process.exit mock
290
- }
291
-
292
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('🔧 Package manager: npm'));
293
-
294
- consoleSpy.mockRestore();
295
- processExitSpy.mockRestore();
296
- });
297
- });
298
-
299
- describe('error handling', () => {
300
- it('should handle workspace parsing errors gracefully', async () => {
301
- // Create invalid workspace (no workspaces config)
302
- writeFileSync(
303
- join(testDir, 'package.json'),
304
- JSON.stringify({
305
- name: 'invalid-workspace',
306
- })
307
- );
308
-
309
- const consoleSpy = spyOn(console, 'error');
310
- const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
311
- throw new Error('process.exit called');
312
- });
313
-
314
- const options = {};
315
-
316
- try {
317
- await runCommand('test', options);
318
- } catch (error) {
319
- expect((error as Error).message).toBe('process.exit called');
320
- }
321
-
322
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('💥 Error:'));
323
- expect(processExitSpy).toHaveBeenCalledWith(1);
324
-
325
- consoleSpy.mockRestore();
326
- processExitSpy.mockRestore();
327
- });
328
- });
329
- });