workgraph 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -1,6 +1,10 @@
1
- # workgraph
2
-
3
- Workspace dependency analyzer and parallel build orchestrator for npm/yarn/pnpm monorepos.
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.png">
4
+ <source media="(prefers-color-scheme: light)" srcset="assets/logo.png">
5
+ <img src="assets/logo.png" alt="workgraph Logo" width="500">
6
+ </picture>
7
+ </div>
4
8
 
5
9
  ## Features
6
10
 
@@ -12,16 +16,17 @@ Workspace dependency analyzer and parallel build orchestrator for npm/yarn/pnpm
12
16
  - **Dev Server Management** - Start and manage multiple dev servers with prefixed output
13
17
  - **Concurrent Execution** - Executes builds with configurable concurrency limits
14
18
  - **Source Generation** - Automatically run code generators for missing/generated sources
19
+ - **Terminal UI** - Split-screen interface with tasks panel, build logs, and task output
20
+
21
+ ## Usage
15
22
 
16
- ## Installation
23
+ Run directly with npx:
17
24
 
18
25
  ```bash
19
- npm install -g workgraph
20
- # or
21
- npm install -D workgraph
26
+ npx workgraph <command>
22
27
  ```
23
28
 
24
- ## CLI Usage
29
+ ## CLI Commands
25
30
 
26
31
  ### Analyze Dependencies
27
32
 
@@ -160,17 +165,35 @@ workgraph watch --dry-run
160
165
  workgraph watch --filter 'libs/*' api web
161
166
  ```
162
167
 
163
- Output:
168
+ ### Terminal UI
169
+
170
+ Watch mode displays a 3-panel terminal interface:
171
+
172
+ ```
173
+ ┌─ Tasks ─────┬─ Workgraph ──────────────┬─ Task Output ──────────────┐
174
+ │ ● api 12345 │ [14:32:01] Watching... │ [api] Server started │
175
+ │ ● web 12346 │ [14:32:05] Building auth │ [web] Compiled successfully│
176
+ │ ○ build:auth│ [14:32:06] auth: done │ │
177
+ └─────────────┴──────────────────────────┴────────────────────────────┘
164
178
  ```
165
- Starting dev server: @myorg/api
166
- Starting dev server: @myorg/web
167
179
 
168
- Watching 7 projects for changes...
169
- Building only projects matching: libs/* (4 projects)
170
- Press Ctrl+C to stop
180
+ **Panels:**
181
+ - **Tasks** - Running processes with status icons and PIDs
182
+ - **Workgraph** - Build logs with timestamps
183
+ - **Task Output** - Dev server and build output
171
184
 
172
- [api] [Nest] Starting Nest application...
173
- [web] Local: http://localhost:3000
185
+ **Task Status Icons:**
186
+ - `●` green - Running
187
+ - `○` gray - Stopped
188
+ - `✖` red - Error
189
+
190
+ **Navigation:**
191
+ - `Tab` - Switch focus between panels
192
+ - `Ctrl+C` - Stop all processes and exit
193
+
194
+ To disable the UI and use plain console output:
195
+ ```bash
196
+ workgraph watch --no-ui
174
197
  ```
175
198
 
176
199
  ## CLI Options
@@ -179,10 +202,12 @@ Press Ctrl+C to stop
179
202
  |--------|-------------|---------|
180
203
  | `-r, --root <path>` | Workspace root directory | `process.cwd()` |
181
204
  | `-c, --changed <projects...>` | Changed projects (names or paths) | `[]` |
182
- | `--concurrency <number>` | Max parallel builds | CPU count - 1 |
205
+ | `--concurrency <number>` | Max parallel builds | `4` |
183
206
  | `--debounce <ms>` | Debounce time for watch mode | `200` |
184
207
  | `--dry-run` | Show plan without executing | `false` |
185
208
  | `--filter <pattern>` | Only build projects matching pattern (e.g., `libs/*`) | - |
209
+ | `--no-ui` | Disable split-screen terminal UI (watch mode) | `false` |
210
+ | `--verbose` | Show detailed watcher output | `false` |
186
211
 
187
212
  ## Source Generation
188
213
 
@@ -196,15 +221,41 @@ Add `workgraph.sources` to your root `package.json`:
196
221
  {
197
222
  "workgraph": {
198
223
  "sources": {
199
- "apps/web-angular/src/generated": "npx exposify-codegen api -o ./apps/web-angular/src/generated"
224
+ "apps/web-angular/src/generated": {
225
+ "command": "npx exposify-codegen api --output ./apps/web-angular/src/generated --target angular",
226
+ "deps": ["api"]
227
+ },
228
+ "apps/web-preact/src/generated": {
229
+ "command": "npx exposify-codegen api --output ./apps/web-preact/src/generated --target preact",
230
+ "deps": ["api"]
231
+ }
200
232
  }
201
233
  }
202
234
  }
203
235
  ```
204
236
 
237
+ You can also use the shorthand string format for simple generators:
238
+
239
+ ```json
240
+ {
241
+ "workgraph": {
242
+ "sources": {
243
+ "libs/types/src/generated": "npx generate-types"
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ ### Configuration Options
250
+
251
+ | Field | Description |
252
+ |-------|-------------|
253
+ | `command` | The shell command to run for generation |
254
+ | `deps` | Array of project names/paths that trigger this generator when changed |
255
+
205
256
  ### How It Works
206
257
 
207
- 1. When `build` or `watch` runs, workgraph checks if any affected project uses a configured source path
258
+ 1. When `build` or `watch` runs, workgraph checks if any affected project matches a generator's `deps`
208
259
  2. If so, the source generation command runs **before** the build
209
260
  3. This ensures generated code is up-to-date before compilation
210
261
 
package/dist/cli.js CHANGED
@@ -44,14 +44,15 @@ const planner_1 = require("./planner");
44
44
  const watcher_1 = require("./watcher");
45
45
  const executor_1 = require("./executor");
46
46
  const source_scanner_1 = require("./source-scanner");
47
+ const ui_1 = require("./ui");
47
48
  function normalizeSourceConfig(config) {
48
49
  if (typeof config === 'string') {
49
- return { command: config };
50
+ return { command: config, deps: [] };
50
51
  }
51
52
  return config;
52
53
  }
53
54
  function shouldRunGenerator(sourcePath, sourceConfig, affectedProjects, projects, root) {
54
- if (sourceConfig.deps && sourceConfig.deps.length > 0) {
55
+ if (sourceConfig.deps.length > 0) {
55
56
  for (const dep of sourceConfig.deps) {
56
57
  if (affectedProjects.has(dep)) {
57
58
  return true;
@@ -78,18 +79,25 @@ function shouldRunGenerator(sourcePath, sourceConfig, affectedProjects, projects
78
79
  }
79
80
  return false;
80
81
  }
81
- async function runSourceGenerators(root, affectedProjects, projects, dryRun = false) {
82
+ async function runSourceGeneratorsWithUI(root, affectedProjects, projects, dryRun = false, callbacks = {
83
+ log: (msg) => console.log(`[${(0, watcher_1.formatTimestamp)()}] ${msg}`),
84
+ taskLog: console.log,
85
+ addTask: () => { },
86
+ updateTask: () => { },
87
+ }) {
88
+ const { log, taskLog, addTask, updateTask } = callbacks;
82
89
  const config = (0, source_scanner_1.loadWorkgraphConfig)(root);
83
- const sources = config.sources || {};
90
+ const sources = config.sources;
84
91
  const generated = [];
85
92
  for (const [sourcePath, rawConfig] of Object.entries(sources)) {
86
93
  const sourceConfig = normalizeSourceConfig(rawConfig);
87
94
  if (!shouldRunGenerator(sourcePath, sourceConfig, affectedProjects, projects, root)) {
88
95
  continue;
89
96
  }
90
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Generating: ${sourcePath}`);
97
+ const taskId = `gen-${sourcePath}`;
98
+ log(`Generating: ${sourcePath}`);
99
+ taskLog(`\x1b[33m$ ${sourceConfig.command}\x1b[0m`);
91
100
  if (dryRun) {
92
- console.log(`[${(0, watcher_1.formatTimestamp)()}] [dry-run] Would run: ${sourceConfig.command}`);
93
101
  generated.push(sourcePath);
94
102
  continue;
95
103
  }
@@ -100,28 +108,46 @@ async function runSourceGenerators(root, affectedProjects, projects, dryRun = fa
100
108
  shell: true,
101
109
  stdio: 'pipe',
102
110
  });
111
+ if (proc.pid) {
112
+ addTask({ id: taskId, name: `gen:${sourcePath}`, pid: proc.pid, status: 'running' });
113
+ }
103
114
  let output = '';
104
- proc.stdout?.on('data', (data) => (output += data));
105
- proc.stderr?.on('data', (data) => (output += data));
115
+ proc.stdout?.on('data', (data) => {
116
+ const text = data.toString();
117
+ output += text;
118
+ text.split('\n').forEach((line) => {
119
+ if (line.trim())
120
+ taskLog(line);
121
+ });
122
+ });
123
+ proc.stderr?.on('data', (data) => {
124
+ const text = data.toString();
125
+ output += text;
126
+ text.split('\n').forEach((line) => {
127
+ if (line.trim())
128
+ taskLog(line);
129
+ });
130
+ });
106
131
  proc.on('close', (code) => {
132
+ updateTask(taskId, code === 0 ? 'stopped' : 'error');
107
133
  resolve({ success: code === 0, output });
108
134
  });
109
135
  proc.on('error', (err) => {
136
+ updateTask(taskId, 'error');
110
137
  resolve({ success: false, output: err.message });
111
138
  });
112
139
  });
113
140
  if (result.success) {
114
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Generated successfully`);
141
+ log('Generated successfully');
115
142
  generated.push(sourcePath);
116
143
  }
117
144
  else {
118
- console.error(`[${(0, watcher_1.formatTimestamp)()}] Generation FAILED`);
119
- console.error(result.output);
145
+ log('Generation FAILED');
120
146
  return { success: false, generated };
121
147
  }
122
148
  }
123
149
  catch (error) {
124
- console.error(`[${(0, watcher_1.formatTimestamp)()}] Generation FAILED: ${error.message}`);
150
+ log(`Generation FAILED: ${error.message}`);
125
151
  return { success: false, generated };
126
152
  }
127
153
  }
@@ -179,6 +205,8 @@ program
179
205
  const config = (0, source_scanner_1.loadWorkgraphConfig)(root);
180
206
  for (const sourcePath of result.configuredSources) {
181
207
  const sourceConfig = config.sources[sourcePath];
208
+ if (!sourceConfig)
209
+ continue;
182
210
  const command = typeof sourceConfig === 'string' ? sourceConfig : sourceConfig.command;
183
211
  console.log(` ${sourcePath}`);
184
212
  console.log(` -> ${command}`);
@@ -242,7 +270,11 @@ program
242
270
  .option('-c, --changed <projects...>', 'Changed projects (names or paths)', [])
243
271
  .option('--concurrency <number>', 'Max parallel builds', String(4))
244
272
  .option('--dry-run', 'Show what would be built without executing')
245
- .action(async (options) => {
273
+ .action(async (rawOptions) => {
274
+ const options = {
275
+ ...rawOptions,
276
+ dryRun: rawOptions.dryRun === true,
277
+ };
246
278
  try {
247
279
  const root = path.resolve(options.root);
248
280
  const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
@@ -267,7 +299,7 @@ program
267
299
  console.log('Nothing to build');
268
300
  return;
269
301
  }
270
- const sourceResult = await runSourceGenerators(root, affected, projects, options.dryRun);
302
+ const sourceResult = await runSourceGeneratorsWithUI(root, affected, projects, options.dryRun);
271
303
  if (!sourceResult.success) {
272
304
  console.error('Source generation failed, aborting build');
273
305
  process.exit(1);
@@ -282,11 +314,11 @@ program
282
314
  const mode = info.isParallel ? 'parallel' : 'sequential';
283
315
  console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
284
316
  },
285
- onComplete: (result) => {
286
- const status = result.success ? 'done' : 'FAILED';
287
- console.log(`[${(0, watcher_1.formatTimestamp)()}] ${result.project}: ${status} (${result.duration}ms)`);
288
- if (!result.success && result.error) {
289
- console.error(result.error);
317
+ onComplete: (buildResult) => {
318
+ const status = buildResult.success ? 'done' : 'FAILED';
319
+ console.log(`[${(0, watcher_1.formatTimestamp)()}] ${buildResult.project}: ${status} (${buildResult.duration}ms)`);
320
+ if (!buildResult.success && buildResult.error) {
321
+ console.error(buildResult.error);
290
322
  }
291
323
  },
292
324
  });
@@ -314,7 +346,14 @@ program
314
346
  .option('--dry-run', 'Show what would be built without executing')
315
347
  .option('--filter <pattern>', 'Only build projects matching pattern (e.g., "libs/*")')
316
348
  .option('--verbose', 'Show detailed watcher and build output')
317
- .action(async (apps, options) => {
349
+ .option('--no-ui', 'Disable split-screen UI')
350
+ .action(async (apps, rawOptions) => {
351
+ const options = {
352
+ ...rawOptions,
353
+ dryRun: rawOptions.dryRun === true,
354
+ filter: rawOptions.filter !== undefined ? rawOptions.filter : '',
355
+ verbose: rawOptions.verbose === true,
356
+ };
318
357
  try {
319
358
  const root = path.resolve(options.root);
320
359
  const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
@@ -324,7 +363,52 @@ program
324
363
  console.error('Cannot watch: cycles detected in dependency graph');
325
364
  process.exit(1);
326
365
  }
366
+ const ui = options.ui !== false ? (0, ui_1.createUI)() : null;
367
+ const log = ui ? ui.log : (msg) => console.log(`[${(0, watcher_1.formatTimestamp)()}] ${msg}`);
368
+ const logRaw = ui ? (msg) => ui.leftPane.log(msg) : (msg) => console.log(msg);
369
+ const taskLog = ui ? ui.taskLog : (msg) => console.log(msg);
370
+ const setStatus = ui ? ui.setStatus : () => { };
371
+ const addTask = ui ? ui.addTask : () => { };
372
+ const updateTask = ui ? ui.updateTask : () => { };
373
+ const updateTaskPort = ui ? ui.updateTaskPort : () => { };
327
374
  const devProcesses = [];
375
+ const cleanup = () => {
376
+ if (ui) {
377
+ ui.destroy();
378
+ }
379
+ const dim = '\x1b[2m';
380
+ const green = '\x1b[32m';
381
+ const red = '\x1b[31m';
382
+ const reset = '\x1b[0m';
383
+ process.stdout.write('\x1b[2J\x1b[H');
384
+ const output = [''];
385
+ if (devProcesses.length > 0) {
386
+ output.push(`${dim}Stopping ${devProcesses.length} dev server(s)${reset}`);
387
+ for (const { proc, name } of devProcesses) {
388
+ if (proc.pid) {
389
+ const shortName = name.includes('/') ? name.split('/').pop() : name;
390
+ try {
391
+ process.kill(-proc.pid, 'SIGKILL');
392
+ output.push(` ${green}✓${reset} ${shortName}`);
393
+ }
394
+ catch {
395
+ try {
396
+ process.kill(proc.pid, 'SIGKILL');
397
+ output.push(` ${green}✓${reset} ${shortName}`);
398
+ }
399
+ catch {
400
+ output.push(` ${red}✗${reset} ${shortName} ${dim}(already stopped)${reset}`);
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
406
+ output.push('');
407
+ process.stdout.write(output.join('\n'));
408
+ process.exit(0);
409
+ };
410
+ process.on('SIGINT', cleanup);
411
+ process.on('SIGTERM', cleanup);
328
412
  if (apps.length > 0) {
329
413
  const resolvedApps = (0, affected_1.resolveProjectNames)(apps, graph);
330
414
  if (resolvedApps.size === 0) {
@@ -333,17 +417,17 @@ program
333
417
  }
334
418
  const depsToBuilt = new Set();
335
419
  for (const appName of resolvedApps) {
336
- const appDeps = graph.deps.get(appName) || new Set();
420
+ const appDeps = graph.deps.get(appName) ?? new Set();
337
421
  for (const dep of appDeps) {
338
422
  depsToBuilt.add(dep);
339
- const transitiveDeps = graph.deps.get(dep) || new Set();
423
+ const transitiveDeps = graph.deps.get(dep) ?? new Set();
340
424
  for (const td of transitiveDeps) {
341
425
  depsToBuilt.add(td);
342
426
  }
343
427
  }
344
428
  }
345
429
  if (depsToBuilt.size > 0) {
346
- console.log(`Building ${depsToBuilt.size} dependencies first...\n`);
430
+ log(`Building ${depsToBuilt.size} dependencies first...`);
347
431
  const plan = (0, planner_1.createBuildPlan)(depsToBuilt, graph.deps);
348
432
  if (plan.waves.length > 0) {
349
433
  const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
@@ -351,74 +435,90 @@ program
351
435
  dryRun: options.dryRun,
352
436
  onStart: (info) => {
353
437
  const mode = info.isParallel ? 'parallel' : 'sequential';
354
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
438
+ log(`Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
439
+ const shortName = info.project.includes('/') ? info.project.split('/').pop() : info.project;
440
+ addTask({ id: `build-${info.project}`, name: `build:${shortName}`, pid: 0, status: 'running' });
355
441
  },
356
442
  onComplete: (buildResult) => {
357
443
  const status = buildResult.success ? 'done' : 'FAILED';
358
- console.log(`[${(0, watcher_1.formatTimestamp)()}] ${buildResult.project}: ${status} (${buildResult.duration}ms)`);
444
+ log(`${buildResult.project}: ${status} (${buildResult.duration}ms)`);
445
+ updateTask(`build-${buildResult.project}`, buildResult.success ? 'stopped' : 'error');
359
446
  if (!buildResult.success && buildResult.error) {
360
- console.error(buildResult.error);
447
+ taskLog(buildResult.error);
361
448
  }
362
449
  },
450
+ onOutput: taskLog,
363
451
  });
364
452
  if (result.success) {
365
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Dependencies built successfully\n`);
453
+ log('Dependencies built successfully');
366
454
  }
367
455
  else {
368
- console.error(`[${(0, watcher_1.formatTimestamp)()}] Dependency build had errors, continuing anyway...\n`);
456
+ log('Dependency build had errors, continuing anyway...');
369
457
  }
370
458
  }
371
459
  }
372
460
  const affected = new Set([...resolvedApps, ...depsToBuilt]);
373
- const sourceResult = await runSourceGenerators(root, affected, projects, options.dryRun);
461
+ const sourceResult = await runSourceGeneratorsWithUI(root, affected, projects, options.dryRun, { log, taskLog, addTask, updateTask });
374
462
  if (!sourceResult.success) {
375
- console.error('Source generation failed, continuing anyway...');
463
+ log('Source generation failed, continuing anyway...');
376
464
  }
377
465
  for (const appName of resolvedApps) {
378
466
  const project = projects.get(appName);
379
467
  if (!project)
380
468
  continue;
381
- const hasDevScript = project.packageJson.scripts?.dev;
469
+ const hasDevScript = project.packageJson.scripts['dev'];
382
470
  if (!hasDevScript) {
383
- console.warn(`Warning: ${appName} has no dev script, skipping`);
471
+ log(`Warning: ${appName} has no dev script, skipping`);
384
472
  continue;
385
473
  }
386
- console.log(`Starting dev server: ${appName}`);
474
+ const devCmd = `npm run dev -w ${appName}`;
475
+ log(`Starting dev server: ${appName}`);
476
+ taskLog(`\x1b[33m$ ${devCmd}\x1b[0m`);
387
477
  const proc = (0, child_process_1.spawn)('npm', ['run', 'dev', '-w', appName], {
388
478
  cwd: root,
389
479
  stdio: ['ignore', 'pipe', 'pipe'],
390
- shell: true,
480
+ detached: true,
391
481
  });
392
482
  const shortName = appName.includes('/') ? appName.split('/').pop() : appName;
393
483
  const prefix = `[${shortName}]`;
394
- const stripClearCodes = (text) => text.replace(/\x1b\[[0-9]*J|\x1b\[[0-9]*H|\x1bc/g, '');
484
+ const taskId = `dev-${appName}`;
485
+ if (!proc.pid) {
486
+ throw new Error(`Failed to start dev server for ${appName}: no pid`);
487
+ }
488
+ addTask({ id: taskId, name: shortName ?? appName, pid: proc.pid, status: 'running' });
489
+ const ESC = '\x1b';
490
+ const clearRegex = new RegExp(`${ESC}\\[\\d*J|${ESC}\\[\\d*H|${ESC}c`, 'g');
491
+ const stripClearCodes = (text) => text.replace(clearRegex, '');
395
492
  proc.stdout?.on('data', (data) => {
396
493
  const lines = stripClearCodes(data.toString()).trim().split('\n');
397
494
  for (const line of lines) {
398
- if (line)
399
- console.log(`${prefix} ${line}`);
495
+ if (line) {
496
+ taskLog(`${prefix} ${line}`);
497
+ const port = (0, ui_1.detectPort)(line);
498
+ if (port > 0) {
499
+ updateTaskPort(taskId, port);
500
+ }
501
+ }
400
502
  }
401
503
  });
402
504
  proc.stderr?.on('data', (data) => {
403
505
  const lines = stripClearCodes(data.toString()).trim().split('\n');
404
506
  for (const line of lines) {
405
- if (line)
406
- console.error(`${prefix} ${line}`);
507
+ if (line) {
508
+ taskLog(`${prefix} ${line}`);
509
+ const port = (0, ui_1.detectPort)(line);
510
+ if (port > 0) {
511
+ updateTaskPort(taskId, port);
512
+ }
513
+ }
407
514
  }
408
515
  });
409
516
  proc.on('close', (code) => {
410
- console.log(`${prefix} exited with code ${code}`);
517
+ log(`${prefix} exited with code ${code}`);
518
+ updateTask(taskId, code === 0 ? 'stopped' : 'error');
411
519
  });
412
- devProcesses.push(proc);
520
+ devProcesses.push({ proc, name: appName, command: devCmd });
413
521
  }
414
- const cleanup = () => {
415
- for (const proc of devProcesses) {
416
- proc.kill();
417
- }
418
- process.exit(0);
419
- };
420
- process.on('SIGINT', cleanup);
421
- process.on('SIGTERM', cleanup);
422
522
  console.log();
423
523
  }
424
524
  const filterPattern = options.filter;
@@ -437,14 +537,15 @@ program
437
537
  const filteredCount = filterPattern
438
538
  ? [...projects.keys()].filter(matchesFilter).length
439
539
  : projects.size;
440
- console.log(`Watching ${projects.size} projects for changes...`);
540
+ log(`Watching ${projects.size} projects for changes...`);
441
541
  if (filterPattern) {
442
- console.log(`Building only projects matching: ${filterPattern} (${filteredCount} projects)`);
542
+ log(`Building only projects matching: ${filterPattern} (${filteredCount} projects)`);
443
543
  }
444
- console.log('Press Ctrl+C to stop\n');
544
+ log('Press Ctrl+C to stop');
445
545
  let isBuilding = false;
446
546
  let pendingChanges = null;
447
- const handleChanges = async (changedProjects) => {
547
+ let buildCount = 0;
548
+ const handleChanges = async (changedProjects, changedFiles) => {
448
549
  if (isBuilding) {
449
550
  pendingChanges = pendingChanges || new Set();
450
551
  for (const p of changedProjects) {
@@ -453,42 +554,90 @@ program
453
554
  return;
454
555
  }
455
556
  isBuilding = true;
456
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Changes detected: ${[...changedProjects].join(', ')}`);
557
+ log('Changes detected:');
558
+ if (changedFiles) {
559
+ for (const [project, files] of changedFiles) {
560
+ for (const file of files) {
561
+ const relativePath = file.replace(root + '/', '');
562
+ logRaw(` ${relativePath} (${project})`);
563
+ }
564
+ }
565
+ }
566
+ else {
567
+ logRaw(` ${[...changedProjects].join(', ')} (pending)`);
568
+ }
457
569
  const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
458
570
  const filteredAffected = new Set([...affected].filter(matchesFilter));
459
571
  if (filteredAffected.size === 0) {
460
- console.log(`[${(0, watcher_1.formatTimestamp)()}] No matching projects to build\n`);
572
+ log('No matching projects to build');
461
573
  isBuilding = false;
462
574
  return;
463
575
  }
576
+ buildCount++;
577
+ setStatus(`build #${buildCount}`);
578
+ logRaw('');
464
579
  const plan = (0, planner_1.createBuildPlan)(filteredAffected, graph.deps);
465
- const sourceResult = await runSourceGenerators(root, affected, projects, options.dryRun);
580
+ const totalSteps = plan.waves.reduce((sum, w) => sum + w.length, 0);
581
+ logRaw('Affected dependencies:');
582
+ for (const proj of [...filteredAffected].sort((a, b) => a.localeCompare(b))) {
583
+ const deps = graph.deps.get(proj) ?? new Set();
584
+ const affectedDeps = [...deps].filter(d => filteredAffected.has(d));
585
+ if (affectedDeps.length > 0) {
586
+ logRaw(` ${proj} -> ${affectedDeps.join(', ')}`);
587
+ }
588
+ else {
589
+ logRaw(` ${proj} (no affected dependencies)`);
590
+ }
591
+ }
592
+ logRaw('Compilation plan:');
593
+ let stepCounter = 0;
594
+ for (let i = 0; i < plan.waves.length; i++) {
595
+ const wave = plan.waves[i];
596
+ const mode = wave.length > 1 ? 'parallel' : 'sequential';
597
+ for (const proj of wave) {
598
+ stepCounter++;
599
+ logRaw(` [${stepCounter}/${totalSteps}] ${proj} (wave ${i + 1}/${plan.waves.length}, ${mode})`);
600
+ }
601
+ }
602
+ logRaw('');
603
+ const sourceResult = await runSourceGeneratorsWithUI(root, affected, projects, options.dryRun, { log, taskLog, addTask, updateTask });
466
604
  if (!sourceResult.success) {
467
- console.error(`[${(0, watcher_1.formatTimestamp)()}] Source generation failed\n`);
605
+ log('Source generation failed');
606
+ setStatus(`build #${buildCount} FAILED`);
468
607
  isBuilding = false;
469
608
  return;
470
609
  }
471
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${[...filteredAffected].join(', ')}`);
610
+ log(`Building: ${[...filteredAffected].join(', ')}`);
472
611
  if (plan.waves.length > 0) {
473
612
  const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
474
613
  concurrency: parseInt(options.concurrency, 10),
475
614
  dryRun: options.dryRun,
476
615
  onStart: (info) => {
477
616
  const mode = info.isParallel ? 'parallel' : 'sequential';
478
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
617
+ log(`Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
618
+ const shortName = info.project.includes('/') ? info.project.split('/').pop() : info.project;
619
+ addTask({ id: `build-${info.project}`, name: `build:${shortName}`, pid: 0, status: 'running' });
479
620
  },
621
+ onOutput: taskLog,
480
622
  onComplete: (buildResult) => {
481
623
  const status = buildResult.success ? 'done' : 'FAILED';
482
- console.log(`[${(0, watcher_1.formatTimestamp)()}] ${buildResult.project}: ${status} (${buildResult.duration}ms)`);
624
+ log(`${buildResult.project}: ${status} (${buildResult.duration}ms)`);
625
+ updateTask(`build-${buildResult.project}`, buildResult.success ? 'stopped' : 'error');
483
626
  },
484
627
  });
485
628
  if (result.success) {
486
- console.log(`[${(0, watcher_1.formatTimestamp)()}] Build complete\n`);
629
+ setStatus(`build #${buildCount} done`);
630
+ log('Build complete');
487
631
  }
488
632
  else {
489
- console.error(`[${(0, watcher_1.formatTimestamp)()}] Build failed\n`);
633
+ setStatus(`build #${buildCount} FAILED`);
634
+ log('Build failed');
490
635
  }
491
636
  }
637
+ else {
638
+ setStatus(`build #${buildCount} done`);
639
+ log('Build complete');
640
+ }
492
641
  isBuilding = false;
493
642
  if (pendingChanges && pendingChanges.size > 0) {
494
643
  const next = pendingChanges;
@@ -497,18 +646,19 @@ program
497
646
  }
498
647
  };
499
648
  const config = (0, source_scanner_1.loadWorkgraphConfig)(root);
500
- const sourcePaths = Object.keys(config.sources || {}).map(p => `**/${p}/**`);
649
+ const sourcePaths = Object.keys(config.sources).map(p => `**/${p}/**`);
501
650
  if (sourcePaths.length > 0) {
502
- console.log('Ignoring generated source paths (to prevent rebuild loops):');
651
+ log('Ignoring generated source paths (to prevent rebuild loops):');
503
652
  for (const p of sourcePaths) {
504
- console.log(` ${p}`);
653
+ logRaw(` ${p}`);
505
654
  }
506
- console.log();
507
655
  }
508
656
  (0, watcher_1.createWatcher)({
509
657
  root,
510
658
  debounceMs: parseInt(options.debounce, 10),
511
- onChange: handleChanges,
659
+ onChange: (changedProjects, changedFiles) => {
660
+ void handleChanges(changedProjects, changedFiles);
661
+ },
512
662
  ignorePatterns: sourcePaths,
513
663
  verbose: options.verbose,
514
664
  }, projects);