ado-sync 0.1.65 → 0.1.68

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 (67) hide show
  1. package/README.md +15 -15
  2. package/dist/__tests__/regressions.test.js +1133 -1
  3. package/dist/__tests__/regressions.test.js.map +1 -1
  4. package/dist/ai/summarizer.d.ts +2 -1
  5. package/dist/ai/summarizer.js +6 -1
  6. package/dist/ai/summarizer.js.map +1 -1
  7. package/dist/azure/test-cases.d.ts +11 -1
  8. package/dist/azure/test-cases.js +286 -43
  9. package/dist/azure/test-cases.js.map +1 -1
  10. package/dist/cli-diagnostics.d.ts +66 -0
  11. package/dist/cli-diagnostics.js +75 -0
  12. package/dist/cli-diagnostics.js.map +1 -0
  13. package/dist/cli.js +335 -23
  14. package/dist/cli.js.map +1 -1
  15. package/dist/config.js +194 -9
  16. package/dist/config.js.map +1 -1
  17. package/dist/extensions.d.ts +8 -0
  18. package/dist/extensions.js +86 -0
  19. package/dist/extensions.js.map +1 -0
  20. package/dist/id-markers.d.ts +1 -0
  21. package/dist/id-markers.js +13 -0
  22. package/dist/id-markers.js.map +1 -1
  23. package/dist/sync/cache.d.ts +2 -0
  24. package/dist/sync/cache.js.map +1 -1
  25. package/dist/sync/engine.d.ts +29 -2
  26. package/dist/sync/engine.js +270 -41
  27. package/dist/sync/engine.js.map +1 -1
  28. package/dist/sync/publish-results.d.ts +25 -0
  29. package/dist/sync/publish-results.js +81 -2
  30. package/dist/sync/publish-results.js.map +1 -1
  31. package/dist/types.d.ts +98 -2
  32. package/llms.txt +11 -11
  33. package/package.json +9 -1
  34. package/docs/advanced.md +0 -989
  35. package/docs/agent-setup.md +0 -204
  36. package/docs/capability-roadmap.md +0 -280
  37. package/docs/cli.md +0 -614
  38. package/docs/configuration.md +0 -322
  39. package/docs/examples/csharp-mstest-local-llm.yaml +0 -35
  40. package/docs/examples/csharp-mstest.yaml +0 -21
  41. package/docs/examples/csharp-nunit.yaml +0 -21
  42. package/docs/examples/csharp-specflow.yaml +0 -16
  43. package/docs/examples/cypress.yaml +0 -21
  44. package/docs/examples/detox-react-native.yaml +0 -21
  45. package/docs/examples/espresso-android.yaml +0 -21
  46. package/docs/examples/flutter-dart.yaml +0 -21
  47. package/docs/examples/java-junit.yaml +0 -21
  48. package/docs/examples/java-testng.yaml +0 -21
  49. package/docs/examples/js-jasmine-wdio.yaml +0 -21
  50. package/docs/examples/js-jest.yaml +0 -21
  51. package/docs/examples/playwright-js.yaml +0 -21
  52. package/docs/examples/playwright-ts.yaml +0 -21
  53. package/docs/examples/puppeteer.yaml +0 -21
  54. package/docs/examples/python-pytest.yaml +0 -21
  55. package/docs/examples/robot-framework.yaml +0 -19
  56. package/docs/examples/testcafe.yaml +0 -21
  57. package/docs/examples/xcuitest-ios.yaml +0 -21
  58. package/docs/mcp-server.md +0 -312
  59. package/docs/publish-test-results.md +0 -947
  60. package/docs/spec-formats.md +0 -1357
  61. package/docs/troubleshooting.md +0 -101
  62. package/docs/vscode-extension.md +0 -139
  63. package/docs/work-item-links.md +0 -115
  64. package/docs/workflows.md +0 -457
  65. package/mkdocs.yml +0 -40
  66. package/requirements-docs.txt +0 -4
  67. package/scripts/build_site.sh +0 -6
package/dist/cli.js CHANGED
@@ -55,7 +55,9 @@ const readline = __importStar(require("readline"));
55
55
  const package_json_1 = __importDefault(require("../package.json"));
56
56
  const summarizer_1 = require("./ai/summarizer");
57
57
  const client_1 = require("./azure/client");
58
+ const cli_diagnostics_1 = require("./cli-diagnostics");
58
59
  const config_1 = require("./config");
60
+ const id_markers_1 = require("./id-markers");
59
61
  const engine_1 = require("./sync/engine");
60
62
  const generate_1 = require("./sync/generate");
61
63
  const manifest_1 = require("./sync/manifest");
@@ -69,6 +71,17 @@ program
69
71
  // Global options
70
72
  program.option('-c, --config <path>', 'Path to config file (default: ado-sync.json)');
71
73
  program.option('--output <format>', 'Output format: text (default) or json');
74
+ program.option('--pat-override <token>', 'Override auth.token for this invocation (not persisted to config)');
75
+ program.option('--org-override <url>', 'Override orgUrl for this invocation (not persisted to config)');
76
+ function loadConfigWithOverrides(configPath) {
77
+ const config = (0, config_1.loadConfig)(configPath);
78
+ const globalOpts = program.opts();
79
+ if (globalOpts.patOverride)
80
+ config.auth.token = globalOpts.patOverride;
81
+ if (globalOpts.orgOverride)
82
+ config.orgUrl = globalOpts.orgOverride;
83
+ return config;
84
+ }
72
85
  // ─── Error formatting (L) ─────────────────────────────────────────────────────
73
86
  function toError(err) {
74
87
  if (err instanceof Error)
@@ -101,6 +114,17 @@ function handleError(err) {
101
114
  function collect(value, previous) {
102
115
  return [...previous, value];
103
116
  }
117
+ function normalizeSourceFilesForCli(sourceFiles, configDir) {
118
+ if (!sourceFiles?.length)
119
+ return undefined;
120
+ return sourceFiles.map((filePath) => path.normalize(path.resolve(configDir, filePath)));
121
+ }
122
+ function printDiagnosticItems(items) {
123
+ console.log(chalk_1.default.bold('Diagnostics:'));
124
+ for (const item of items) {
125
+ console.log(chalk_1.default.dim(` ${item.label}: ${item.value}`));
126
+ }
127
+ }
104
128
  // ─── AI summary helper ────────────────────────────────────────────────────────
105
129
  /**
106
130
  * Build AiSummaryOpts from parsed CLI opts, falling back to config file values.
@@ -243,12 +267,97 @@ async function runInitWizard(isYaml) {
243
267
  }
244
268
  return JSON.stringify(cfg, null, 2) + '\n';
245
269
  }
270
+ // ─── config ───────────────────────────────────────────────────────────────────
271
+ const configCmd = program
272
+ .command('config')
273
+ .description('Configuration inspection and management');
274
+ configCmd
275
+ .command('show')
276
+ .description('Display the fully resolved configuration (after parent merge, env vars, and overrides)')
277
+ .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
278
+ .action(async (opts) => {
279
+ const globalOpts = program.opts();
280
+ try {
281
+ const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
282
+ const config = loadConfigWithOverrides(configPath);
283
+ if (opts.configOverride?.length)
284
+ (0, config_1.applyOverrides)(config, opts.configOverride);
285
+ const redacted = { ...config, auth: { ...config.auth, token: config.auth?.token ? '***' : undefined } };
286
+ process.stdout.write(JSON.stringify(redacted, null, 2) + '\n');
287
+ }
288
+ catch (err) {
289
+ handleError(err);
290
+ }
291
+ });
292
+ // ─── extensions ───────────────────────────────────────────────────────────────
293
+ const extCmd = program
294
+ .command('extensions')
295
+ .description('Manage ado-sync extensions');
296
+ extCmd
297
+ .command('list')
298
+ .description('List registered extensions from config')
299
+ .action(async () => {
300
+ const globalOpts = program.opts();
301
+ try {
302
+ const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
303
+ const config = loadConfigWithOverrides(configPath);
304
+ const extensions = config.extensions ?? [];
305
+ if (extensions.length === 0) {
306
+ console.log(chalk_1.default.dim('No extensions configured.'));
307
+ return;
308
+ }
309
+ for (const ext of extensions) {
310
+ console.log(`${chalk_1.default.bold(ext.name)} ${chalk_1.default.dim(`(${ext.type})`)} — ${ext.package}`);
311
+ if (ext.filePatterns?.length) {
312
+ console.log(chalk_1.default.dim(` patterns: ${ext.filePatterns.join(', ')}`));
313
+ }
314
+ }
315
+ }
316
+ catch (err) {
317
+ handleError(err);
318
+ }
319
+ });
320
+ extCmd
321
+ .command('validate')
322
+ .description('Validate extension compatibility and loading')
323
+ .action(async () => {
324
+ const globalOpts = program.opts();
325
+ try {
326
+ const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
327
+ const config = loadConfigWithOverrides(configPath);
328
+ const configDir = path.dirname(configPath);
329
+ const extensions = config.extensions ?? [];
330
+ if (extensions.length === 0) {
331
+ console.log(chalk_1.default.dim('No extensions configured.'));
332
+ return;
333
+ }
334
+ const { loadExtensions, validateExtensions } = await Promise.resolve().then(() => __importStar(require('./extensions')));
335
+ const loaded = await loadExtensions(extensions, configDir);
336
+ const errors = validateExtensions(loaded, package_json_1.default.version);
337
+ if (errors.length) {
338
+ for (const err of errors)
339
+ console.log(chalk_1.default.red(` ✗ ${err}`));
340
+ process.exit(1);
341
+ }
342
+ for (const ext of loaded) {
343
+ console.log(chalk_1.default.green(` ✓ ${ext.config.name}`) + chalk_1.default.dim(` v${ext.manifest.version} (${ext.manifest.type})`));
344
+ }
345
+ }
346
+ catch (err) {
347
+ handleError(err);
348
+ }
349
+ });
246
350
  // ─── push ─────────────────────────────────────────────────────────────────────
247
351
  program
248
352
  .command('push')
249
353
  .description('Push local test specs to Azure DevOps (create or update test cases)')
250
354
  .option('--dry-run', 'Show what would change without making any modifications')
355
+ .option('--create-only', 'Only create test cases for unlinked local specs; skip linked items and removed-case detection')
356
+ .option('--link-only', 'Only restore local IDs by linking unlinked specs to uniquely matching existing test cases in the current scope')
357
+ .option('--update-only', 'Only update linked test cases; skip unlinked specs and removed-case detection')
251
358
  .option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
359
+ .option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
360
+ .option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
252
361
  .option('--config-override <path=value>', 'Override a config value (repeatable, e.g. --config-override sync.tagPrefix=mytag)', collect, [])
253
362
  .option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
254
363
  .option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
@@ -260,9 +369,10 @@ program
260
369
  const globalOpts = program.opts();
261
370
  try {
262
371
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
263
- const config = (0, config_1.loadConfig)(configPath);
372
+ const config = loadConfigWithOverrides(configPath);
264
373
  if (opts.configOverride?.length)
265
374
  (0, config_1.applyOverrides)(config, opts.configOverride);
375
+ (0, engine_1.validatePushModeOptions)(opts);
266
376
  const configDir = path.dirname(configPath);
267
377
  console.log(chalk_1.default.bold('ado-sync push'));
268
378
  console.log(chalk_1.default.dim(`Config: ${configPath}`));
@@ -270,8 +380,18 @@ program
270
380
  console.log(chalk_1.default.dim(`Plan: ${config.testPlan.id}`));
271
381
  if (opts.dryRun)
272
382
  console.log(chalk_1.default.yellow('Dry run — no changes will be made'));
383
+ if (opts.createOnly)
384
+ console.log(chalk_1.default.dim('Mode: create-only'));
385
+ if (opts.linkOnly)
386
+ console.log(chalk_1.default.dim('Mode: link-only'));
387
+ if (opts.updateOnly)
388
+ console.log(chalk_1.default.dim('Mode: update-only'));
273
389
  if (opts.tags)
274
390
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
391
+ if (opts.sourceFile?.length)
392
+ console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
393
+ if (opts.include?.length)
394
+ console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
275
395
  if (opts.configOverride?.length)
276
396
  console.log(chalk_1.default.dim(`Overrides: ${opts.configOverride.join(', ')}`));
277
397
  console.log('');
@@ -280,7 +400,7 @@ program
280
400
  const outputFormat = program.opts().output;
281
401
  const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
282
402
  const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
283
- const results = await (0, engine_1.push)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, onProgress, onAiProgress, aiSummary });
403
+ const results = await (0, engine_1.push)(config, configDir, { dryRun: opts.dryRun, createOnly: opts.createOnly, linkOnly: opts.linkOnly, updateOnly: opts.updateOnly, tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, onProgress, onAiProgress, aiSummary });
284
404
  if (isTTY && outputFormat !== 'json')
285
405
  clearProgressLine();
286
406
  printResults(results, config.toolSettings?.outputLevel, outputFormat);
@@ -295,12 +415,14 @@ program
295
415
  .description('Pull updates from Azure DevOps into local spec files')
296
416
  .option('--dry-run', 'Show what would change without modifying local files')
297
417
  .option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
418
+ .option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
419
+ .option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
298
420
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
299
421
  .action(async (opts) => {
300
422
  const globalOpts = program.opts();
301
423
  try {
302
424
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
303
- const config = (0, config_1.loadConfig)(configPath);
425
+ const config = loadConfigWithOverrides(configPath);
304
426
  if (opts.configOverride?.length)
305
427
  (0, config_1.applyOverrides)(config, opts.configOverride);
306
428
  const configDir = path.dirname(configPath);
@@ -310,11 +432,15 @@ program
310
432
  console.log(chalk_1.default.yellow('Dry run — no changes will be made'));
311
433
  if (opts.tags)
312
434
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
435
+ if (opts.sourceFile?.length)
436
+ console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
437
+ if (opts.include?.length)
438
+ console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
313
439
  console.log('');
314
440
  const isTTY = process.stdout.isTTY ?? false;
315
441
  const outputFormat = program.opts().output;
316
442
  const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
317
- const results = await (0, engine_1.pull)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, onProgress });
443
+ const results = await (0, engine_1.pull)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, onProgress });
318
444
  if (isTTY && outputFormat !== 'json')
319
445
  clearProgressLine();
320
446
  printResults(results, config.toolSettings?.outputLevel, outputFormat);
@@ -328,6 +454,8 @@ program
328
454
  .command('status')
329
455
  .description('Show diff between local specs and Azure DevOps without making changes')
330
456
  .option('--tags <expression>', 'Only check scenarios matching this tag expression')
457
+ .option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
458
+ .option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
331
459
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
332
460
  .option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
333
461
  .option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
@@ -339,7 +467,7 @@ program
339
467
  const globalOpts = program.opts();
340
468
  try {
341
469
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
342
- const config = (0, config_1.loadConfig)(configPath);
470
+ const config = loadConfigWithOverrides(configPath);
343
471
  if (opts.configOverride?.length)
344
472
  (0, config_1.applyOverrides)(config, opts.configOverride);
345
473
  const configDir = path.dirname(configPath);
@@ -347,13 +475,17 @@ program
347
475
  console.log(chalk_1.default.dim(`Config: ${configPath}`));
348
476
  if (opts.tags)
349
477
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
478
+ if (opts.sourceFile?.length)
479
+ console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
480
+ if (opts.include?.length)
481
+ console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
350
482
  console.log('');
351
483
  const aiSummary = buildAiOpts(opts, config, configDir);
352
484
  const isTTY = process.stdout.isTTY ?? false;
353
485
  const outputFormat = program.opts().output;
354
486
  const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
355
487
  const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
356
- const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, onProgress, onAiProgress, aiSummary });
488
+ const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, onProgress, onAiProgress, aiSummary });
357
489
  if (isTTY && outputFormat !== 'json')
358
490
  clearProgressLine();
359
491
  printResults(results, config.toolSettings?.outputLevel, outputFormat);
@@ -392,7 +524,7 @@ program
392
524
  const globalOpts = program.opts();
393
525
  try {
394
526
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
395
- const config = (0, config_1.loadConfig)(configPath);
527
+ const config = loadConfigWithOverrides(configPath);
396
528
  const cliPublishOverrides = [
397
529
  ...(opts.testConfiguration ? [/^\d+$/.test(opts.testConfiguration)
398
530
  ? `publishTestResults.testConfiguration.id=${opts.testConfiguration}`
@@ -452,6 +584,28 @@ program
452
584
  console.log(chalk_1.default.dim(`Run ID: ${result.runId}`));
453
585
  console.log(chalk_1.default.dim(`URL: ${result.runUrl}`));
454
586
  }
587
+ if (config.toolSettings?.outputLevel === 'diagnostic' && result.diagnostics) {
588
+ console.log('');
589
+ console.log(chalk_1.default.bold('Diagnostics:'));
590
+ if (result.diagnostics.sources.length) {
591
+ console.log(chalk_1.default.dim(' Sources:'));
592
+ for (const source of result.diagnostics.sources) {
593
+ console.log(chalk_1.default.dim(` ${path.relative(process.cwd(), source.filePath)} (${source.format})`));
594
+ }
595
+ }
596
+ if (result.diagnostics.configurationId) {
597
+ console.log(chalk_1.default.dim(` Configuration ID: ${result.diagnostics.configurationId}`));
598
+ }
599
+ if (result.diagnostics.plannedRun) {
600
+ console.log(chalk_1.default.dim(` Planned run: plan ${result.diagnostics.plannedRun.planId}, suite ${result.diagnostics.plannedRun.suiteId}, ${result.diagnostics.plannedRun.pointCount} point(s)`));
601
+ }
602
+ if (result.diagnostics.attachments) {
603
+ console.log(chalk_1.default.dim(` Attachments: ${result.diagnostics.attachments.resultCount} result-level, ${result.diagnostics.attachments.runCount} run-level`));
604
+ }
605
+ if (result.diagnostics.analyzedFailures) {
606
+ console.log(chalk_1.default.dim(` AI analyses: ${result.diagnostics.analyzedFailures}`));
607
+ }
608
+ }
455
609
  if (result.issuesSummary) {
456
610
  const s = result.issuesSummary;
457
611
  console.log('');
@@ -476,6 +630,7 @@ program
476
630
  const ACTION_SYMBOL = {
477
631
  created: chalk_1.default.green('+'),
478
632
  updated: chalk_1.default.blue('~'),
633
+ linked: chalk_1.default.green('↔'),
479
634
  pulled: chalk_1.default.cyan('↓'),
480
635
  skipped: chalk_1.default.dim('='),
481
636
  conflict: chalk_1.default.yellow('!'),
@@ -536,8 +691,10 @@ function printResults(results, outputLevel, outputFormat) {
536
691
  process.stdout.write(JSON.stringify(results, null, 2) + '\n');
537
692
  return;
538
693
  }
539
- const counts = { created: 0, updated: 0, pulled: 0, skipped: 0, conflict: 0, removed: 0, error: 0 };
694
+ const counts = { created: 0, updated: 0, linked: 0, pulled: 0, skipped: 0, conflict: 0, removed: 0, error: 0 };
540
695
  const quiet = outputLevel === 'quiet';
696
+ const verbose = outputLevel === 'verbose';
697
+ const diagnostic = outputLevel === 'diagnostic';
541
698
  for (const r of results) {
542
699
  counts[r.action]++;
543
700
  // In quiet mode, only print actionable results (skip the '= skipped' lines)
@@ -555,6 +712,9 @@ function printResults(results, outputLevel, outputFormat) {
555
712
  case 'updated':
556
713
  console.log(`${chalk_1.default.blue('~')} ${filePart} ${r.title}${idStr}`);
557
714
  break;
715
+ case 'linked':
716
+ console.log(`${chalk_1.default.green('↔')} ${filePart} ${r.title}${idStr}${detailStr}`);
717
+ break;
558
718
  case 'pulled':
559
719
  console.log(`${chalk_1.default.cyan('↓')} ${filePart} ${r.title}${idStr}${detailStr}`);
560
720
  break;
@@ -571,11 +731,24 @@ function printResults(results, outputLevel, outputFormat) {
571
731
  console.log(`${chalk_1.default.red('✗')} ${filePart} ${r.title}${idStr}${chalk_1.default.red(detailStr)}`);
572
732
  break;
573
733
  }
734
+ if (r.targetSuitePath && (r.action === 'created' || r.action === 'updated' || r.action === 'conflict' || verbose || diagnostic)) {
735
+ console.log(chalk_1.default.dim(` target suite: ${r.targetSuitePath}`));
736
+ }
737
+ if (r.previousSuitePath && (r.action === 'updated' || r.action === 'conflict' || diagnostic)) {
738
+ console.log(chalk_1.default.dim(` previous suite: ${r.previousSuitePath}`));
739
+ }
740
+ if (diagnostic && r.changedFields?.length) {
741
+ console.log(chalk_1.default.dim(` changed fields: ${r.changedFields.join(', ')}`));
742
+ }
743
+ if (diagnostic && r.detail && r.action !== 'linked' && r.action !== 'pulled' && r.action !== 'error') {
744
+ console.log(chalk_1.default.dim(` detail: ${r.detail}`));
745
+ }
574
746
  }
575
747
  console.log('');
576
748
  const summary = [
577
749
  counts.created && chalk_1.default.green(`${counts.created} created`),
578
750
  counts.updated && chalk_1.default.blue(`${counts.updated} updated`),
751
+ counts.linked && chalk_1.default.green(`${counts.linked} linked`),
579
752
  counts.pulled && chalk_1.default.cyan(`${counts.pulled} pulled`),
580
753
  counts.skipped && chalk_1.default.dim(`${counts.skipped} skipped`),
581
754
  counts.conflict && chalk_1.default.yellow(`${counts.conflict} conflicts`),
@@ -597,10 +770,19 @@ program
597
770
  console.log('');
598
771
  // 1. Load config
599
772
  let config;
773
+ let validateDiagnostics;
600
774
  try {
601
775
  config = (0, config_1.loadConfig)(configPath);
602
776
  if (opts.configOverride?.length)
603
777
  (0, config_1.applyOverrides)(config, opts.configOverride);
778
+ const plans = config.testPlans ?? [config.testPlan];
779
+ validateDiagnostics = {
780
+ authType: config.auth.type,
781
+ localType: config.local.type,
782
+ syncTargetMode: config.syncTarget?.mode ?? 'suite',
783
+ planIds: plans.map((plan) => plan.id),
784
+ overrideCount: opts.configOverride?.length ?? 0,
785
+ };
604
786
  console.log(chalk_1.default.green(' ✓ Config is valid'));
605
787
  }
606
788
  catch (err) {
@@ -616,6 +798,10 @@ program
616
798
  }
617
799
  catch (err) {
618
800
  console.log(chalk_1.default.red(` ✗ Azure connection failed: ${formatError(err)}`));
801
+ if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
802
+ console.log('');
803
+ printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
804
+ }
619
805
  process.exit(1);
620
806
  }
621
807
  // 3. Verify project
@@ -628,6 +814,10 @@ program
628
814
  }
629
815
  catch (err) {
630
816
  console.log(chalk_1.default.red(` ✗ Project "${config.project}" not found: ${formatError(err)}`));
817
+ if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
818
+ console.log('');
819
+ printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
820
+ }
631
821
  process.exit(1);
632
822
  }
633
823
  // 4. Verify test plan(s)
@@ -644,6 +834,10 @@ program
644
834
  allPlansOk = false;
645
835
  }
646
836
  }
837
+ if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
838
+ console.log('');
839
+ printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
840
+ }
647
841
  if (!allPlansOk)
648
842
  process.exit(1);
649
843
  console.log('');
@@ -654,6 +848,8 @@ program
654
848
  .command('diff')
655
849
  .description('Show field-level diff between local specs and Azure DevOps')
656
850
  .option('--tags <expression>', 'Only check scenarios matching this tag expression')
851
+ .option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
852
+ .option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
657
853
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
658
854
  .option('--format <fmt>', 'Output format: text (default) or json')
659
855
  .option('--fail-on-drift', 'Exit with code 1 when any differences are found (useful as a CI quality gate)')
@@ -665,7 +861,7 @@ program
665
861
  const globalOpts = program.opts();
666
862
  try {
667
863
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
668
- const config = (0, config_1.loadConfig)(configPath);
864
+ const config = loadConfigWithOverrides(configPath);
669
865
  if (opts.configOverride?.length)
670
866
  (0, config_1.applyOverrides)(config, opts.configOverride);
671
867
  const configDir = path.dirname(configPath);
@@ -673,19 +869,25 @@ program
673
869
  console.log(chalk_1.default.dim(`Config: ${configPath}`));
674
870
  if (opts.tags)
675
871
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
872
+ if (opts.sourceFile?.length)
873
+ console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
874
+ if (opts.include?.length)
875
+ console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
676
876
  console.log('');
677
877
  const aiSummary = buildAiOpts(opts, config, configDir);
678
- const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, aiSummary });
878
+ const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, aiSummary });
679
879
  // --format json or global --output json
680
880
  const outputFormat = opts.format ?? program.opts().output;
681
881
  if (outputFormat === 'json') {
682
882
  const diffs = results
683
- .filter((r) => r.action === 'updated' || r.action === 'conflict')
883
+ .filter((r) => r.action === 'created' || r.action === 'updated' || r.action === 'conflict')
684
884
  .map((r) => ({
685
885
  action: r.action,
686
886
  azureId: r.azureId,
687
887
  title: r.title,
688
888
  filePath: r.filePath ? path.relative(process.cwd(), r.filePath) : '',
889
+ targetSuitePath: r.targetSuitePath,
890
+ previousSuitePath: r.previousSuitePath,
689
891
  changedFields: r.changedFields ?? [],
690
892
  diffDetail: r.diffDetail ?? [],
691
893
  }));
@@ -696,13 +898,19 @@ program
696
898
  }
697
899
  let anyDiff = false;
698
900
  for (const r of results) {
699
- if (r.action !== 'updated' && r.action !== 'conflict')
901
+ if (r.action !== 'created' && r.action !== 'updated' && r.action !== 'conflict')
700
902
  continue;
701
903
  anyDiff = true;
702
904
  const idStr = r.azureId ? chalk_1.default.dim(` [#${r.azureId}]`) : '';
703
905
  const filePart = r.filePath ? chalk_1.default.dim(path.relative(process.cwd(), r.filePath) + ':') : '';
704
- const symbol = r.action === 'conflict' ? chalk_1.default.yellow('!') : chalk_1.default.blue('~');
906
+ const symbol = r.action === 'conflict' ? chalk_1.default.yellow('!') : r.action === 'created' ? chalk_1.default.green('+') : chalk_1.default.blue('~');
705
907
  console.log(`${symbol} ${filePart} ${r.title}${idStr}`);
908
+ if (r.targetSuitePath) {
909
+ console.log(chalk_1.default.dim(` target suite: ${r.targetSuitePath}`));
910
+ }
911
+ if (r.previousSuitePath) {
912
+ console.log(chalk_1.default.dim(` previous suite: ${r.previousSuitePath}`));
913
+ }
706
914
  if (r.changedFields?.length) {
707
915
  console.log(chalk_1.default.dim(` changed fields: ${r.changedFields.join(', ')}`));
708
916
  }
@@ -751,7 +959,7 @@ program
751
959
  const globalOpts = program.opts();
752
960
  try {
753
961
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
754
- const config = (0, config_1.loadConfig)(configPath);
962
+ const config = loadConfigWithOverrides(configPath);
755
963
  if (opts.configOverride?.length)
756
964
  (0, config_1.applyOverrides)(config, opts.configOverride);
757
965
  const configDir = path.dirname(configPath);
@@ -876,7 +1084,7 @@ program
876
1084
  const globalOpts = program.opts();
877
1085
  try {
878
1086
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
879
- const config = (0, config_1.loadConfig)(configPath);
1087
+ const config = loadConfigWithOverrides(configPath);
880
1088
  if (opts.configOverride?.length)
881
1089
  (0, config_1.applyOverrides)(config, opts.configOverride);
882
1090
  const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
@@ -926,23 +1134,31 @@ program
926
1134
  const globalOpts = program.opts();
927
1135
  try {
928
1136
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
929
- const config = (0, config_1.loadConfig)(configPath);
1137
+ const config = loadConfigWithOverrides(configPath);
930
1138
  if (opts.configOverride?.length)
931
1139
  (0, config_1.applyOverrides)(config, opts.configOverride);
932
1140
  const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
933
1141
  const { acGate, getWorkItemsByQuery, getWorkItemsByAreaPath } = await Promise.resolve().then(() => __importStar(require('./azure/work-items')));
934
1142
  const client = await AzureClient.create(config);
935
1143
  let storyIds = [];
1144
+ let selectorMode = 'default-states';
1145
+ let selectorValue = opts.states ?? 'Active,Resolved,Closed';
936
1146
  if (opts.query) {
937
1147
  const stories = await getWorkItemsByQuery(client, config.project, opts.query);
938
1148
  storyIds = stories.map((s) => s.id);
1149
+ selectorMode = 'query';
1150
+ selectorValue = opts.query;
939
1151
  }
940
1152
  else if (opts.areaPath) {
941
1153
  const stories = await getWorkItemsByAreaPath(client, config.project, opts.areaPath);
942
1154
  storyIds = stories.map((s) => s.id);
1155
+ selectorMode = 'area-path';
1156
+ selectorValue = opts.areaPath;
943
1157
  }
944
1158
  else if (opts.storyIds) {
945
1159
  storyIds = opts.storyIds.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean);
1160
+ selectorMode = 'story-ids';
1161
+ selectorValue = opts.storyIds;
946
1162
  }
947
1163
  else {
948
1164
  // Default: all active stories in project
@@ -951,6 +1167,7 @@ program
951
1167
  const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '${config.project.replace(/'/g, "''")}' AND [System.WorkItemType] = 'User Story' AND [System.State] IN (${stateList}) ORDER BY [System.Id]`;
952
1168
  const stories = await getWorkItemsByQuery(client, config.project, wiql);
953
1169
  storyIds = stories.map((s) => s.id);
1170
+ selectorValue = states;
954
1171
  }
955
1172
  if (storyIds.length === 0) {
956
1173
  console.log(chalk_1.default.yellow('No stories found to validate.'));
@@ -961,6 +1178,23 @@ program
961
1178
  console.log(chalk_1.default.dim(`Stories: ${storyIds.length} to validate`));
962
1179
  console.log('');
963
1180
  const report = await acGate(client, config.project, storyIds, config.orgUrl);
1181
+ if (config.toolSettings?.outputLevel === 'diagnostic') {
1182
+ const noAc = report.failed.filter((r) => r.outcome === 'no-ac').length;
1183
+ const noTc = report.failed.filter((r) => r.outcome === 'no-tc').length;
1184
+ printDiagnosticItems((0, cli_diagnostics_1.getAcGateDiagnosticItems)({
1185
+ selectorMode,
1186
+ selectorValue,
1187
+ states: selectorMode === 'default-states' ? selectorValue : undefined,
1188
+ failMode: opts.failOnNoAc ? 'no-ac-only' : 'all-failures',
1189
+ totalStories: storyIds.length,
1190
+ passed: report.passed.length,
1191
+ failed: report.failed.length,
1192
+ noAc,
1193
+ noTc,
1194
+ overrideCount: opts.configOverride?.length ?? 0,
1195
+ }));
1196
+ console.log('');
1197
+ }
964
1198
  const outputFormat = globalOpts.output;
965
1199
  if (outputFormat === 'json') {
966
1200
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -1001,6 +1235,7 @@ program
1001
1235
  .command('stale')
1002
1236
  .description('List Azure DevOps Test Cases that have no corresponding local spec')
1003
1237
  .option('--tags <expression>', 'Only consider local specs matching this tag expression')
1238
+ .option('--suites', 'Report orphaned suite memberships (affected by stalenessPolicy config)')
1004
1239
  .option('--retire', 'Automatically transition stale Test Cases to Closed state and tag ado-sync:retired')
1005
1240
  .option('--retire-state <state>', 'Target state when retiring (default: Closed)', 'Closed')
1006
1241
  .option('--dry-run', 'Show what --retire would do without making changes')
@@ -1009,7 +1244,7 @@ program
1009
1244
  const globalOpts = program.opts();
1010
1245
  try {
1011
1246
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1012
- const config = (0, config_1.loadConfig)(configPath);
1247
+ const config = loadConfigWithOverrides(configPath);
1013
1248
  if (opts.configOverride?.length)
1014
1249
  (0, config_1.applyOverrides)(config, opts.configOverride);
1015
1250
  const configDir = path.dirname(configPath);
@@ -1021,6 +1256,23 @@ program
1021
1256
  console.log(chalk_1.default.dim(`Retire: will transition to "${opts.retireState}"${opts.dryRun ? ' (dry-run)' : ''}`));
1022
1257
  console.log('');
1023
1258
  const staleCases = await (0, engine_1.detectStaleTestCases)(config, configDir, { tags: opts.tags });
1259
+ if (config.toolSettings?.outputLevel === 'diagnostic') {
1260
+ console.log(chalk_1.default.bold('Diagnostics:'));
1261
+ for (const item of (0, cli_diagnostics_1.getStaleDiagnosticItems)({
1262
+ syncTargetMode: config.syncTarget?.mode ?? 'suite',
1263
+ planIds: (config.testPlans?.length ? config.testPlans : [config.testPlan]).map((plan) => plan.id),
1264
+ markerPrefix: (0, id_markers_1.getPreferredMarkerTagPrefix)(config),
1265
+ ownershipTag: (0, id_markers_1.getSyncTargetOwnershipTag)(config),
1266
+ tagExpression: opts.tags,
1267
+ staleCount: staleCases.length,
1268
+ retireState: opts.retire ? opts.retireState : undefined,
1269
+ dryRun: Boolean(opts.dryRun),
1270
+ overrideCount: opts.configOverride?.length ?? 0,
1271
+ })) {
1272
+ console.log(chalk_1.default.dim(` ${item.label}: ${item.value}`));
1273
+ }
1274
+ console.log('');
1275
+ }
1024
1276
  const outputFormat = globalOpts.output;
1025
1277
  if (outputFormat === 'json') {
1026
1278
  process.stdout.write(JSON.stringify(staleCases, null, 2) + '\n');
@@ -1085,7 +1337,7 @@ program
1085
1337
  const globalOpts = program.opts();
1086
1338
  try {
1087
1339
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1088
- const config = (0, config_1.loadConfig)(configPath);
1340
+ const config = loadConfigWithOverrides(configPath);
1089
1341
  if (opts.configOverride?.length)
1090
1342
  (0, config_1.applyOverrides)(config, opts.configOverride);
1091
1343
  const configDir = path.dirname(configPath);
@@ -1095,6 +1347,23 @@ program
1095
1347
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
1096
1348
  console.log('');
1097
1349
  const report = await (0, engine_1.coverageReport)(config, configDir, { tags: opts.tags });
1350
+ const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
1351
+ if (config.toolSettings?.outputLevel === 'diagnostic') {
1352
+ printDiagnosticItems((0, cli_diagnostics_1.getCoverageDiagnosticItems)({
1353
+ localType: config.local.type,
1354
+ syncTargetMode: config.syncTarget?.mode ?? 'suite',
1355
+ tagExpression: opts.tags,
1356
+ totalLocalSpecs: report.totalLocalSpecs,
1357
+ linkedSpecs: report.linkedSpecs,
1358
+ unlinkedSpecs: report.unlinkedSpecs,
1359
+ storiesReferenced: report.storiesReferenced.length,
1360
+ storiesCovered: report.storiesCovered.length,
1361
+ storyPrefix: config.sync?.links?.find((link) => link.prefix === 'story')?.prefix ?? 'story',
1362
+ failBelow: threshold,
1363
+ overrideCount: opts.configOverride?.length ?? 0,
1364
+ }));
1365
+ console.log('');
1366
+ }
1098
1367
  const outputFormat = globalOpts.output;
1099
1368
  if (outputFormat === 'json') {
1100
1369
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -1120,7 +1389,6 @@ program
1120
1389
  console.log(chalk_1.default.dim('No @story: tags found — add @story:ID tags to specs to track story coverage.'));
1121
1390
  }
1122
1391
  }
1123
- const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
1124
1392
  if (threshold !== undefined && report.specLinkRate < threshold) {
1125
1393
  console.error(chalk_1.default.red(`\nSpec link rate ${report.specLinkRate}% is below threshold ${threshold}%`));
1126
1394
  process.exit(1);
@@ -1147,7 +1415,7 @@ program
1147
1415
  const globalOpts = program.opts();
1148
1416
  try {
1149
1417
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1150
- const config = (0, config_1.loadConfig)(configPath);
1418
+ const config = loadConfigWithOverrides(configPath);
1151
1419
  if (opts.configOverride?.length)
1152
1420
  (0, config_1.applyOverrides)(config, opts.configOverride);
1153
1421
  const { trendReport, postTrendToWebhook } = await Promise.resolve().then(() => __importStar(require('./azure/test-runs')));
@@ -1161,6 +1429,24 @@ program
1161
1429
  topN: parseInt(opts.top, 10),
1162
1430
  runNameFilter: opts.runName,
1163
1431
  });
1432
+ const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
1433
+ if (config.toolSettings?.outputLevel === 'diagnostic') {
1434
+ printDiagnosticItems((0, cli_diagnostics_1.getTrendDiagnosticItems)({
1435
+ days: parseInt(opts.days, 10),
1436
+ maxRuns: parseInt(opts.maxRuns, 10),
1437
+ topN: parseInt(opts.top, 10),
1438
+ runNameFilter: opts.runName,
1439
+ webhookType: opts.webhookUrl ? (opts.webhookType ?? 'slack') : undefined,
1440
+ failOnFlaky: Boolean(opts.failOnFlaky),
1441
+ failBelow: threshold,
1442
+ runsAnalyzed: report.runsAnalyzed,
1443
+ totalResults: report.totalResults,
1444
+ flakyCount: report.flakyTests.length,
1445
+ failingCount: report.topFailingTests.length,
1446
+ overrideCount: opts.configOverride?.length ?? 0,
1447
+ }));
1448
+ console.log('');
1449
+ }
1164
1450
  const outputFormat = globalOpts.output;
1165
1451
  if (outputFormat === 'json') {
1166
1452
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -1203,7 +1489,6 @@ program
1203
1489
  console.error(chalk_1.default.red(`\n${report.flakyTests.length} flaky test${report.flakyTests.length !== 1 ? 's' : ''} detected.`));
1204
1490
  process.exit(1);
1205
1491
  }
1206
- const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
1207
1492
  if (threshold !== undefined && report.overallPassRate < threshold) {
1208
1493
  console.error(chalk_1.default.red(`\nOverall pass rate ${report.overallPassRate}% is below threshold ${threshold}%`));
1209
1494
  process.exit(1);
@@ -1218,7 +1503,12 @@ program
1218
1503
  .command('watch')
1219
1504
  .description('Watch local spec files for changes and auto-push on save')
1220
1505
  .option('--dry-run', 'Show what would change without making any modifications')
1506
+ .option('--create-only', 'Only create test cases for unlinked local specs during each watched push')
1507
+ .option('--link-only', 'Only restore local IDs during each watched push')
1508
+ .option('--update-only', 'Only update linked test cases during each watched push')
1221
1509
  .option('--tags <expression>', 'Only sync scenarios matching this tag expression')
1510
+ .option('--source-file <path>', 'Restrict watch and push to a specific local file (repeatable)', collect, [])
1511
+ .option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
1222
1512
  .option('--debounce <ms>', 'Debounce delay in milliseconds before running push (default: 800)', '800')
1223
1513
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
1224
1514
  .option('--ai-provider <provider>', 'AI provider for test step generation')
@@ -1229,10 +1519,12 @@ program
1229
1519
  const globalOpts = program.opts();
1230
1520
  try {
1231
1521
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1232
- const config = (0, config_1.loadConfig)(configPath);
1522
+ const config = loadConfigWithOverrides(configPath);
1233
1523
  if (opts.configOverride?.length)
1234
1524
  (0, config_1.applyOverrides)(config, opts.configOverride);
1525
+ (0, engine_1.validatePushModeOptions)(opts);
1235
1526
  const configDir = path.dirname(configPath);
1527
+ const sourceFiles = normalizeSourceFilesForCli(opts.sourceFile, configDir);
1236
1528
  const debounceMs = parseInt(opts.debounce ?? '800', 10);
1237
1529
  console.log(chalk_1.default.bold('ado-sync watch'));
1238
1530
  console.log(chalk_1.default.dim(`Config: ${configPath}`));
@@ -1240,8 +1532,18 @@ program
1240
1532
  console.log(chalk_1.default.dim(`Plan: ${config.testPlan.id}`));
1241
1533
  if (opts.dryRun)
1242
1534
  console.log(chalk_1.default.yellow('Dry run — no changes will be made'));
1535
+ if (opts.createOnly)
1536
+ console.log(chalk_1.default.dim('Mode: create-only'));
1537
+ if (opts.linkOnly)
1538
+ console.log(chalk_1.default.dim('Mode: link-only'));
1539
+ if (opts.updateOnly)
1540
+ console.log(chalk_1.default.dim('Mode: update-only'));
1243
1541
  if (opts.tags)
1244
1542
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
1543
+ if (sourceFiles?.length)
1544
+ console.log(chalk_1.default.dim(`Files: ${sourceFiles.join(', ')}`));
1545
+ if (opts.include?.length)
1546
+ console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
1245
1547
  console.log('');
1246
1548
  console.log(chalk_1.default.dim(`Watching ${configDir} for changes... (Ctrl+C to stop)`));
1247
1549
  console.log('');
@@ -1259,7 +1561,12 @@ program
1259
1561
  const onProgress = createProgressCallback(isTTY);
1260
1562
  const results = await (0, engine_1.push)(config, configDir, {
1261
1563
  dryRun: opts.dryRun,
1564
+ createOnly: opts.createOnly,
1565
+ linkOnly: opts.linkOnly,
1566
+ updateOnly: opts.updateOnly,
1262
1567
  tags: opts.tags,
1568
+ sourceFiles,
1569
+ includePatterns: opts.include?.length ? opts.include : undefined,
1263
1570
  onProgress,
1264
1571
  aiSummary,
1265
1572
  });
@@ -1287,6 +1594,11 @@ program
1287
1594
  const skipPatterns = ['.ado-sync-cache.json', 'ado-sync.json', 'ado-sync.yml', 'node_modules'];
1288
1595
  if (skipPatterns.some((p) => filename.includes(p)))
1289
1596
  return;
1597
+ if (sourceFiles?.length) {
1598
+ const changedFilePath = path.normalize(path.resolve(configDir, filename));
1599
+ if (!sourceFiles.includes(changedFilePath))
1600
+ return;
1601
+ }
1290
1602
  onChange();
1291
1603
  });
1292
1604
  // Keep process alive
@@ -1315,7 +1627,7 @@ program
1315
1627
  const globalOpts = program.opts();
1316
1628
  try {
1317
1629
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1318
- const config = (0, config_1.loadConfig)(configPath);
1630
+ const config = loadConfigWithOverrides(configPath);
1319
1631
  if (opts.configOverride?.length)
1320
1632
  (0, config_1.applyOverrides)(config, opts.configOverride);
1321
1633
  if (!opts.hours && !opts.days) {