workgraph 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # workgraph
2
+
3
+ Workspace dependency analyzer and parallel build orchestrator for npm/yarn/pnpm monorepos.
4
+
5
+ ## Features
6
+
7
+ - **Dependency Graph Analysis** - Scans workspace projects and builds a directed dependency graph
8
+ - **Cycle Detection** - Detects circular dependencies using DFS with coloring
9
+ - **Affected Project Detection** - Determines which projects are affected by changes (transitive dependents)
10
+ - **Parallel Build Planning** - Uses Kahn's algorithm to plan build waves (parallel within wave, sequential between waves)
11
+ - **File Watching** - Monitors file changes with debouncing and triggers rebuilds
12
+ - **Dev Server Management** - Start and manage multiple dev servers with prefixed output
13
+ - **Concurrent Execution** - Executes builds with configurable concurrency limits
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install -g workgraph
19
+ # or
20
+ npm install -D workgraph
21
+ ```
22
+
23
+ ## CLI Usage
24
+
25
+ ### Analyze Dependencies
26
+
27
+ Show the dependency graph and detect cycles:
28
+
29
+ ```bash
30
+ workgraph analyze
31
+ ```
32
+
33
+ Output:
34
+ ```
35
+ Analyzing workspace at: /path/to/workspace
36
+
37
+ Found 6 projects
38
+
39
+ Dependency Graph:
40
+ @myorg/api (apps/api)
41
+ -> @myorg/auth
42
+ @myorg/auth (libs/auth)
43
+ (no dependencies)
44
+ @myorg/web (apps/web)
45
+ -> @myorg/api
46
+
47
+ No cycles detected
48
+ ```
49
+
50
+ ### Plan Build
51
+
52
+ Show what would be built for specific changes:
53
+
54
+ ```bash
55
+ # By package name
56
+ workgraph plan -c @myorg/auth
57
+
58
+ # By shorthand name
59
+ workgraph plan -c auth
60
+
61
+ # By path
62
+ workgraph plan -c libs/auth
63
+
64
+ # Multiple changes
65
+ workgraph plan -c auth -c utils
66
+ ```
67
+
68
+ Output:
69
+ ```
70
+ Changed: @myorg/auth, @myorg/utils
71
+ Affected: @myorg/api, @myorg/auth, @myorg/utils, @myorg/web
72
+
73
+ Build Plan:
74
+ Wave 1 (parallel): @myorg/auth, @myorg/utils
75
+ Wave 2: @myorg/api
76
+ Wave 3: @myorg/web
77
+
78
+ Total: 4 projects in 3 waves
79
+ ```
80
+
81
+ ### Build Affected Projects
82
+
83
+ Execute the build plan:
84
+
85
+ ```bash
86
+ # Build all
87
+ workgraph build
88
+
89
+ # Build affected by specific changes
90
+ workgraph build -c auth
91
+
92
+ # Dry run (show plan without executing)
93
+ workgraph build -c auth --dry-run
94
+
95
+ # With custom concurrency
96
+ workgraph build -c auth --concurrency 2
97
+ ```
98
+
99
+ ### Watch Mode
100
+
101
+ Watch for file changes and automatically rebuild affected projects:
102
+
103
+ ```bash
104
+ # Watch all projects
105
+ workgraph watch
106
+
107
+ # Watch all, but only rebuild libs (for dev with app servers)
108
+ workgraph watch --filter 'libs/*'
109
+
110
+ # Watch libs + start app dev servers
111
+ workgraph watch --filter 'libs/*' api web
112
+
113
+ # Dry run mode
114
+ workgraph watch --dry-run
115
+ ```
116
+
117
+ **Dev workflow (single terminal):**
118
+ ```bash
119
+ # Watch libs + start API and web dev servers
120
+ workgraph watch --filter 'libs/*' api web
121
+ ```
122
+
123
+ Output:
124
+ ```
125
+ Starting dev server: @myorg/api
126
+ Starting dev server: @myorg/web
127
+
128
+ Watching 7 projects for changes...
129
+ Building only projects matching: libs/* (4 projects)
130
+ Press Ctrl+C to stop
131
+
132
+ [api] [Nest] Starting Nest application...
133
+ [web] ➜ Local: http://localhost:3000
134
+ ```
135
+
136
+ ## CLI Options
137
+
138
+ | Option | Description | Default |
139
+ |--------|-------------|---------|
140
+ | `-r, --root <path>` | Workspace root directory | `process.cwd()` |
141
+ | `-c, --changed <projects...>` | Changed projects (names or paths) | `[]` |
142
+ | `--concurrency <number>` | Max parallel builds | CPU count - 1 |
143
+ | `--debounce <ms>` | Debounce time for watch mode | `200` |
144
+ | `--dry-run` | Show plan without executing | `false` |
145
+ | `--filter <pattern>` | Only build projects matching pattern (e.g., `libs/*`) | - |
146
+
147
+ ## Programmatic API
148
+
149
+ ```typescript
150
+ import {
151
+ loadWorkspaceProjects,
152
+ buildGraph,
153
+ detectCycles,
154
+ getAffectedProjects,
155
+ planWaves,
156
+ executePlan,
157
+ createWatcher,
158
+ } from 'workgraph';
159
+
160
+ // Load workspace projects
161
+ const projects = await loadWorkspaceProjects('/path/to/workspace');
162
+
163
+ // Build dependency graph
164
+ const graph = buildGraph(projects);
165
+
166
+ // Check for cycles
167
+ const cycles = detectCycles(graph);
168
+ if (cycles) {
169
+ console.error('Cycles detected:', cycles);
170
+ }
171
+
172
+ // Get affected projects
173
+ const changed = new Set(['@myorg/auth']);
174
+ const affected = getAffectedProjects(changed, graph.rdeps);
175
+
176
+ // Plan build waves
177
+ const waves = planWaves(affected, graph.deps);
178
+ // Result: [['@myorg/auth'], ['@myorg/api'], ['@myorg/web']]
179
+
180
+ // Execute build plan
181
+ const result = await executePlan(waves, projects, '/path/to/workspace', {
182
+ concurrency: 4,
183
+ dryRun: false,
184
+ });
185
+ ```
186
+
187
+ ## Algorithm Details
188
+
189
+ ### Dependency Graph
190
+
191
+ The library builds two maps:
192
+ - `deps[A] = Set<B>` - A depends on B
193
+ - `rdeps[B] = Set<A>` - B is a dependency of A (reverse graph)
194
+
195
+ Dependencies are collected from:
196
+ - `dependencies`
197
+ - `devDependencies`
198
+ - `peerDependencies`
199
+ - `optionalDependencies`
200
+
201
+ ### Cycle Detection
202
+
203
+ Uses DFS with three-color marking:
204
+ - WHITE (0): Unvisited
205
+ - GRAY (1): Currently visiting (in stack)
206
+ - BLACK (2): Fully visited
207
+
208
+ A back edge to a GRAY node indicates a cycle.
209
+
210
+ ### Wave Planning (Kahn's Algorithm)
211
+
212
+ 1. Build induced subgraph from affected projects
213
+ 2. Calculate in-degree for each node (count of unbuilt dependencies)
214
+ 3. Repeat until all nodes processed:
215
+ - Wave N = all nodes with in-degree 0
216
+ - Remove wave nodes and decrement dependents' in-degrees
217
+
218
+ Projects in the same wave have no dependencies on each other and can be built in parallel.
219
+
220
+ ### Affected Detection
221
+
222
+ Uses BFS on the reverse dependency graph:
223
+ 1. Start with changed projects
224
+ 2. Add all transitive dependents via `rdeps`
225
+ 3. Result includes original changes + all projects that depend on them
226
+
227
+ ## Ignored Paths
228
+
229
+ The watcher ignores these patterns by default:
230
+ - `**/node_modules/**`
231
+ - `**/dist/**`
232
+ - `**/.angular/**`
233
+ - `**/.nx/**`
234
+ - `**/coverage/**`
235
+ - `**/*.log`
236
+ - `**/.git/**`
237
+ - `**/tmp/**`
238
+ - `**/.cache/**`
239
+
240
+ ## Root Config Changes
241
+
242
+ Changes to root configuration files trigger a rebuild of all projects:
243
+ - `package.json`
244
+ - `package-lock.json`
245
+ - `pnpm-lock.yaml`
246
+ - `yarn.lock`
247
+ - `tsconfig.json`
248
+ - `tsconfig.base.json`
249
+
250
+ ## License
251
+
252
+ MIT
@@ -0,0 +1,7 @@
1
+ import { DependencyGraph, Project } from './types';
2
+ export declare function getAffectedProjects(changedProjects: Set<string>, rdeps: Map<string, Set<string>>): Set<string>;
3
+ export declare function getChangedProjectsFromFiles(changedFiles: string[], projects: Map<string, Project>, root: string): {
4
+ changedProjects: Set<string>;
5
+ isGlobalChange: boolean;
6
+ };
7
+ export declare function resolveProjectNames(projectIdentifiers: string[], graph: DependencyGraph): Set<string>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAffectedProjects = getAffectedProjects;
4
+ exports.getChangedProjectsFromFiles = getChangedProjectsFromFiles;
5
+ exports.resolveProjectNames = resolveProjectNames;
6
+ const workspace_1 = require("./workspace");
7
+ function getAffectedProjects(changedProjects, rdeps) {
8
+ const affected = new Set();
9
+ const queue = [...changedProjects];
10
+ while (queue.length > 0) {
11
+ const current = queue.shift();
12
+ if (affected.has(current))
13
+ continue;
14
+ affected.add(current);
15
+ const dependents = rdeps.get(current) || new Set();
16
+ for (const dependent of dependents) {
17
+ if (!affected.has(dependent)) {
18
+ queue.push(dependent);
19
+ }
20
+ }
21
+ }
22
+ return affected;
23
+ }
24
+ function getChangedProjectsFromFiles(changedFiles, projects, root) {
25
+ const changedProjects = new Set();
26
+ let isGlobalChange = false;
27
+ for (const file of changedFiles) {
28
+ if ((0, workspace_1.isRootConfig)(file, root)) {
29
+ isGlobalChange = true;
30
+ continue;
31
+ }
32
+ const project = (0, workspace_1.getProjectFromPath)(file, projects, root);
33
+ if (project) {
34
+ changedProjects.add(project);
35
+ }
36
+ }
37
+ return { changedProjects, isGlobalChange };
38
+ }
39
+ function resolveProjectNames(projectIdentifiers, graph) {
40
+ const resolved = new Set();
41
+ for (const identifier of projectIdentifiers) {
42
+ if (graph.projects.has(identifier)) {
43
+ resolved.add(identifier);
44
+ continue;
45
+ }
46
+ for (const [name, project] of graph.projects) {
47
+ if (project.path === identifier || project.absolutePath === identifier) {
48
+ resolved.add(name);
49
+ break;
50
+ }
51
+ }
52
+ for (const name of graph.projects.keys()) {
53
+ if (name.endsWith('/' + identifier) || name === identifier) {
54
+ resolved.add(name);
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ return resolved;
60
+ }
61
+ //# sourceMappingURL=affected.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affected.js","sourceRoot":"","sources":["../src/affected.ts"],"names":[],"mappings":";;AAGA,kDAsBC;AAED,kEAqBC;AAED,kDA+BC;AAhFD,2CAA+D;AAE/D,SAAgB,mBAAmB,CACjC,eAA4B,EAC5B,KAA+B;IAE/B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,MAAM,KAAK,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC;IAEnC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;QAE/B,IAAI,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,SAAS;QACpC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEtB,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;QACnD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAgB,2BAA2B,CACzC,YAAsB,EACtB,QAA8B,EAC9B,IAAY;IAEZ,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC1C,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,IAAA,wBAAY,EAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAC7B,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,IAAA,8BAAkB,EAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QACzD,IAAI,OAAO,EAAE,CAAC;YACZ,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AAC7C,CAAC;AAED,SAAgB,mBAAmB,CACjC,kBAA4B,EAC5B,KAAsB;IAEtB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAEnC,KAAK,MAAM,UAAU,IAAI,kBAAkB,EAAE,CAAC;QAE5C,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACnC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACzB,SAAS;QACX,CAAC;QAGD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7C,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,IAAI,OAAO,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;gBACvE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,MAAM;YACR,CAAC;QACH,CAAC;QAGD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACzC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,UAAU,CAAC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC3D,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const child_process_1 = require("child_process");
39
+ const path = __importStar(require("path"));
40
+ const workspace_1 = require("./workspace");
41
+ const graph_1 = require("./graph");
42
+ const affected_1 = require("./affected");
43
+ const planner_1 = require("./planner");
44
+ const watcher_1 = require("./watcher");
45
+ const executor_1 = require("./executor");
46
+ const program = new commander_1.Command();
47
+ program
48
+ .name('workgraph')
49
+ .description('Lightweight workspace dependency graph and parallel build orchestrator for monorepos')
50
+ .version('0.0.1');
51
+ program
52
+ .command('analyze')
53
+ .description('Analyze workspace dependencies and show graph')
54
+ .option('-r, --root <path>', 'Workspace root directory', process.cwd())
55
+ .action(async (options) => {
56
+ try {
57
+ const root = path.resolve(options.root);
58
+ console.log(`Analyzing workspace at: ${root}\n`);
59
+ const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
60
+ console.log(`Found ${projects.size} projects\n`);
61
+ const graph = (0, graph_1.buildGraph)(projects);
62
+ console.log('Dependency Graph:');
63
+ console.log((0, graph_1.formatGraph)(graph));
64
+ console.log();
65
+ const cycles = (0, graph_1.detectCycles)(graph);
66
+ if (cycles) {
67
+ console.log('Cycles detected:');
68
+ for (const cycle of cycles) {
69
+ console.log(` ${cycle.join(' -> ')}`);
70
+ }
71
+ process.exit(1);
72
+ }
73
+ else {
74
+ console.log('No cycles detected');
75
+ }
76
+ }
77
+ catch (error) {
78
+ console.error('Error:', error.message);
79
+ process.exit(1);
80
+ }
81
+ });
82
+ program
83
+ .command('plan')
84
+ .description('Show build plan for changed projects')
85
+ .option('-r, --root <path>', 'Workspace root directory', process.cwd())
86
+ .option('-c, --changed <projects...>', 'Changed projects (names or paths)', [])
87
+ .action(async (options) => {
88
+ try {
89
+ const root = path.resolve(options.root);
90
+ const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
91
+ const graph = (0, graph_1.buildGraph)(projects);
92
+ const cycles = (0, graph_1.detectCycles)(graph);
93
+ if (cycles) {
94
+ console.error('Cannot plan: cycles detected in dependency graph');
95
+ process.exit(1);
96
+ }
97
+ let changedProjects;
98
+ if (options.changed.length === 0) {
99
+ console.log('No --changed specified, showing plan for all projects\n');
100
+ changedProjects = new Set(projects.keys());
101
+ }
102
+ else {
103
+ changedProjects = (0, affected_1.resolveProjectNames)(options.changed, graph);
104
+ if (changedProjects.size === 0) {
105
+ console.error('Could not resolve any projects from:', options.changed);
106
+ process.exit(1);
107
+ }
108
+ console.log(`Changed: ${[...changedProjects].join(', ')}\n`);
109
+ }
110
+ const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
111
+ const plan = (0, planner_1.createBuildPlan)(affected, graph.deps);
112
+ console.log((0, planner_1.formatBuildPlan)(plan));
113
+ }
114
+ catch (error) {
115
+ console.error('Error:', error.message);
116
+ process.exit(1);
117
+ }
118
+ });
119
+ program
120
+ .command('build')
121
+ .description('Build affected projects')
122
+ .option('-r, --root <path>', 'Workspace root directory', process.cwd())
123
+ .option('-c, --changed <projects...>', 'Changed projects (names or paths)', [])
124
+ .option('--concurrency <number>', 'Max parallel builds', String(4))
125
+ .option('--dry-run', 'Show what would be built without executing')
126
+ .action(async (options) => {
127
+ try {
128
+ const root = path.resolve(options.root);
129
+ const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
130
+ const graph = (0, graph_1.buildGraph)(projects);
131
+ const cycles = (0, graph_1.detectCycles)(graph);
132
+ if (cycles) {
133
+ console.error('Cannot build: cycles detected in dependency graph');
134
+ process.exit(1);
135
+ }
136
+ let changedProjects;
137
+ if (options.changed.length === 0) {
138
+ changedProjects = new Set(projects.keys());
139
+ }
140
+ else {
141
+ changedProjects = (0, affected_1.resolveProjectNames)(options.changed, graph);
142
+ }
143
+ const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
144
+ const plan = (0, planner_1.createBuildPlan)(affected, graph.deps);
145
+ console.log((0, planner_1.formatBuildPlan)(plan));
146
+ console.log();
147
+ if (plan.waves.length === 0) {
148
+ console.log('Nothing to build');
149
+ return;
150
+ }
151
+ const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
152
+ concurrency: parseInt(options.concurrency, 10),
153
+ dryRun: options.dryRun,
154
+ onStart: (project) => {
155
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${project}`);
156
+ },
157
+ onComplete: (result) => {
158
+ const status = result.success ? 'done' : 'FAILED';
159
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] ${result.project}: ${status} (${result.duration}ms)`);
160
+ if (!result.success && result.error) {
161
+ console.error(result.error);
162
+ }
163
+ },
164
+ });
165
+ console.log();
166
+ if (result.success) {
167
+ console.log(`Build complete in ${result.duration}ms`);
168
+ }
169
+ else {
170
+ console.error(`Build failed after ${result.duration}ms`);
171
+ process.exit(1);
172
+ }
173
+ }
174
+ catch (error) {
175
+ console.error('Error:', error.message);
176
+ process.exit(1);
177
+ }
178
+ });
179
+ program
180
+ .command('watch')
181
+ .description('Watch for changes and rebuild affected projects')
182
+ .argument('[apps...]', 'Apps to run dev servers for (e.g., api web-angular)')
183
+ .option('-r, --root <path>', 'Workspace root directory', process.cwd())
184
+ .option('--concurrency <number>', 'Max parallel builds', String(4))
185
+ .option('--debounce <ms>', 'Debounce time in milliseconds', String(200))
186
+ .option('--dry-run', 'Show what would be built without executing')
187
+ .option('--filter <pattern>', 'Only build projects matching pattern (e.g., "libs/*")')
188
+ .action(async (apps, options) => {
189
+ try {
190
+ const root = path.resolve(options.root);
191
+ const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
192
+ const graph = (0, graph_1.buildGraph)(projects);
193
+ const cycles = (0, graph_1.detectCycles)(graph);
194
+ if (cycles) {
195
+ console.error('Cannot watch: cycles detected in dependency graph');
196
+ process.exit(1);
197
+ }
198
+ const devProcesses = [];
199
+ if (apps.length > 0) {
200
+ const resolvedApps = (0, affected_1.resolveProjectNames)(apps, graph);
201
+ if (resolvedApps.size === 0) {
202
+ console.error('Could not resolve any apps from:', apps);
203
+ process.exit(1);
204
+ }
205
+ for (const appName of resolvedApps) {
206
+ const project = projects.get(appName);
207
+ if (!project)
208
+ continue;
209
+ const hasDevScript = project.packageJson.scripts?.dev;
210
+ if (!hasDevScript) {
211
+ console.warn(`Warning: ${appName} has no dev script, skipping`);
212
+ continue;
213
+ }
214
+ console.log(`Starting dev server: ${appName}`);
215
+ const proc = (0, child_process_1.spawn)('npm', ['run', 'dev', '-w', appName], {
216
+ cwd: root,
217
+ stdio: ['ignore', 'pipe', 'pipe'],
218
+ shell: true,
219
+ });
220
+ const shortName = appName.includes('/') ? appName.split('/').pop() : appName;
221
+ const prefix = `[${shortName}]`;
222
+ proc.stdout?.on('data', (data) => {
223
+ const lines = data.toString().trim().split('\n');
224
+ for (const line of lines) {
225
+ console.log(`${prefix} ${line}`);
226
+ }
227
+ });
228
+ proc.stderr?.on('data', (data) => {
229
+ const lines = data.toString().trim().split('\n');
230
+ for (const line of lines) {
231
+ console.error(`${prefix} ${line}`);
232
+ }
233
+ });
234
+ proc.on('close', (code) => {
235
+ console.log(`${prefix} exited with code ${code}`);
236
+ });
237
+ devProcesses.push(proc);
238
+ }
239
+ const cleanup = () => {
240
+ for (const proc of devProcesses) {
241
+ proc.kill();
242
+ }
243
+ process.exit(0);
244
+ };
245
+ process.on('SIGINT', cleanup);
246
+ process.on('SIGTERM', cleanup);
247
+ console.log();
248
+ }
249
+ const filterPattern = options.filter;
250
+ const matchesFilter = (projectName) => {
251
+ if (!filterPattern)
252
+ return true;
253
+ const project = projects.get(projectName);
254
+ if (!project)
255
+ return false;
256
+ if (filterPattern.includes('*')) {
257
+ const regex = new RegExp('^' + filterPattern.replace(/\*/g, '.*') + '$');
258
+ return regex.test(project.path);
259
+ }
260
+ return project.path.startsWith(filterPattern) || projectName.includes(filterPattern);
261
+ };
262
+ const filteredCount = filterPattern
263
+ ? [...projects.keys()].filter(matchesFilter).length
264
+ : projects.size;
265
+ console.log(`Watching ${projects.size} projects for changes...`);
266
+ if (filterPattern) {
267
+ console.log(`Building only projects matching: ${filterPattern} (${filteredCount} projects)`);
268
+ }
269
+ console.log('Press Ctrl+C to stop\n');
270
+ let isBuilding = false;
271
+ let pendingChanges = null;
272
+ const handleChanges = async (changedProjects) => {
273
+ if (isBuilding) {
274
+ pendingChanges = pendingChanges || new Set();
275
+ for (const p of changedProjects) {
276
+ pendingChanges.add(p);
277
+ }
278
+ return;
279
+ }
280
+ isBuilding = true;
281
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] Changes detected: ${[...changedProjects].join(', ')}`);
282
+ const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
283
+ const filteredAffected = new Set([...affected].filter(matchesFilter));
284
+ if (filteredAffected.size === 0) {
285
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] No matching projects to build\n`);
286
+ isBuilding = false;
287
+ return;
288
+ }
289
+ const plan = (0, planner_1.createBuildPlan)(filteredAffected, graph.deps);
290
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${[...filteredAffected].join(', ')}`);
291
+ if (plan.waves.length > 0) {
292
+ const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
293
+ concurrency: parseInt(options.concurrency, 10),
294
+ dryRun: options.dryRun,
295
+ onStart: (project) => {
296
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${project}`);
297
+ },
298
+ onComplete: (buildResult) => {
299
+ const status = buildResult.success ? 'done' : 'FAILED';
300
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] ${buildResult.project}: ${status} (${buildResult.duration}ms)`);
301
+ },
302
+ });
303
+ if (result.success) {
304
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] Build complete\n`);
305
+ }
306
+ else {
307
+ console.error(`[${(0, watcher_1.formatTimestamp)()}] Build failed\n`);
308
+ }
309
+ }
310
+ isBuilding = false;
311
+ if (pendingChanges && pendingChanges.size > 0) {
312
+ const next = pendingChanges;
313
+ pendingChanges = null;
314
+ await handleChanges(next);
315
+ }
316
+ };
317
+ (0, watcher_1.createWatcher)({
318
+ root,
319
+ debounceMs: parseInt(options.debounce, 10),
320
+ onChange: handleChanges,
321
+ }, projects);
322
+ }
323
+ catch (error) {
324
+ console.error('Error:', error.message);
325
+ process.exit(1);
326
+ }
327
+ });
328
+ program.parse();
329
+ //# sourceMappingURL=cli.js.map