workspace-utils 1.0.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 (49) hide show
  1. package/.github/workflows/mdbook.yml +64 -0
  2. package/.prettierignore +22 -0
  3. package/.prettierrc +13 -0
  4. package/LICENSE +21 -0
  5. package/README.md +278 -0
  6. package/docs/book.toml +10 -0
  7. package/docs/src/SUMMARY.md +24 -0
  8. package/docs/src/commands/build.md +110 -0
  9. package/docs/src/commands/dev.md +118 -0
  10. package/docs/src/commands/overview.md +239 -0
  11. package/docs/src/commands/run.md +153 -0
  12. package/docs/src/configuration.md +249 -0
  13. package/docs/src/examples.md +567 -0
  14. package/docs/src/installation.md +148 -0
  15. package/docs/src/introduction.md +117 -0
  16. package/docs/src/quick-start.md +278 -0
  17. package/docs/src/troubleshooting.md +533 -0
  18. package/index.ts +84 -0
  19. package/package.json +54 -0
  20. package/src/commands/build.ts +158 -0
  21. package/src/commands/dev.ts +192 -0
  22. package/src/commands/run.test.ts +329 -0
  23. package/src/commands/run.ts +118 -0
  24. package/src/core/dependency-graph.ts +262 -0
  25. package/src/core/process-runner.ts +355 -0
  26. package/src/core/workspace.test.ts +404 -0
  27. package/src/core/workspace.ts +228 -0
  28. package/src/package-managers/bun.test.ts +209 -0
  29. package/src/package-managers/bun.ts +79 -0
  30. package/src/package-managers/detector.test.ts +199 -0
  31. package/src/package-managers/detector.ts +111 -0
  32. package/src/package-managers/index.ts +10 -0
  33. package/src/package-managers/npm.ts +79 -0
  34. package/src/package-managers/pnpm.ts +101 -0
  35. package/src/package-managers/types.ts +42 -0
  36. package/src/utils/output.ts +301 -0
  37. package/src/utils/package-utils.ts +243 -0
  38. package/tests/bun-workspace/apps/web-app/package.json +18 -0
  39. package/tests/bun-workspace/bun.lockb +0 -0
  40. package/tests/bun-workspace/package.json +18 -0
  41. package/tests/bun-workspace/packages/shared-utils/package.json +15 -0
  42. package/tests/bun-workspace/packages/ui-components/package.json +17 -0
  43. package/tests/npm-workspace/package-lock.json +0 -0
  44. package/tests/npm-workspace/package.json +18 -0
  45. package/tests/npm-workspace/packages/core/package.json +15 -0
  46. package/tests/pnpm-workspace/package.json +14 -0
  47. package/tests/pnpm-workspace/packages/utils/package.json +15 -0
  48. package/tests/pnpm-workspace/pnpm-workspace.yaml +3 -0
  49. package/tsconfig.json +29 -0
@@ -0,0 +1,101 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import type { PackageManager, WorkspaceConfig } from './types.ts';
5
+
6
+ export class PnpmPackageManager implements PackageManager {
7
+ readonly name = 'pnpm';
8
+
9
+ isActive(workspaceRoot: string): boolean {
10
+ // Check for pnpm-lock.yaml file
11
+ const lockFile = join(workspaceRoot, 'pnpm-lock.yaml');
12
+ if (existsSync(lockFile)) {
13
+ return true;
14
+ }
15
+
16
+ // Check for pnpm-workspace.yaml
17
+ const workspaceFile = join(workspaceRoot, 'pnpm-workspace.yaml');
18
+ if (existsSync(workspaceFile)) {
19
+ return true;
20
+ }
21
+
22
+ // Check for .pnpmfile.cjs or pnpm configuration in package.json
23
+ const pnpmFile = join(workspaceRoot, '.pnpmfile.cjs');
24
+ if (existsSync(pnpmFile)) {
25
+ return true;
26
+ }
27
+
28
+ const packageJsonPath = join(workspaceRoot, 'package.json');
29
+ if (existsSync(packageJsonPath)) {
30
+ try {
31
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record<
32
+ string,
33
+ unknown
34
+ >;
35
+ // Check for pnpm-specific fields
36
+ const publishConfig = packageJson.publishConfig as Record<string, unknown> | undefined;
37
+ if (packageJson.pnpm || publishConfig?.registry) {
38
+ return true;
39
+ }
40
+ } catch {
41
+ // Ignore JSON parse errors
42
+ }
43
+ }
44
+
45
+ return false;
46
+ }
47
+
48
+ getRunCommand(scriptName: string): { command: string; args: string[] } {
49
+ return {
50
+ command: 'pnpm',
51
+ args: ['run', scriptName],
52
+ };
53
+ }
54
+
55
+ parseWorkspaceConfig(workspaceRoot: string): WorkspaceConfig {
56
+ // First try pnpm-workspace.yaml
57
+ const workspaceFile = join(workspaceRoot, 'pnpm-workspace.yaml');
58
+ if (existsSync(workspaceFile)) {
59
+ const content = readFileSync(workspaceFile, 'utf8');
60
+ const config = parseYaml(content) as { packages?: string[] };
61
+
62
+ if (!config.packages || !Array.isArray(config.packages)) {
63
+ throw new Error('Invalid pnpm-workspace.yaml: packages must be an array');
64
+ }
65
+
66
+ return { packages: config.packages };
67
+ }
68
+
69
+ // Fallback to package.json workspaces
70
+ const packageJsonPath = join(workspaceRoot, 'package.json');
71
+ if (!existsSync(packageJsonPath)) {
72
+ throw new Error('No pnpm-workspace.yaml or package.json found in workspace root');
73
+ }
74
+
75
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record<
76
+ string,
77
+ unknown
78
+ >;
79
+
80
+ if (!packageJson.workspaces) {
81
+ throw new Error(
82
+ 'No pnpm-workspace.yaml found and no workspaces configuration in package.json'
83
+ );
84
+ }
85
+
86
+ const workspaces = packageJson.workspaces as string[] | { packages: string[] };
87
+ const packages = Array.isArray(workspaces) ? workspaces : workspaces.packages;
88
+
89
+ if (!Array.isArray(packages)) {
90
+ throw new Error(
91
+ 'Invalid workspaces configuration: must be an array or object with packages array'
92
+ );
93
+ }
94
+
95
+ return { packages };
96
+ }
97
+
98
+ getLockFileName(): string {
99
+ return 'pnpm-lock.yaml';
100
+ }
101
+ }
@@ -0,0 +1,42 @@
1
+ export interface PackageManagerConfig {
2
+ name: string;
3
+ lockFile: string;
4
+ command: string;
5
+ runArgs: string[];
6
+ }
7
+
8
+ export interface WorkspaceConfig {
9
+ packages: string[];
10
+ }
11
+
12
+ export interface PackageManager {
13
+ /**
14
+ * Name of the package manager
15
+ */
16
+ readonly name: string;
17
+
18
+ /**
19
+ * Check if this package manager is being used in the current directory
20
+ */
21
+ isActive(workspaceRoot: string): boolean;
22
+
23
+ /**
24
+ * Get the command and arguments to run a script
25
+ */
26
+ getRunCommand(scriptName: string): { command: string; args: string[] };
27
+
28
+ /**
29
+ * Parse workspace configuration
30
+ */
31
+ parseWorkspaceConfig(workspaceRoot: string): WorkspaceConfig;
32
+
33
+ /**
34
+ * Get the lock file name for this package manager
35
+ */
36
+ getLockFileName(): string;
37
+ }
38
+
39
+ export interface PackageManagerDetectionResult {
40
+ packageManager: PackageManager;
41
+ confidence: number;
42
+ }
@@ -0,0 +1,301 @@
1
+ import pc from 'picocolors';
2
+
3
+ interface OutputSymbols {
4
+ rocket: string;
5
+ folder: string;
6
+ package: string;
7
+ checkmark: string;
8
+ crossmark: string;
9
+ warning: string;
10
+ wrench: string;
11
+ lightning: string;
12
+ clock: string;
13
+ target: string;
14
+ magnifying: string;
15
+ chart: string;
16
+ fire: string;
17
+ trophy: string;
18
+ seedling: string;
19
+ leaf: string;
20
+ books: string;
21
+ gear: string;
22
+ construction: string;
23
+ movie: string;
24
+ lightbulb: string;
25
+ sparkles: string;
26
+ party: string;
27
+ boom: string;
28
+ building: string;
29
+ arrow: string;
30
+ dot: string;
31
+ }
32
+
33
+ // Always use ASCII for maximum compatibility
34
+ function supportsUnicode(): boolean {
35
+ // Only use Unicode if explicitly requested
36
+ if (process.env.WSU_UNICODE === '1' || process.env.WSU_UNICODE === 'true') {
37
+ return true;
38
+ }
39
+
40
+ // Default to ASCII for all environments
41
+ return false;
42
+ }
43
+
44
+ // Create symbol sets based on Unicode support
45
+ const UNICODE_SYMBOLS: OutputSymbols = {
46
+ rocket: '🚀',
47
+ folder: '📁',
48
+ package: '📦',
49
+ checkmark: '✅',
50
+ crossmark: '❌',
51
+ warning: '⚠️',
52
+ wrench: '🔧',
53
+ lightning: '🚀',
54
+ clock: '⏱️',
55
+ target: '🎯',
56
+ magnifying: '🔍',
57
+ chart: '📊',
58
+ fire: '💥',
59
+ trophy: '🏆',
60
+ seedling: '🌱',
61
+ leaf: '🍃',
62
+ books: '📚',
63
+ gear: '🔧',
64
+ construction: '🏗️',
65
+ movie: '🎬',
66
+ lightbulb: '💡',
67
+ sparkles: '✨',
68
+ party: '🎉',
69
+ boom: '💥',
70
+ building: '🏢',
71
+ arrow: '🔗',
72
+ dot: '🔸',
73
+ };
74
+
75
+ const ASCII_SYMBOLS: OutputSymbols = {
76
+ rocket: '>',
77
+ folder: '[DIR]',
78
+ package: '[PKG]',
79
+ checkmark: '[OK]',
80
+ crossmark: '[ERR]',
81
+ warning: '[WARN]',
82
+ wrench: '[TOOL]',
83
+ lightning: '[FAST]',
84
+ clock: '[TIME]',
85
+ target: '[TARGET]',
86
+ magnifying: '[FIND]',
87
+ chart: '[CHART]',
88
+ fire: '[BOOM]',
89
+ trophy: '[WIN]',
90
+ seedling: '[ROOT]',
91
+ leaf: '[LEAF]',
92
+ books: '[DOCS]',
93
+ gear: '[GEAR]',
94
+ construction: '[BUILD]',
95
+ movie: '[START]',
96
+ lightbulb: '[TIP]',
97
+ sparkles: '[DONE]',
98
+ party: '[SUCCESS]',
99
+ boom: '[ERROR]',
100
+ building: '[CORP]',
101
+ arrow: '->',
102
+ dot: '*',
103
+ };
104
+
105
+ // Select appropriate symbol set
106
+ const symbols: OutputSymbols = supportsUnicode() ? UNICODE_SYMBOLS : ASCII_SYMBOLS;
107
+
108
+ /**
109
+ * Output utility class for consistent, terminal-compatible logging
110
+ */
111
+ export class Output {
112
+ private static readonly symbols = symbols;
113
+
114
+ /**
115
+ * Log a message with appropriate formatting and symbols
116
+ */
117
+ static log(
118
+ message: string,
119
+ symbol?: keyof OutputSymbols,
120
+ color?: 'blue' | 'green' | 'red' | 'yellow' | 'dim'
121
+ ): void {
122
+ const prefix = symbol ? `${this.symbols[symbol]} ` : '';
123
+ const formattedMessage = `${prefix}${message}`;
124
+
125
+ if (color) {
126
+ console.log(pc[color](formattedMessage));
127
+ } else {
128
+ console.log(formattedMessage);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Log an info message (blue with rocket)
134
+ */
135
+ static info(message: string): void {
136
+ this.log(message, 'rocket', 'blue');
137
+ }
138
+
139
+ /**
140
+ * Log a success message (green with checkmark)
141
+ */
142
+ static success(message: string): void {
143
+ this.log(message, 'checkmark', 'green');
144
+ }
145
+
146
+ /**
147
+ * Log an error message (red with crossmark)
148
+ */
149
+ static error(message: string): void {
150
+ this.log(message, 'crossmark', 'red');
151
+ }
152
+
153
+ /**
154
+ * Log a warning message (yellow with warning)
155
+ */
156
+ static warning(message: string): void {
157
+ this.log(message, 'warning', 'yellow');
158
+ }
159
+
160
+ /**
161
+ * Log a dim/muted message
162
+ */
163
+ static dim(message: string, symbol?: keyof OutputSymbols): void {
164
+ this.log(message, symbol, 'dim');
165
+ }
166
+
167
+ /**
168
+ * Log a build-related message
169
+ */
170
+ static build(message: string): void {
171
+ this.log(message, 'construction', 'blue');
172
+ }
173
+
174
+ /**
175
+ * Log a development-related message
176
+ */
177
+ static dev(message: string): void {
178
+ this.log(message, 'movie', 'blue');
179
+ }
180
+
181
+ /**
182
+ * Log a package-related message
183
+ */
184
+ static package(message: string): void {
185
+ this.log(message, 'package', 'blue');
186
+ }
187
+
188
+ /**
189
+ * Log a timing message
190
+ */
191
+ static timing(message: string): void {
192
+ this.log(message, 'clock', 'blue');
193
+ }
194
+
195
+ /**
196
+ * Log a target/summary message
197
+ */
198
+ static summary(message: string): void {
199
+ this.log(message, 'target', 'blue');
200
+ }
201
+
202
+ /**
203
+ * Log a celebration message
204
+ */
205
+ static celebrate(message: string): void {
206
+ this.log(message, 'party', 'green');
207
+ }
208
+
209
+ /**
210
+ * Log a tip message
211
+ */
212
+ static tip(message: string): void {
213
+ this.log(message, 'lightbulb', 'yellow');
214
+ }
215
+
216
+ /**
217
+ * Create a formatted list item
218
+ */
219
+ static listItem(item: string, indent: number = 2): void {
220
+ const spaces = ' '.repeat(indent);
221
+ this.log(`${spaces}${item}`, 'dot', 'dim');
222
+ }
223
+
224
+ /**
225
+ * Create a separator line
226
+ */
227
+ static separator(): void {
228
+ const line = supportsUnicode()
229
+ ? '────────────────────────────────────────────────────────'
230
+ : '--------------------------------------------------------';
231
+ console.log(pc.dim(line));
232
+ }
233
+
234
+ /**
235
+ * Get a symbol without logging
236
+ */
237
+ static getSymbol(symbol: keyof OutputSymbols): string {
238
+ return this.symbols[symbol];
239
+ }
240
+
241
+ /**
242
+ * Format a package name with brackets
243
+ */
244
+ static formatPackageName(name: string, color?: string): string {
245
+ const formatted = `[${name}]`;
246
+ return color ? pc[color as keyof typeof pc](formatted) : formatted;
247
+ }
248
+
249
+ /**
250
+ * Format duration in a human-readable way
251
+ */
252
+ static formatDuration(ms: number): string {
253
+ if (ms < 1000) {
254
+ return `${ms}ms`;
255
+ } else if (ms < 60000) {
256
+ return `${(ms / 1000).toFixed(1)}s`;
257
+ } else {
258
+ const minutes = Math.floor(ms / 60000);
259
+ const seconds = ((ms % 60000) / 1000).toFixed(0);
260
+ return `${minutes}m ${seconds}s`;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Log execution summary with consistent formatting
266
+ */
267
+ static executionSummary(successful: number, failed: number, totalDuration: number): void {
268
+ console.log(pc.bold(`\n${this.symbols.chart} Execution Summary:`));
269
+ this.success(`Successful: ${successful}`);
270
+
271
+ if (failed > 0) {
272
+ this.error(`Failed: ${failed}`);
273
+ }
274
+
275
+ this.timing(`Total duration: ${this.formatDuration(totalDuration)}`);
276
+ }
277
+
278
+ /**
279
+ * Log build summary with consistent formatting
280
+ */
281
+ static buildSummary(successful: number, failed: number, totalDuration: number): void {
282
+ console.log(pc.bold(`\n${this.symbols.target} Build Summary:`));
283
+ this.success(`Successfully built: ${successful} packages`);
284
+
285
+ if (failed > 0) {
286
+ this.error(`Failed to build: ${failed} packages`);
287
+ }
288
+
289
+ this.timing(`Total build time: ${this.formatDuration(totalDuration)}`);
290
+ }
291
+
292
+ /**
293
+ * Check if Unicode is supported in current environment
294
+ */
295
+ static get supportsUnicode(): boolean {
296
+ return supportsUnicode();
297
+ }
298
+ }
299
+
300
+ // Re-export symbols for direct access if needed
301
+ export { symbols };
@@ -0,0 +1,243 @@
1
+ import type { PackageInfo, WorkspaceInfo } from '../core/workspace.ts';
2
+ import { DependencyGraph } from '../core/dependency-graph.ts';
3
+ import type { ProcessResult } from '../core/process-runner.ts';
4
+ import type { PackageManager } from '../package-managers/index.ts';
5
+
6
+ export interface BuildContext {
7
+ workspace: WorkspaceInfo;
8
+ packages: PackageInfo[];
9
+ dependencyGraph: DependencyGraph;
10
+ }
11
+
12
+ /**
13
+ * Build a dependency graph from workspace packages
14
+ */
15
+ export function buildDependencyGraph(packages: PackageInfo[]): DependencyGraph {
16
+ const graph = new DependencyGraph();
17
+ const packageNames = new Set(packages.map(pkg => pkg.name));
18
+
19
+ // Add all packages to the graph
20
+ packages.forEach(pkg => {
21
+ graph.addPackage(pkg.name);
22
+ });
23
+
24
+ // Add dependency relationships
25
+ packages.forEach(pkg => {
26
+ // Check both dependencies and devDependencies
27
+ const allDeps = [...pkg.dependencies, ...pkg.devDependencies];
28
+
29
+ allDeps.forEach(depName => {
30
+ // Only add dependency if it's also a workspace package
31
+ if (packageNames.has(depName)) {
32
+ graph.addDependency(pkg.name, depName);
33
+ }
34
+ });
35
+ });
36
+
37
+ return graph;
38
+ }
39
+
40
+ /**
41
+ * Filter packages by script availability
42
+ */
43
+ export function filterPackagesByScript(packages: PackageInfo[], scriptName: string): PackageInfo[] {
44
+ return packages.filter(pkg => pkg.scripts[scriptName]);
45
+ }
46
+
47
+ /**
48
+ * Build command arguments for running a script with the specified package manager
49
+ */
50
+ export function buildScriptCommand(
51
+ scriptName: string,
52
+ packageManager: PackageManager
53
+ ): { command: string; args: string[] } {
54
+ return packageManager.getRunCommand(scriptName);
55
+ }
56
+
57
+ /**
58
+ * Validate that packages have the required script
59
+ */
60
+ export function validatePackagesHaveScript(
61
+ packages: PackageInfo[],
62
+ scriptName: string
63
+ ): {
64
+ valid: PackageInfo[];
65
+ invalid: PackageInfo[];
66
+ } {
67
+ const valid: PackageInfo[] = [];
68
+ const invalid: PackageInfo[] = [];
69
+
70
+ packages.forEach(pkg => {
71
+ if (pkg.scripts && typeof pkg.scripts === 'object' && pkg.scripts[scriptName]) {
72
+ valid.push(pkg);
73
+ } else {
74
+ invalid.push(pkg);
75
+ }
76
+ });
77
+
78
+ return { valid, invalid };
79
+ }
80
+
81
+ /**
82
+ * Prepare command execution data for process runner
83
+ */
84
+ export function prepareCommandExecution(
85
+ packages: PackageInfo[],
86
+ scriptName: string,
87
+ packageManager: PackageManager
88
+ ) {
89
+ const { command, args } = buildScriptCommand(scriptName, packageManager);
90
+
91
+ return packages.map(pkg => {
92
+ if (!isValidPackagePath(pkg.path)) {
93
+ throw new Error(`Invalid package path: ${pkg.path}`);
94
+ }
95
+
96
+ return {
97
+ command,
98
+ args,
99
+ options: {
100
+ cwd: pkg.path,
101
+ env: {
102
+ // Ensure consistent environment
103
+ FORCE_COLOR: '1',
104
+ NODE_ENV: process.env.NODE_ENV || 'development',
105
+ },
106
+ },
107
+ logOptions: {
108
+ prefix: pkg.name,
109
+ color: getPackageColor(pkg.name),
110
+ },
111
+ packageInfo: pkg,
112
+ };
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Simple color assignment for packages
118
+ */
119
+ const colorPalette = [
120
+ 'red',
121
+ 'green',
122
+ 'yellow',
123
+ 'blue',
124
+ 'magenta',
125
+ 'cyan',
126
+ 'gray',
127
+ 'redBright',
128
+ 'greenBright',
129
+ 'yellowBright',
130
+ 'blueBright',
131
+ 'magentaBright',
132
+ 'cyanBright',
133
+ ];
134
+
135
+ const packageColors = new Map<string, string>();
136
+ let colorIndex = 0;
137
+
138
+ function getPackageColor(packageName: string): string {
139
+ if (!packageColors.has(packageName)) {
140
+ const color = colorPalette[colorIndex % colorPalette.length];
141
+ if (color) {
142
+ packageColors.set(packageName, color);
143
+ }
144
+ colorIndex++;
145
+ }
146
+ return packageColors.get(packageName) || 'white';
147
+ }
148
+
149
+ /**
150
+ * Format package names for display
151
+ */
152
+ export function formatPackageName(packageName: string, maxLength = 20): string {
153
+ if (packageName.length <= maxLength) {
154
+ return packageName.padEnd(maxLength);
155
+ }
156
+
157
+ // Truncate and add ellipsis
158
+ return packageName.substring(0, maxLength - 3) + '...';
159
+ }
160
+
161
+ /**
162
+ * Group packages by their scope (for @scope/package naming)
163
+ */
164
+ export function groupPackagesByScope(packages: PackageInfo[]): Map<string, PackageInfo[]> {
165
+ const groups = new Map<string, PackageInfo[]>();
166
+
167
+ packages.forEach(pkg => {
168
+ const scope = pkg.name.startsWith('@') ? pkg.name.split('/')[0] || 'unscoped' : 'unscoped';
169
+
170
+ if (!groups.has(scope)) {
171
+ groups.set(scope, []);
172
+ }
173
+ const group = groups.get(scope);
174
+ if (group) {
175
+ group.push(pkg);
176
+ }
177
+ });
178
+
179
+ return groups;
180
+ }
181
+
182
+ /**
183
+ * Calculate execution statistics
184
+ */
185
+ export interface ExecutionStats {
186
+ totalPackages: number;
187
+ successfulPackages: number;
188
+ failedPackages: number;
189
+ totalDuration: number;
190
+ averageDuration: number;
191
+ longestDuration: number;
192
+ shortestDuration: number;
193
+ }
194
+
195
+ export function calculateExecutionStats(results: ProcessResult[]): ExecutionStats {
196
+ const successful = results.filter(r => r.success);
197
+ const failed = results.filter(r => !r.success);
198
+ const durations = results.map(r => r.duration);
199
+ const totalDuration = durations.reduce((sum, d) => sum + d, 0);
200
+ const avgDuration = durations.length > 0 ? Math.round(totalDuration / durations.length) : 0;
201
+ const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
202
+ const minDuration = durations.length > 0 ? Math.min(...durations) : 0;
203
+
204
+ return {
205
+ totalPackages: results.length,
206
+ successfulPackages: successful.length,
207
+ failedPackages: failed.length,
208
+ totalDuration,
209
+ averageDuration: avgDuration,
210
+ longestDuration: maxDuration,
211
+ shortestDuration: minDuration,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Check if a package path exists and is valid
217
+ */
218
+ export function isValidPackagePath(packagePath: string): boolean {
219
+ const fs = require('fs');
220
+ const path = require('path');
221
+
222
+ try {
223
+ const packageJsonPath = path.join(packagePath, 'package.json');
224
+ if (!fs.existsSync(packageJsonPath)) {
225
+ return false;
226
+ }
227
+
228
+ // Try to parse the package.json to ensure it's valid
229
+ const content = fs.readFileSync(packageJsonPath, 'utf8');
230
+ const pkg = JSON.parse(content);
231
+ return typeof pkg === 'object' && pkg !== null && typeof pkg.name === 'string';
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Extract package name from path (for display purposes)
239
+ */
240
+ export function extractPackageNameFromPath(packagePath: string): string {
241
+ const parts = packagePath.split('/');
242
+ return parts[parts.length - 1] || '';
243
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@test/web-app",
3
+ "version": "1.0.0",
4
+ "description": "Web application",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "build": "echo 'Building web-app with Bun...' && sleep 4 && echo 'web-app build complete!'",
8
+ "dev": "echo 'Starting web-app dev server with Bun...' && while true; do echo '[web-app] Dev server running on port 3000...'; sleep 3; done",
9
+ "test": "echo 'Running web-app tests with Bun...' && sleep 2 && echo 'web-app tests passed!'",
10
+ "lint": "echo 'Linting web-app with Bun...' && sleep 1 && echo 'web-app linting complete!'",
11
+ "start": "echo 'Starting web-app production server with Bun...' && echo 'web-app running on port 3000'"
12
+ },
13
+ "dependencies": {
14
+ "@test/shared-utils": "workspace:*",
15
+ "@test/ui-components": "workspace:*"
16
+ },
17
+ "devDependencies": {}
18
+ }
File without changes