ado-sync 0.1.67 → 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.
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)
@@ -106,6 +119,12 @@ function normalizeSourceFilesForCli(sourceFiles, configDir) {
106
119
  return undefined;
107
120
  return sourceFiles.map((filePath) => path.normalize(path.resolve(configDir, filePath)));
108
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
+ }
109
128
  // ─── AI summary helper ────────────────────────────────────────────────────────
110
129
  /**
111
130
  * Build AiSummaryOpts from parsed CLI opts, falling back to config file values.
@@ -248,6 +267,86 @@ async function runInitWizard(isYaml) {
248
267
  }
249
268
  return JSON.stringify(cfg, null, 2) + '\n';
250
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
+ });
251
350
  // ─── push ─────────────────────────────────────────────────────────────────────
252
351
  program
253
352
  .command('push')
@@ -258,6 +357,7 @@ program
258
357
  .option('--update-only', 'Only update linked test cases; skip unlinked specs and removed-case detection')
259
358
  .option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
260
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, [])
261
361
  .option('--config-override <path=value>', 'Override a config value (repeatable, e.g. --config-override sync.tagPrefix=mytag)', collect, [])
262
362
  .option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
263
363
  .option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
@@ -269,7 +369,7 @@ program
269
369
  const globalOpts = program.opts();
270
370
  try {
271
371
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
272
- const config = (0, config_1.loadConfig)(configPath);
372
+ const config = loadConfigWithOverrides(configPath);
273
373
  if (opts.configOverride?.length)
274
374
  (0, config_1.applyOverrides)(config, opts.configOverride);
275
375
  (0, engine_1.validatePushModeOptions)(opts);
@@ -290,6 +390,8 @@ program
290
390
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
291
391
  if (opts.sourceFile?.length)
292
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(', ')}`));
293
395
  if (opts.configOverride?.length)
294
396
  console.log(chalk_1.default.dim(`Overrides: ${opts.configOverride.join(', ')}`));
295
397
  console.log('');
@@ -298,7 +400,7 @@ program
298
400
  const outputFormat = program.opts().output;
299
401
  const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
300
402
  const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
301
- 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, 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 });
302
404
  if (isTTY && outputFormat !== 'json')
303
405
  clearProgressLine();
304
406
  printResults(results, config.toolSettings?.outputLevel, outputFormat);
@@ -314,12 +416,13 @@ program
314
416
  .option('--dry-run', 'Show what would change without modifying local files')
315
417
  .option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
316
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, [])
317
420
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
318
421
  .action(async (opts) => {
319
422
  const globalOpts = program.opts();
320
423
  try {
321
424
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
322
- const config = (0, config_1.loadConfig)(configPath);
425
+ const config = loadConfigWithOverrides(configPath);
323
426
  if (opts.configOverride?.length)
324
427
  (0, config_1.applyOverrides)(config, opts.configOverride);
325
428
  const configDir = path.dirname(configPath);
@@ -331,11 +434,13 @@ program
331
434
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
332
435
  if (opts.sourceFile?.length)
333
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(', ')}`));
334
439
  console.log('');
335
440
  const isTTY = process.stdout.isTTY ?? false;
336
441
  const outputFormat = program.opts().output;
337
442
  const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
338
- const results = await (0, engine_1.pull)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, sourceFiles: opts.sourceFile, 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 });
339
444
  if (isTTY && outputFormat !== 'json')
340
445
  clearProgressLine();
341
446
  printResults(results, config.toolSettings?.outputLevel, outputFormat);
@@ -350,6 +455,7 @@ program
350
455
  .description('Show diff between local specs and Azure DevOps without making changes')
351
456
  .option('--tags <expression>', 'Only check scenarios matching this tag expression')
352
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, [])
353
459
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
354
460
  .option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
355
461
  .option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
@@ -361,7 +467,7 @@ program
361
467
  const globalOpts = program.opts();
362
468
  try {
363
469
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
364
- const config = (0, config_1.loadConfig)(configPath);
470
+ const config = loadConfigWithOverrides(configPath);
365
471
  if (opts.configOverride?.length)
366
472
  (0, config_1.applyOverrides)(config, opts.configOverride);
367
473
  const configDir = path.dirname(configPath);
@@ -371,13 +477,15 @@ program
371
477
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
372
478
  if (opts.sourceFile?.length)
373
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(', ')}`));
374
482
  console.log('');
375
483
  const aiSummary = buildAiOpts(opts, config, configDir);
376
484
  const isTTY = process.stdout.isTTY ?? false;
377
485
  const outputFormat = program.opts().output;
378
486
  const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
379
487
  const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
380
- const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, 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 });
381
489
  if (isTTY && outputFormat !== 'json')
382
490
  clearProgressLine();
383
491
  printResults(results, config.toolSettings?.outputLevel, outputFormat);
@@ -416,7 +524,7 @@ program
416
524
  const globalOpts = program.opts();
417
525
  try {
418
526
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
419
- const config = (0, config_1.loadConfig)(configPath);
527
+ const config = loadConfigWithOverrides(configPath);
420
528
  const cliPublishOverrides = [
421
529
  ...(opts.testConfiguration ? [/^\d+$/.test(opts.testConfiguration)
422
530
  ? `publishTestResults.testConfiguration.id=${opts.testConfiguration}`
@@ -476,6 +584,28 @@ program
476
584
  console.log(chalk_1.default.dim(`Run ID: ${result.runId}`));
477
585
  console.log(chalk_1.default.dim(`URL: ${result.runUrl}`));
478
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
+ }
479
609
  if (result.issuesSummary) {
480
610
  const s = result.issuesSummary;
481
611
  console.log('');
@@ -640,10 +770,19 @@ program
640
770
  console.log('');
641
771
  // 1. Load config
642
772
  let config;
773
+ let validateDiagnostics;
643
774
  try {
644
775
  config = (0, config_1.loadConfig)(configPath);
645
776
  if (opts.configOverride?.length)
646
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
+ };
647
786
  console.log(chalk_1.default.green(' ✓ Config is valid'));
648
787
  }
649
788
  catch (err) {
@@ -659,6 +798,10 @@ program
659
798
  }
660
799
  catch (err) {
661
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
+ }
662
805
  process.exit(1);
663
806
  }
664
807
  // 3. Verify project
@@ -671,6 +814,10 @@ program
671
814
  }
672
815
  catch (err) {
673
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
+ }
674
821
  process.exit(1);
675
822
  }
676
823
  // 4. Verify test plan(s)
@@ -687,6 +834,10 @@ program
687
834
  allPlansOk = false;
688
835
  }
689
836
  }
837
+ if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
838
+ console.log('');
839
+ printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
840
+ }
690
841
  if (!allPlansOk)
691
842
  process.exit(1);
692
843
  console.log('');
@@ -698,6 +849,7 @@ program
698
849
  .description('Show field-level diff between local specs and Azure DevOps')
699
850
  .option('--tags <expression>', 'Only check scenarios matching this tag expression')
700
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, [])
701
853
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
702
854
  .option('--format <fmt>', 'Output format: text (default) or json')
703
855
  .option('--fail-on-drift', 'Exit with code 1 when any differences are found (useful as a CI quality gate)')
@@ -709,7 +861,7 @@ program
709
861
  const globalOpts = program.opts();
710
862
  try {
711
863
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
712
- const config = (0, config_1.loadConfig)(configPath);
864
+ const config = loadConfigWithOverrides(configPath);
713
865
  if (opts.configOverride?.length)
714
866
  (0, config_1.applyOverrides)(config, opts.configOverride);
715
867
  const configDir = path.dirname(configPath);
@@ -719,9 +871,11 @@ program
719
871
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
720
872
  if (opts.sourceFile?.length)
721
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(', ')}`));
722
876
  console.log('');
723
877
  const aiSummary = buildAiOpts(opts, config, configDir);
724
- const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, 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 });
725
879
  // --format json or global --output json
726
880
  const outputFormat = opts.format ?? program.opts().output;
727
881
  if (outputFormat === 'json') {
@@ -805,7 +959,7 @@ program
805
959
  const globalOpts = program.opts();
806
960
  try {
807
961
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
808
- const config = (0, config_1.loadConfig)(configPath);
962
+ const config = loadConfigWithOverrides(configPath);
809
963
  if (opts.configOverride?.length)
810
964
  (0, config_1.applyOverrides)(config, opts.configOverride);
811
965
  const configDir = path.dirname(configPath);
@@ -930,7 +1084,7 @@ program
930
1084
  const globalOpts = program.opts();
931
1085
  try {
932
1086
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
933
- const config = (0, config_1.loadConfig)(configPath);
1087
+ const config = loadConfigWithOverrides(configPath);
934
1088
  if (opts.configOverride?.length)
935
1089
  (0, config_1.applyOverrides)(config, opts.configOverride);
936
1090
  const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
@@ -980,23 +1134,31 @@ program
980
1134
  const globalOpts = program.opts();
981
1135
  try {
982
1136
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
983
- const config = (0, config_1.loadConfig)(configPath);
1137
+ const config = loadConfigWithOverrides(configPath);
984
1138
  if (opts.configOverride?.length)
985
1139
  (0, config_1.applyOverrides)(config, opts.configOverride);
986
1140
  const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
987
1141
  const { acGate, getWorkItemsByQuery, getWorkItemsByAreaPath } = await Promise.resolve().then(() => __importStar(require('./azure/work-items')));
988
1142
  const client = await AzureClient.create(config);
989
1143
  let storyIds = [];
1144
+ let selectorMode = 'default-states';
1145
+ let selectorValue = opts.states ?? 'Active,Resolved,Closed';
990
1146
  if (opts.query) {
991
1147
  const stories = await getWorkItemsByQuery(client, config.project, opts.query);
992
1148
  storyIds = stories.map((s) => s.id);
1149
+ selectorMode = 'query';
1150
+ selectorValue = opts.query;
993
1151
  }
994
1152
  else if (opts.areaPath) {
995
1153
  const stories = await getWorkItemsByAreaPath(client, config.project, opts.areaPath);
996
1154
  storyIds = stories.map((s) => s.id);
1155
+ selectorMode = 'area-path';
1156
+ selectorValue = opts.areaPath;
997
1157
  }
998
1158
  else if (opts.storyIds) {
999
1159
  storyIds = opts.storyIds.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean);
1160
+ selectorMode = 'story-ids';
1161
+ selectorValue = opts.storyIds;
1000
1162
  }
1001
1163
  else {
1002
1164
  // Default: all active stories in project
@@ -1005,6 +1167,7 @@ program
1005
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]`;
1006
1168
  const stories = await getWorkItemsByQuery(client, config.project, wiql);
1007
1169
  storyIds = stories.map((s) => s.id);
1170
+ selectorValue = states;
1008
1171
  }
1009
1172
  if (storyIds.length === 0) {
1010
1173
  console.log(chalk_1.default.yellow('No stories found to validate.'));
@@ -1015,6 +1178,23 @@ program
1015
1178
  console.log(chalk_1.default.dim(`Stories: ${storyIds.length} to validate`));
1016
1179
  console.log('');
1017
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
+ }
1018
1198
  const outputFormat = globalOpts.output;
1019
1199
  if (outputFormat === 'json') {
1020
1200
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -1055,6 +1235,7 @@ program
1055
1235
  .command('stale')
1056
1236
  .description('List Azure DevOps Test Cases that have no corresponding local spec')
1057
1237
  .option('--tags <expression>', 'Only consider local specs matching this tag expression')
1238
+ .option('--suites', 'Report orphaned suite memberships (affected by stalenessPolicy config)')
1058
1239
  .option('--retire', 'Automatically transition stale Test Cases to Closed state and tag ado-sync:retired')
1059
1240
  .option('--retire-state <state>', 'Target state when retiring (default: Closed)', 'Closed')
1060
1241
  .option('--dry-run', 'Show what --retire would do without making changes')
@@ -1063,7 +1244,7 @@ program
1063
1244
  const globalOpts = program.opts();
1064
1245
  try {
1065
1246
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1066
- const config = (0, config_1.loadConfig)(configPath);
1247
+ const config = loadConfigWithOverrides(configPath);
1067
1248
  if (opts.configOverride?.length)
1068
1249
  (0, config_1.applyOverrides)(config, opts.configOverride);
1069
1250
  const configDir = path.dirname(configPath);
@@ -1075,6 +1256,23 @@ program
1075
1256
  console.log(chalk_1.default.dim(`Retire: will transition to "${opts.retireState}"${opts.dryRun ? ' (dry-run)' : ''}`));
1076
1257
  console.log('');
1077
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
+ }
1078
1276
  const outputFormat = globalOpts.output;
1079
1277
  if (outputFormat === 'json') {
1080
1278
  process.stdout.write(JSON.stringify(staleCases, null, 2) + '\n');
@@ -1139,7 +1337,7 @@ program
1139
1337
  const globalOpts = program.opts();
1140
1338
  try {
1141
1339
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1142
- const config = (0, config_1.loadConfig)(configPath);
1340
+ const config = loadConfigWithOverrides(configPath);
1143
1341
  if (opts.configOverride?.length)
1144
1342
  (0, config_1.applyOverrides)(config, opts.configOverride);
1145
1343
  const configDir = path.dirname(configPath);
@@ -1149,6 +1347,23 @@ program
1149
1347
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
1150
1348
  console.log('');
1151
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
+ }
1152
1367
  const outputFormat = globalOpts.output;
1153
1368
  if (outputFormat === 'json') {
1154
1369
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -1174,7 +1389,6 @@ program
1174
1389
  console.log(chalk_1.default.dim('No @story: tags found — add @story:ID tags to specs to track story coverage.'));
1175
1390
  }
1176
1391
  }
1177
- const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
1178
1392
  if (threshold !== undefined && report.specLinkRate < threshold) {
1179
1393
  console.error(chalk_1.default.red(`\nSpec link rate ${report.specLinkRate}% is below threshold ${threshold}%`));
1180
1394
  process.exit(1);
@@ -1201,7 +1415,7 @@ program
1201
1415
  const globalOpts = program.opts();
1202
1416
  try {
1203
1417
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1204
- const config = (0, config_1.loadConfig)(configPath);
1418
+ const config = loadConfigWithOverrides(configPath);
1205
1419
  if (opts.configOverride?.length)
1206
1420
  (0, config_1.applyOverrides)(config, opts.configOverride);
1207
1421
  const { trendReport, postTrendToWebhook } = await Promise.resolve().then(() => __importStar(require('./azure/test-runs')));
@@ -1215,6 +1429,24 @@ program
1215
1429
  topN: parseInt(opts.top, 10),
1216
1430
  runNameFilter: opts.runName,
1217
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
+ }
1218
1450
  const outputFormat = globalOpts.output;
1219
1451
  if (outputFormat === 'json') {
1220
1452
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -1257,7 +1489,6 @@ program
1257
1489
  console.error(chalk_1.default.red(`\n${report.flakyTests.length} flaky test${report.flakyTests.length !== 1 ? 's' : ''} detected.`));
1258
1490
  process.exit(1);
1259
1491
  }
1260
- const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
1261
1492
  if (threshold !== undefined && report.overallPassRate < threshold) {
1262
1493
  console.error(chalk_1.default.red(`\nOverall pass rate ${report.overallPassRate}% is below threshold ${threshold}%`));
1263
1494
  process.exit(1);
@@ -1277,6 +1508,7 @@ program
1277
1508
  .option('--update-only', 'Only update linked test cases during each watched push')
1278
1509
  .option('--tags <expression>', 'Only sync scenarios matching this tag expression')
1279
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, [])
1280
1512
  .option('--debounce <ms>', 'Debounce delay in milliseconds before running push (default: 800)', '800')
1281
1513
  .option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
1282
1514
  .option('--ai-provider <provider>', 'AI provider for test step generation')
@@ -1287,7 +1519,7 @@ program
1287
1519
  const globalOpts = program.opts();
1288
1520
  try {
1289
1521
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1290
- const config = (0, config_1.loadConfig)(configPath);
1522
+ const config = loadConfigWithOverrides(configPath);
1291
1523
  if (opts.configOverride?.length)
1292
1524
  (0, config_1.applyOverrides)(config, opts.configOverride);
1293
1525
  (0, engine_1.validatePushModeOptions)(opts);
@@ -1310,6 +1542,8 @@ program
1310
1542
  console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
1311
1543
  if (sourceFiles?.length)
1312
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(', ')}`));
1313
1547
  console.log('');
1314
1548
  console.log(chalk_1.default.dim(`Watching ${configDir} for changes... (Ctrl+C to stop)`));
1315
1549
  console.log('');
@@ -1332,6 +1566,7 @@ program
1332
1566
  updateOnly: opts.updateOnly,
1333
1567
  tags: opts.tags,
1334
1568
  sourceFiles,
1569
+ includePatterns: opts.include?.length ? opts.include : undefined,
1335
1570
  onProgress,
1336
1571
  aiSummary,
1337
1572
  });
@@ -1392,7 +1627,7 @@ program
1392
1627
  const globalOpts = program.opts();
1393
1628
  try {
1394
1629
  const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
1395
- const config = (0, config_1.loadConfig)(configPath);
1630
+ const config = loadConfigWithOverrides(configPath);
1396
1631
  if (opts.configOverride?.length)
1397
1632
  (0, config_1.applyOverrides)(config, opts.configOverride);
1398
1633
  if (!opts.hours && !opts.days) {