@terra-symposium/standards-quality 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runner.js CHANGED
@@ -10,16 +10,16 @@ IMPORTS
10
10
  import ts from 'typescript';
11
11
  import { aliasFindings, declarationScope, isCallbackDeclaration, isProtectedPath, isRenamableDeclaration, namingFindings, renameDeclaration, renameId } from './naming.js';
12
12
  import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
13
+ import { buildGitHubReport, formatGitHubReport, hasGitHubAuth } from './github-report.js';
13
14
  import { checkImports } from './imports.js';
14
15
  import { collapseInlineIfs } from './inline-if.js';
15
16
  import { collapseTypesAndObjects } from './collapse.js';
16
- import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
17
17
  import { deadCodeEditsIntroduceTypeErrors, deadCodeTypeProgram, fixSingleUseDeclarations, singleUseFindings } from './single-use.js';
18
18
  import { documentSrcFile, documentationFindings, duplicateImportFindings, maintainSrcFile, protectLongImports } from './documentation.js';
19
+ import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
19
20
  import { fileURLToPath } from 'node:url';
20
- import { buildGitHubReport, formatGitHubReport, hasGitHubAuth } from './github-report.js';
21
21
  import { fixObjectForwarding, objectForwardingFindings } from './object-forwarding.js';
22
- import { fixRegexDedup, fixRepeatedClasses, regexFindings, repeatedClassFindings, uiPatternFindings } from './patterns.js';
22
+ import { fixRegexDedup, fixRepeatedClasses, regexFindings, repeatedClassFindings, runTailwindCanonical, uiPatternFindings } from './patterns.js';
23
23
  import { qualityProgress } from './progress.js';
24
24
  import { securityFindings } from './security.js';
25
25
  import { spawnSync } from 'node:child_process';
@@ -53,6 +53,7 @@ const CLI_CMDS = new Set([
53
53
  'dead-code',
54
54
  'security',
55
55
  'repeated-classes',
56
+ 'tailwind-canonical',
56
57
  'regex-dedup',
57
58
  'ui-patterns',
58
59
  'imports',
@@ -190,6 +191,7 @@ export function createQualityCfg(cfg) {
190
191
  const defaultSkipped = ['.git', 'node_modules', 'dist', 'build', 'coverage', 'test-results', '.next', '.astro', '.sanity', '.netlify', '.cache', '.output', '.vercel', '.nuxt', '.husky'];
191
192
  return {
192
193
  ...cfg,
194
+ manualWarnings: cfg.manualWarnings ?? 'error',
193
195
  renameExports: cfg.renameExports ?? false,
194
196
  root: resolve(cfg.root),
195
197
  sourceRoots: cfg.sourceRoots.map((p) => resolve(cfg.root, p)),
@@ -564,12 +566,10 @@ function focusedEslintCfgSrc(cfg, flatCfg, files) {
564
566
  return `
565
567
  import { createRequire } from 'node:module';
566
568
  import { resolve } from 'node:path';
567
-
568
569
  const root = ${JSON.stringify(cfg.root)};
569
570
  const configPath = ${JSON.stringify(flatCfg)};
570
571
  const sourceFiles = ${JSON.stringify(files)};
571
572
  const filePatterns = ${JSON.stringify(patterns)};
572
-
573
573
  try {
574
574
  const requireFromRepo = createRequire(resolve(root, 'package.json'));
575
575
  const { ESLint } = requireFromRepo('eslint');
@@ -1040,10 +1040,13 @@ function assertKnipPath(cfg, path, label) {
1040
1040
  throw new Error(`Knip returned a ${label} symlink outside the repository: ${path}`);
1041
1041
  return absolute;
1042
1042
  }
1043
- /** Converts Knip's validated issue inventory into dead-code review findings.
1043
+ /**
1044
+ * Converts Knip's validated issue inventory into dead-code review findings.
1045
+ *
1044
1046
  * @param cfg Quality configuration defining scope and explicit entry-point contracts.
1045
1047
  * @param importedConsumers Export names referenced by configured source files.
1046
- * @returns Knip findings with safe export-modifier changes marked autofixable.
1048
+ *
1049
+ * @return Knip findings with safe export-modifier changes marked autofixable.
1047
1050
  */
1048
1051
  function collectKnipFindings(cfg, importedConsumers = importedExportConsumers(cfg)) {
1049
1052
  const issues = readKnipIssues(cfg);
@@ -1078,11 +1081,14 @@ function collectKnipFindings(cfg, importedConsumers = importedExportConsumers(cf
1078
1081
  }
1079
1082
  return findings;
1080
1083
  }
1081
- /** Checks whether a name is a configured framework-discovered route export.
1084
+ /**
1085
+ * Checks whether a name is a configured framework-discovered route export.
1086
+ *
1082
1087
  * @param cfg Quality configuration containing framework entry exports.
1083
1088
  * @param file Repository-relative source filename reported by Knip.
1084
1089
  * @param name Exported identifier to check.
1085
- * @returns Whether the configured file or supported TypeScript route pattern includes the export.
1090
+ *
1091
+ * @return Whether the configured file or supported TypeScript route pattern includes the export.
1086
1092
  */
1087
1093
  function isFrameworkEntryExport(cfg, file, name) {
1088
1094
  const publicRoute = !basename(file).startsWith('_');
@@ -1093,9 +1099,12 @@ function isFrameworkEntryExport(cfg, file, name) {
1093
1099
  return pathMatches && names.includes(name);
1094
1100
  });
1095
1101
  }
1096
- /** Selects the TypeScript parser mode from a source filename's extension.
1102
+ /**
1103
+ * Selects the TypeScript parser mode from a source filename's extension.
1104
+ *
1097
1105
  * @param file Source filename to classify.
1098
- * @returns The JavaScript, JSX, TypeScript, or TSX compiler script kind.
1106
+ *
1107
+ * @return The JavaScript, JSX, TypeScript, or TSX compiler script kind.
1099
1108
  */
1100
1109
  function deadCodeScriptKind(file) {
1101
1110
  if (/\.tsx$/i.test(file))
@@ -1106,10 +1115,13 @@ function deadCodeScriptKind(file) {
1106
1115
  return ts.ScriptKind.JS;
1107
1116
  return ts.ScriptKind.TS;
1108
1117
  }
1109
- /** Checks whether Knip identified a direct named export that can become private.
1118
+ /**
1119
+ * Checks whether Knip identified a direct named export that can become private.
1120
+ *
1110
1121
  * @param file Absolute source filename containing the export.
1111
1122
  * @param name Exported identifier reported by Knip.
1112
- * @returns Whether the export modifier can be removed without an ambiguous rewrite.
1123
+ *
1124
+ * @return Whether the export modifier can be removed without an ambiguous rewrite.
1113
1125
  */
1114
1126
  function knipExportCanBeMadePrivate(file, name) {
1115
1127
  if (!existsSync(file) || file.endsWith('.astro'))
@@ -1222,9 +1234,12 @@ function fixKnipExports(cfg, changedFiles) {
1222
1234
  }
1223
1235
  /** Finds export names that configured source files import and actually reference. */
1224
1236
  const importedExportConsumerCache = new WeakMap();
1225
- /** Builds a cache of exported names referenced by configured source consumers.
1237
+ /**
1238
+ * Builds a cache of exported names referenced by configured source consumers.
1239
+ *
1226
1240
  * @param cfg Quality configuration defining roots, aliases, and skipped paths.
1227
- * @returns Export names grouped by their resolved absolute source file.
1241
+ *
1242
+ * @return Export names grouped by their resolved absolute source file.
1228
1243
  */
1229
1244
  function importedExportConsumers(cfg) {
1230
1245
  const srcFiles = filesFor(cfg, true).filter((path) => C_M_JS_TS_X_CLASS_2.test(path) || path.endsWith('.astro'));
@@ -1309,12 +1324,15 @@ function importedExportConsumers(cfg) {
1309
1324
  importedExportConsumerCache.set(cfg, { signature, consumers });
1310
1325
  return consumers;
1311
1326
  }
1312
- /** Accepts proposed edits only when they add no TypeScript errors to changed files.
1327
+ /**
1328
+ * Accepts proposed edits only when they add no TypeScript errors to changed files.
1329
+ *
1313
1330
  * @param program TypeScript program representing the original source.
1314
1331
  * @param proposed Proposed source text keyed by absolute filename.
1315
1332
  * @param original Original source text for the same files.
1316
1333
  * @param onBatch Optional progress callback for each validation batch.
1317
- * @returns Accepted source edits and filenames rejected by the type-safety check.
1334
+ *
1335
+ * @return Accepted source edits and filenames rejected by the type-safety check.
1318
1336
  */
1319
1337
  function acceptTypeSafeEdits(program, proposed, original, onBatch) {
1320
1338
  const rejected = new Set();
@@ -1342,11 +1360,14 @@ function acceptTypeSafeEdits(program, proposed, original, onBatch) {
1342
1360
  rejected,
1343
1361
  };
1344
1362
  }
1345
- /** Prints dead-code findings and sets a failing process status for autofixable issues.
1363
+ /**
1364
+ * Prints dead-code findings and fails when errors or blocking manual findings remain.
1365
+ *
1346
1366
  * @param findings Findings collected by the dead-code coordinator.
1367
+ * @param manualWarnings Policy for manual-only findings.
1347
1368
  * @param typeSafetyVerified Whether the candidate autofixes passed TypeScript safety checks.
1348
1369
  */
1349
- function printDeadCodeFindings(findings, typeSafetyVerified = true) {
1370
+ function printDeadCodeFindings(findings, manualWarnings, typeSafetyVerified = true) {
1350
1371
  if (reportStages) {
1351
1372
  const stage = reportStages.get('dead-code');
1352
1373
  if (stage) {
@@ -1359,16 +1380,20 @@ function printDeadCodeFindings(findings, typeSafetyVerified = true) {
1359
1380
  console.error(' ✓ dead-code checks passed; no autofixable findings or manual warnings remain');
1360
1381
  return;
1361
1382
  }
1362
- const errs = findings.filter((finding) => finding.severity === 'error');
1363
- const warnings = findings.filter((finding) => finding.severity === 'warning');
1383
+ const errs = findings.filter((finding) => finding.severity === 'error' || manualWarnings === 'error');
1384
+ const warnings = findings.filter((finding) => finding.severity === 'warning' && manualWarnings !== 'error');
1385
+ const manualCount = findings.filter((finding) => finding.severity === 'warning').length;
1364
1386
  for (const finding of findings) {
1365
- console.error(` - [${finding.severity.toUpperCase()}] ${finding.rule}: ${finding.file}:${finding.line} ${finding.name}`);
1387
+ const severity = finding.severity === 'warning' && manualWarnings === 'error' ? 'ERROR' : finding.severity.toUpperCase();
1388
+ console.error(` - [${severity}] ${finding.rule}: ${finding.file}:${finding.line} ${finding.name}`);
1366
1389
  console.error(` Reason: ${finding.reason}`);
1367
1390
  const autofix = !finding.autofixable ? 'not safe or not supported' : typeSafetyVerified ? 'available' : 'candidate; fix mode checks TypeScript before writing';
1368
1391
  console.error(` Autofix: ${autofix}`);
1369
1392
  console.error(` Agent action: ${finding.agentAction}`);
1370
1393
  }
1371
- console.error(` ${errs.length} error${errs.length === 1 ? '' : 's'}; ${warnings.length} manual warning${warnings.length === 1 ? '' : 's'}.`);
1394
+ console.error(` ${errs.length} error${errs.length === 1 ? '' : 's'}; ${manualCount} manual finding${manualCount === 1 ? '' : 's'}${manualWarnings === 'warning' ? ` (${warnings.length} warning${warnings.length === 1 ? '' : 's'})` : ''}.`);
1395
+ if (manualWarnings === 'error' && manualCount > 0)
1396
+ console.error(' Manual findings block quality by default. Fix each finding one by one, rerun quality, and continue only when all are resolved before committing.');
1372
1397
  if (errs.length)
1373
1398
  process.exitCode = 1;
1374
1399
  }
@@ -1527,17 +1552,21 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1527
1552
  }
1528
1553
  : finding);
1529
1554
  if (emit)
1530
- printDeadCodeFindings(finalFindings, fix);
1555
+ printDeadCodeFindings(finalFindings, cfg.manualWarnings ?? 'error', fix);
1531
1556
  return finalFindings;
1532
1557
  }
1533
- /** Runs dead-code detectors once without modifying the source tree.
1558
+ /**
1559
+ * Runs dead-code detectors once without modifying the source tree.
1560
+ *
1534
1561
  * @param cfg Quality configuration defining source scope and tools.
1535
1562
  * @param all Whether to inspect all configured source files.
1536
1563
  */
1537
1564
  function runDeadCodeReadOnly(cfg, all) {
1538
1565
  runDeadCode(cfg, false, all);
1539
1566
  }
1540
- /** Dispatches the dead-code coordinator in fix or read-only mode.
1567
+ /**
1568
+ * Dispatches the dead-code coordinator in fix or read-only mode.
1569
+ *
1541
1570
  * @param cfg Quality configuration defining source scope and tools.
1542
1571
  * @param fix Whether eligible changes may be applied to source files.
1543
1572
  * @param all Whether to inspect all configured source files.
@@ -1588,6 +1617,7 @@ function printFindings(findings, fmt) {
1588
1617
  }
1589
1618
  /**
1590
1619
  * Run the optional, read-only GitHub analytics stage.
1620
+ *
1591
1621
  * @param cfg Quality configuration containing repositories and expected settings.
1592
1622
  * @param repos Repository overrides supplied to `terra-quality github`.
1593
1623
  * @param explicit Whether GitHub reporting was requested as the command itself.
@@ -1595,8 +1625,8 @@ function printFindings(findings, fmt) {
1595
1625
  */
1596
1626
  function runGitHubCheck(cfg, repos = [], explicit = false, warn = false) {
1597
1627
  qualityHeader('github', 'Reading open issues, Dependabot, code scanning, and secret scanning alerts');
1598
- const targets = repos.length ? repos : (cfg.github?.repositories ?? [inferGitHubRepo(cfg.root)].filter((repo) => Boolean(repo)));
1599
- if (!targets.length) {
1628
+ const tgts = repos.length ? repos : (cfg.github?.repositories ?? [inferGitHubRepo(cfg.root)].filter((repo) => Boolean(repo)));
1629
+ if (!tgts.length) {
1600
1630
  const detail = 'Set github.repositories in quality.json or pass --repository owner/name.';
1601
1631
  reportGitHubData = { schemaVersion: 1, generatedAt: new Date().toISOString(), state: 'partial', detail, repositories: [] };
1602
1632
  updateGitHubStage('failed', 0);
@@ -1614,7 +1644,7 @@ function runGitHubCheck(cfg, repos = [], explicit = false, warn = false) {
1614
1644
  return;
1615
1645
  }
1616
1646
  try {
1617
- reportGitHubData = buildGitHubReport(targets, cfg.github?.requiredFeatures, undefined, cfg.github?.featureExemptions);
1647
+ reportGitHubData = buildGitHubReport(tgts, cfg.github?.requiredFeatures, undefined, cfg.github?.featureExemptions);
1618
1648
  const findings = reportGitHubData.repositories.flatMap((repo) => repo.findings);
1619
1649
  const failed = reportGitHubData.state === 'partial';
1620
1650
  updateGitHubStage(failed ? 'failed' : findings.length ? 'findings' : 'passed', findings.length);
@@ -1634,7 +1664,9 @@ function runGitHubCheck(cfg, repos = [], explicit = false, warn = false) {
1634
1664
  process.exitCode = 1;
1635
1665
  }
1636
1666
  }
1637
- /** Update the JSON summary for the active GitHub report stage.
1667
+ /**
1668
+ * Update the JSON summary for the active GitHub report stage.
1669
+ *
1638
1670
  * @param status Result state to record.
1639
1671
  * @param findings Number of GitHub findings in the report.
1640
1672
  */
@@ -1645,8 +1677,11 @@ function updateGitHubStage(status, findings) {
1645
1677
  stage.findings = findings;
1646
1678
  }
1647
1679
  }
1648
- /** Infer the current repository slug from its GitHub origin remote.
1680
+ /**
1681
+ * Infer the current repository slug from its GitHub origin remote.
1682
+ *
1649
1683
  * @param root Repository root to inspect.
1684
+ *
1650
1685
  * @return The owner/name slug, or undefined for a non-GitHub remote.
1651
1686
  */
1652
1687
  function inferGitHubRepo(root) {
@@ -1675,6 +1710,25 @@ function runGenericChecks(cfg, analysis) {
1675
1710
  process.exitCode = 1;
1676
1711
  }
1677
1712
  }
1713
+ /**
1714
+ * Runs the configured Tailwind canonical-class check and records its CLI report stage.
1715
+ *
1716
+ * @param cfg Quality configuration containing the Tailwind stylesheet and source roots.
1717
+ * @param fix Whether to write Tailwind's canonical class suggestions.
1718
+ *
1719
+ * @return Promise resolving when the diagnostic or autofix completes.
1720
+ */
1721
+ function runTailwindCanonicalCheck(cfg, fix) {
1722
+ qualityHeader('tailwind-canonical', `${fix ? 'Fixing and checking' : 'Checking'} canonical Tailwind classes`);
1723
+ return runTailwindCanonical(cfg, fix).then(() => undefined, (error) => {
1724
+ const stage = reportStages?.get('tailwind-canonical');
1725
+ if (stage) {
1726
+ stage.status = 'failed';
1727
+ stage.findings = Number(/failed with (\d+) suggestion/.exec(error instanceof Error ? error.message : '')?.[1] ?? 0);
1728
+ }
1729
+ throw error;
1730
+ });
1731
+ }
1678
1732
  /**
1679
1733
  * Renames safe repository-local declarations and repairs matching references.
1680
1734
  *
@@ -2043,25 +2097,20 @@ function runDocumentation(cfg, opts, args, quiet) {
2043
2097
  * @param cfg Normalized quality configuration for the current repository.
2044
2098
  * @param args CLI arguments to dispatch.
2045
2099
  *
2046
- * @return Nothing; command failures are reflected in the process exit code.
2100
+ * @return A completion promise for asynchronous checks, or nothing for synchronous commands.
2047
2101
  */
2048
2102
  export function runQualityCli(cfg, args = process.argv.slice(2)) {
2049
2103
  validCliArgs(args);
2050
2104
  const reportIdx = args.indexOf('--report-json');
2051
2105
  const reportPath = reportIdx < 0 ? undefined : args[reportIdx + 1];
2052
2106
  const cmdArgs = reportIdx < 0 ? args : args.filter((_, i) => i !== reportIdx && i !== reportIdx + 1);
2053
- if (!reportPath) {
2054
- dispatchQualityCli(cfg, cmdArgs);
2055
- return;
2056
- }
2107
+ if (!reportPath)
2108
+ return dispatchQualityCli(cfg, cmdArgs);
2057
2109
  reportStages = new Map();
2058
2110
  reportStageName = undefined;
2059
2111
  reportGitHubData = undefined;
2060
- try {
2061
- dispatchQualityCli(cfg, cmdArgs);
2062
- }
2063
- finally {
2064
- const executed = [...reportStages.values()].filter((s) => {
2112
+ const writeReport = () => {
2113
+ const executed = [...(reportStages?.values() ?? [])].filter((s) => {
2065
2114
  if (s.name.includes('/') || s.name.startsWith('recommendation:'))
2066
2115
  return false;
2067
2116
  const names = { imports: 'import', naming: 'name', aliases: 'alias', forwarding: 'forward', documentation: 'document', typecheck: 'type' };
@@ -2072,6 +2121,8 @@ export function runQualityCli(cfg, args = process.argv.slice(2)) {
2072
2121
  return false;
2073
2122
  if (s.name === 'repeated-classes' && !cfg.repeatedClasses)
2074
2123
  return false;
2124
+ if (s.name === 'tailwind-canonical' && !cfg.tailwindCanonical)
2125
+ return false;
2075
2126
  if (s.name === 'regex-dedup' && !cfg.regexDedup)
2076
2127
  return false;
2077
2128
  if (s.name === 'ui-patterns' && !cfg.uiPatterns)
@@ -2091,6 +2142,7 @@ export function runQualityCli(cfg, args = process.argv.slice(2)) {
2091
2142
  'aliases',
2092
2143
  'security',
2093
2144
  'repeated-classes',
2145
+ 'tailwind-canonical',
2094
2146
  'regex-dedup',
2095
2147
  'ui-patterns',
2096
2148
  'github',
@@ -2106,13 +2158,25 @@ export function runQualityCli(cfg, args = process.argv.slice(2)) {
2106
2158
  writeFileSync(resolve(cfg.root, reportPath), `${JSON.stringify(report, null, 2)}\n`);
2107
2159
  reportStages = undefined;
2108
2160
  reportStageName = undefined;
2161
+ };
2162
+ try {
2163
+ const result = dispatchQualityCli(cfg, cmdArgs);
2164
+ if (result instanceof Promise)
2165
+ return result.finally(writeReport);
2166
+ }
2167
+ catch (err) {
2168
+ writeReport();
2169
+ throw err;
2109
2170
  }
2171
+ writeReport();
2110
2172
  }
2111
2173
  /**
2112
2174
  * Route validated arguments to the requested quality command.
2113
2175
  *
2114
2176
  * @param cfg Normalized repository quality configuration.
2115
2177
  * @param args Validated CLI arguments.
2178
+ *
2179
+ * @return A completion promise for asynchronous checks, or nothing for synchronous commands.
2116
2180
  */
2117
2181
  function dispatchQualityCli(cfg, args) {
2118
2182
  validCliArgs(args);
@@ -2123,7 +2187,7 @@ function dispatchQualityCli(cfg, args) {
2123
2187
  }
2124
2188
  if (args.includes('--help') || args.includes('-h')) {
2125
2189
  console.info('Usage: terra-quality <command> [options]');
2126
- console.info('Commands include quality, history, github, document, format, lint, knip, types, single-use, dead-code, security, and domain.');
2190
+ console.info('Commands include quality, history, github, document, format, lint, knip, types, single-use, dead-code, tailwind-canonical, security, and domain.');
2127
2191
  console.info('Options: --all, --warn, --strict, --skip-commands, --skip-recommendations, --report-json <path>, --repository owner/name; history also accepts --branch <name> and --base-sha <sha>.');
2128
2192
  return;
2129
2193
  }
@@ -2136,6 +2200,11 @@ function dispatchQualityCli(cfg, args) {
2136
2200
  skipRecommendations: args.includes('--skip-recommendations'),
2137
2201
  };
2138
2202
  const fix = (check) => qualityAutofixEnabled(cfg, check, opts.fix);
2203
+ if (cfg.tailwindCanonical && qualityCheckEnabled(cfg, 'tailwind-canonical') && ['quality', 'quality:all', 'quality:baseline', 'imports', 'quality:patterns', 'types:architecture'].includes(cmd))
2204
+ return runTailwindCanonicalCheck(cfg, fix('tailwindCanonical')).then(() => {
2205
+ const checks = { ...cfg.enabledChecks, 'tailwind-canonical': false };
2206
+ return dispatchQualityCli({ ...cfg, enabledChecks: checks }, args);
2207
+ });
2139
2208
  if (cmd === 'github') {
2140
2209
  runGitHubCheck(cfg, optVals(args, '--repository'), true, args.includes('--warn'));
2141
2210
  return;
@@ -2187,6 +2256,11 @@ function dispatchQualityCli(cfg, args) {
2187
2256
  runDeadCodeCmd(cfg, qualityAutofixEnabled(cfg, 'dead-code', opts.fix), opts.all);
2188
2257
  return;
2189
2258
  }
2259
+ if (cmd === 'tailwind-canonical') {
2260
+ if (!qualityCheckEnabled(cfg, 'tailwind-canonical') || !cfg.tailwindCanonical)
2261
+ return;
2262
+ return runTailwindCanonicalCheck(cfg, fix('tailwindCanonical'));
2263
+ }
2190
2264
  if (cmd === 'security' || cmd === 'repeated-classes' || cmd === 'regex-dedup' || cmd === 'ui-patterns') {
2191
2265
  if (cmd === 'repeated-classes' && fix('repeatedClasses')) {
2192
2266
  const result = fixRepeatedClasses(cfg);
@@ -2303,7 +2377,7 @@ function dispatchQualityCli(cfg, args) {
2303
2377
  * @param file Optional path to the quality configuration file.
2304
2378
  * @param args CLI arguments passed to the dispatcher.
2305
2379
  *
2306
- * @return Nothing; command failures are reflected in the process exit code.
2380
+ * @return A completion promise for asynchronous checks, or nothing for synchronous commands.
2307
2381
  */
2308
2382
  export function runQualityFromCfg(file, args = process.argv.slice(2)) {
2309
2383
  validCliArgs(args);
@@ -2331,7 +2405,7 @@ export function runQualityFromCfg(file, args = process.argv.slice(2)) {
2331
2405
  if (!flatCfg)
2332
2406
  throw new Error(`ESLint flat config is required for ${cmd}. Add eslint.config.js, eslint.config.mjs, or eslint.config.ts at ${cfg.root} before running Terra Quality.`);
2333
2407
  }
2334
- runQualityCli(cfg, args);
2408
+ return runQualityCli(cfg, args);
2335
2409
  }
2336
2410
  /**
2337
2411
  * Reject unknown commands and flags before loading configuration or invoking a fixer.
@@ -2364,15 +2438,19 @@ function validCliArgs(args) {
2364
2438
  *
2365
2439
  * @param args CLI arguments.
2366
2440
  * @param flag Flag whose following value is returned.
2441
+ *
2367
2442
  * @return The flag value, or undefined when the flag was not supplied.
2368
2443
  */
2369
2444
  function optVal(args, flag) {
2370
2445
  const idx = args.indexOf(flag);
2371
2446
  return idx < 0 ? undefined : args[idx + 1];
2372
2447
  }
2373
- /** Read each occurrence of a repeated CLI option.
2448
+ /**
2449
+ * Read each occurrence of a repeated CLI option.
2450
+ *
2374
2451
  * @param args Validated command-line arguments.
2375
2452
  * @param flag Repeated flag to read.
2453
+ *
2376
2454
  * @return Values supplied after each occurrence of the flag.
2377
2455
  */
2378
2456
  function optVals(args, flag) {
@@ -2477,5 +2555,8 @@ export function runTypecheck(cfg, args = []) {
2477
2555
  console.error(` ${result.status ? '✗ failed' : '✓ passed'}`);
2478
2556
  }
2479
2557
  if (process.argv[1] && realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url)))
2480
- runQualityFromCfg();
2558
+ Promise.resolve(runQualityFromCfg()).catch((error) => {
2559
+ console.error(error instanceof Error ? error.message : error);
2560
+ process.exitCode = 1;
2561
+ });
2481
2562
  //# sourceMappingURL=runner.js.map