truecourse 0.5.8-windows.3 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,8 +114,25 @@ truecourse rules categories --disable style # Disable a category
114
114
  truecourse rules llm # Show LLM rules status
115
115
  truecourse rules llm --enable # Enable LLM rules
116
116
  truecourse rules llm --disable # Disable LLM rules
117
+
118
+ # Individual rules
119
+ truecourse rules list # List rules with on/off status
120
+ truecourse rules list --disabled # Show only disabled rules
121
+ truecourse rules disable <ruleKey> # Disable a single rule
122
+ truecourse rules enable <ruleKey> # Re-enable a single rule
123
+ truecourse rules reset [ruleKey] # Clear per-rule overrides (one or all)
117
124
  ```
118
125
 
126
+ Disabled rules are skipped at analyze time (no detection cost, no LLM
127
+ calls) and any existing violations from them are hidden from the
128
+ dashboard and `truecourse list` until re-enabled. The list of disabled
129
+ rule keys lives in `<repo>/.truecourse/config.json` under
130
+ `disabledRules`, which is intended to be committed.
131
+
132
+ In the dashboard you can also toggle rules from the Rules panel
133
+ (Shield icon in the top-right) or silence a noisy rule directly from
134
+ any violation card via the **⋮** menu → **Disable rule for this repo**.
135
+
119
136
  ### Git Hooks
120
137
 
121
138
  TrueCourse can install a pre-commit hook that blocks commits introducing new violations at or above a configured severity:
package/cli.mjs CHANGED
@@ -11168,6 +11168,11 @@ var init_service_patterns = __esm({
11168
11168
  import * as ts from "typescript";
11169
11169
  import { dirname as dirname2, join as join2 } from "path";
11170
11170
  import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
11171
+ function createRepoScopedCompilerHost(repoPath, options) {
11172
+ const host = ts.createCompilerHost(options);
11173
+ host.getCurrentDirectory = () => repoPath;
11174
+ return host;
11175
+ }
11171
11176
  function buildScopedCompilerOptions(rootPath) {
11172
11177
  const result = [];
11173
11178
  const candidates = [join2(rootPath, "tsconfig.json")];
@@ -11219,14 +11224,15 @@ function resolveModule(specifier, containingFile, scoped) {
11219
11224
  return null;
11220
11225
  return resolved;
11221
11226
  }
11222
- function analyzeSemantics(filePaths, scoped) {
11227
+ function analyzeSemantics(filePaths, scoped, repoPath) {
11223
11228
  const baseOptions = scoped[scoped.length - 1]?.options ?? {};
11224
11229
  const options = {
11225
11230
  ...baseOptions,
11226
11231
  skipLibCheck: true,
11227
11232
  noEmit: true
11228
11233
  };
11229
- const program3 = ts.createProgram(filePaths, options);
11234
+ const host = createRepoScopedCompilerHost(repoPath, options);
11235
+ const program3 = ts.createProgram(filePaths, options, host);
11230
11236
  const checker = program3.getTypeChecker();
11231
11237
  const exportMap = /* @__PURE__ */ new Map();
11232
11238
  const interfaceMethods = /* @__PURE__ */ new Set();
@@ -11321,7 +11327,7 @@ function getNodeAtPosition(sourceFile, line, column, endLine, endColumn) {
11321
11327
  }
11322
11328
  return candidate;
11323
11329
  }
11324
- function createTypeQueryService(filePaths, scopedOptions) {
11330
+ function createTypeQueryService(filePaths, scopedOptions, repoPath) {
11325
11331
  const scopedPrograms = [];
11326
11332
  const fileToProgram = /* @__PURE__ */ new Map();
11327
11333
  const filesByScope = /* @__PURE__ */ new Map();
@@ -11348,7 +11354,8 @@ function createTypeQueryService(filePaths, scopedOptions) {
11348
11354
  skipLibCheck: true,
11349
11355
  noEmit: true
11350
11356
  };
11351
- const program3 = ts.createProgram(files, options);
11357
+ const host = createRepoScopedCompilerHost(repoPath, options);
11358
+ const program3 = ts.createProgram(files, options, host);
11352
11359
  const checker = program3.getTypeChecker();
11353
11360
  const sp = { program: program3, checker, files: new Set(files) };
11354
11361
  scopedPrograms.push(sp);
@@ -11356,7 +11363,9 @@ function createTypeQueryService(filePaths, scopedOptions) {
11356
11363
  fileToProgram.set(f, sp);
11357
11364
  }
11358
11365
  if (scopedPrograms.length === 0) {
11359
- const program3 = ts.createProgram(filePaths, { skipLibCheck: true, noEmit: true });
11366
+ const options = { skipLibCheck: true, noEmit: true };
11367
+ const host = createRepoScopedCompilerHost(repoPath, options);
11368
+ const program3 = ts.createProgram(filePaths, options, host);
11360
11369
  const checker = program3.getTypeChecker();
11361
11370
  const sp = { program: program3, checker, files: new Set(filePaths) };
11362
11371
  scopedPrograms.push(sp);
@@ -95191,7 +95200,7 @@ async function runAnalysis(repoPath, _branch, onProgress, options) {
95191
95200
  const scopedOptions = buildScopedCompilerOptions(repoPath);
95192
95201
  if (scopedOptions.length > 0) {
95193
95202
  const filePaths = fileAnalyses.map((fa) => fa.filePath);
95194
- const { exportMap } = analyzeSemantics(filePaths, scopedOptions);
95203
+ const { exportMap } = analyzeSemantics(filePaths, scopedOptions, repoPath);
95195
95204
  for (const fa of fileAnalyses) {
95196
95205
  const fileExports = exportMap.get(fa.filePath);
95197
95206
  if (!fileExports)
@@ -100451,7 +100460,7 @@ function deriveDomain(key) {
100451
100460
  const validDomains = new Set(DOMAIN_ORDER);
100452
100461
  return validDomains.has(prefix) ? prefix : void 0;
100453
100462
  }
100454
- function toAnalysisRule(rule) {
100463
+ function toAnalysisRule(rule, enabled = rule.enabled) {
100455
100464
  return {
100456
100465
  key: rule.key,
100457
100466
  category: rule.category,
@@ -100459,20 +100468,25 @@ function toAnalysisRule(rule) {
100459
100468
  name: rule.name,
100460
100469
  description: rule.description,
100461
100470
  prompt: rule.prompt ?? void 0,
100462
- enabled: rule.enabled,
100471
+ enabled,
100463
100472
  severity: rule.severity,
100464
100473
  type: rule.type,
100465
100474
  contextRequirement: rule.contextRequirement
100466
100475
  };
100467
100476
  }
100477
+ async function getRules(repoPath) {
100478
+ const disabled = repoPath ? new Set(readProjectConfig(repoPath).disabledRules ?? []) : null;
100479
+ return getAllDefaultRules().map((r) => toAnalysisRule(r, disabled ? r.enabled && !disabled.has(r.key) : r.enabled));
100480
+ }
100468
100481
  async function getEnabledRules() {
100469
- return getAllDefaultRules().filter((r) => r.enabled).map(toAnalysisRule);
100482
+ return getAllDefaultRules().filter((r) => r.enabled).map((r) => toAnalysisRule(r));
100470
100483
  }
100471
100484
  var init_rules_service = __esm({
100472
100485
  "packages/core/dist/services/rules.service.js"() {
100473
100486
  "use strict";
100474
100487
  init_dist6();
100475
100488
  init_dist7();
100489
+ init_project_config();
100476
100490
  }
100477
100491
  });
100478
100492
 
@@ -102811,7 +102825,7 @@ var require_main = __commonJS({
102811
102825
  "node_modules/.pnpm/dotenv@16.6.1/node_modules/dotenv/lib/main.js"(exports, module) {
102812
102826
  var fs17 = __require("fs");
102813
102827
  var path23 = __require("path");
102814
- var os8 = __require("os");
102828
+ var os9 = __require("os");
102815
102829
  var crypto2 = __require("crypto");
102816
102830
  var packageJson = require_package();
102817
102831
  var version = packageJson.version;
@@ -102934,7 +102948,7 @@ var require_main = __commonJS({
102934
102948
  return null;
102935
102949
  }
102936
102950
  function _resolveHome(envPath) {
102937
- return envPath[0] === "~" ? path23.join(os8.homedir(), envPath.slice(1)) : envPath;
102951
+ return envPath[0] === "~" ? path23.join(os9.homedir(), envPath.slice(1)) : envPath;
102938
102952
  }
102939
102953
  function _configVault(options) {
102940
102954
  const debug2 = Boolean(options && options.debug);
@@ -105410,7 +105424,8 @@ function compareDeterministicViolations(current, previous) {
105410
105424
  }
105411
105425
  async function runViolationPipeline(input) {
105412
105426
  await initParsers();
105413
- const { repoPath, analysisId, now, result, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap, previousActiveViolations, changedFileSet, onProgress, tracker, provider: externalProvider, enabledCategories, enableLlmRules, signal } = input;
105427
+ const { repoPath, analysisId, now, result, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap, previousActiveViolations, changedFileSet, onProgress, tracker, provider: externalProvider, enabledCategories, enableLlmRules, disabledRules, signal } = input;
105428
+ const disabledRuleSet = new Set(disabledRules ?? []);
105414
105429
  const added = [];
105415
105430
  const unchanged = [];
105416
105431
  const resolved = [];
@@ -105428,7 +105443,7 @@ async function runViolationPipeline(input) {
105428
105443
  for (const [name, id] of dbIdMap)
105429
105444
  databaseIdToName.set(id, name);
105430
105445
  const previousActiveCodeViolations = previousActiveViolations.filter((v) => v.filePath != null);
105431
- let allRules = (await getEnabledRules()).filter((r) => !enabledCategories || enabledCategories.includes(r.domain ?? r.category)).filter((r) => enableLlmRules !== false || r.type !== "llm");
105446
+ let allRules = (await getEnabledRules()).filter((r) => !enabledCategories || enabledCategories.includes(r.domain ?? r.category)).filter((r) => enableLlmRules !== false || r.type !== "llm").filter((r) => !disabledRuleSet.has(r.key));
105432
105447
  let llmSkipped = false;
105433
105448
  const enabledDeterministic = allRules.filter((r) => r.type === "deterministic");
105434
105449
  const enabledLlm = allRules.filter((r) => r.type === "llm");
@@ -105469,7 +105484,7 @@ async function runViolationPipeline(input) {
105469
105484
  const tsFiles = filesToScan.filter(({ filePath: fp }) => /\.(ts|tsx|js|jsx)$/.test(fp)).map(({ filePath: fp, resolve: res }) => res ? path12.resolve(repoPath, fp) : path12.isAbsolute(fp) ? fp : path12.join(repoPath, fp));
105470
105485
  if (tsFiles.length > 0) {
105471
105486
  const scoped = buildScopedCompilerOptions(repoPath);
105472
- typeQuery = createTypeQueryService(tsFiles, scoped);
105487
+ typeQuery = createTypeQueryService(tsFiles, scoped, repoPath);
105473
105488
  }
105474
105489
  }
105475
105490
  let schemaIndex;
@@ -106626,6 +106641,7 @@ async function analyzeCore(project, options) {
106626
106641
  tracker: options.tracker,
106627
106642
  enabledCategories: effectiveCategories,
106628
106643
  enableLlmRules: effectiveLlmRules,
106644
+ disabledRules: projectConfig.disabledRules,
106629
106645
  provider,
106630
106646
  signal,
106631
106647
  onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
@@ -107459,6 +107475,7 @@ var MacOSService = class {
107459
107475
  envVars.PATH = process.env.PATH;
107460
107476
  }
107461
107477
  envVars.TRUECOURSE_LOG_DIR = path17.dirname(logPath);
107478
+ envVars.TRUECOURSE_HOME = path17.join(os5.homedir(), ".truecourse");
107462
107479
  fs12.mkdirSync(PLIST_DIR, { recursive: true });
107463
107480
  fs12.mkdirSync(path17.dirname(logPath), { recursive: true });
107464
107481
  const plist = buildPlist(serverPath, logPath, envVars);
@@ -107518,7 +107535,8 @@ var SERVICE_NAME = "truecourse";
107518
107535
  var UNIT_DIR = path18.join(os6.homedir(), ".config", "systemd", "user");
107519
107536
  var UNIT_PATH = path18.join(UNIT_DIR, `${SERVICE_NAME}.service`);
107520
107537
  function buildUnitFile(serverPath, logPath) {
107521
- const envFile = path18.join(os6.homedir(), ".truecourse", ".env");
107538
+ const truecourseHome = path18.join(os6.homedir(), ".truecourse");
107539
+ const envFile = path18.join(truecourseHome, ".env");
107522
107540
  const logDir = path18.dirname(logPath);
107523
107541
  return `[Unit]
107524
107542
  Description=TrueCourse Server
@@ -107530,6 +107548,7 @@ ExecStart=${process.execPath} ${serverPath}
107530
107548
  Restart=on-failure
107531
107549
  RestartSec=5
107532
107550
  EnvironmentFile=${envFile}
107551
+ Environment=TRUECOURSE_HOME=${truecourseHome}
107533
107552
  Environment=TRUECOURSE_LOG_DIR=${logDir}
107534
107553
  StandardOutput=append:${path18.join(logDir, "dashboard.out.log")}
107535
107554
  StandardError=append:${path18.join(logDir, "dashboard.err.log")}
@@ -107592,11 +107611,11 @@ var LinuxService = class {
107592
107611
 
107593
107612
  // tools/cli/src/commands/service/windows.ts
107594
107613
  import { execSync as execSync3 } from "node:child_process";
107614
+ import os7 from "node:os";
107595
107615
  import path19 from "node:path";
107596
- var SERVICE_NAME2 = "TrueCourse";
107597
- var SERVICE_ID = "dashboard";
107616
+ var SERVICE_DISPLAY_NAME = "TrueCourse";
107617
+ var SERVICE_SCM_NAME = "truecourse.exe";
107598
107618
  var WindowsService = class {
107599
- svc;
107600
107619
  async getNodeWindows() {
107601
107620
  try {
107602
107621
  return __require("node-windows");
@@ -107610,20 +107629,20 @@ var WindowsService = class {
107610
107629
  const nw = await this.getNodeWindows();
107611
107630
  const { Service } = nw;
107612
107631
  const logDir = path19.dirname(logPath);
107632
+ const truecourseHome = path19.join(os7.homedir(), ".truecourse");
107613
107633
  return new Promise((resolve8, reject) => {
107614
107634
  const svc = new Service({
107615
- name: SERVICE_NAME2,
107616
- id: SERVICE_ID,
107635
+ name: SERVICE_DISPLAY_NAME,
107617
107636
  description: "TrueCourse Server",
107618
107637
  script: serverPath,
107619
107638
  nodeOptions: [],
107620
107639
  // Land wrapper logs in our shared log dir (default is the
107621
107640
  // node-windows install dir, often inside an npx cache).
107622
107641
  logpath: logDir,
107623
- env: [{
107624
- name: "TRUECOURSE_LOG_DIR",
107625
- value: logDir
107626
- }]
107642
+ env: [
107643
+ { name: "TRUECOURSE_HOME", value: truecourseHome },
107644
+ { name: "TRUECOURSE_LOG_DIR", value: logDir }
107645
+ ]
107627
107646
  });
107628
107647
  svc.on("install", () => {
107629
107648
  svc.start();
@@ -107638,7 +107657,7 @@ var WindowsService = class {
107638
107657
  const { Service } = nw;
107639
107658
  return new Promise((resolve8, reject) => {
107640
107659
  const svc = new Service({
107641
- name: SERVICE_NAME2,
107660
+ name: SERVICE_DISPLAY_NAME,
107642
107661
  script: ""
107643
107662
  // Not needed for uninstall
107644
107663
  });
@@ -107648,14 +107667,14 @@ var WindowsService = class {
107648
107667
  });
107649
107668
  }
107650
107669
  async start() {
107651
- execSync3(`sc.exe start ${SERVICE_NAME2}`, { stdio: "pipe" });
107670
+ execSync3(`sc.exe start ${SERVICE_SCM_NAME}`, { stdio: "pipe" });
107652
107671
  }
107653
107672
  async stop() {
107654
- execSync3(`sc.exe stop ${SERVICE_NAME2}`, { stdio: "pipe" });
107673
+ execSync3(`sc.exe stop ${SERVICE_SCM_NAME}`, { stdio: "pipe" });
107655
107674
  }
107656
107675
  async status() {
107657
107676
  try {
107658
- const output = execSync3(`sc.exe query ${SERVICE_NAME2}`, {
107677
+ const output = execSync3(`sc.exe query ${SERVICE_SCM_NAME}`, {
107659
107678
  stdio: ["pipe", "pipe", "pipe"],
107660
107679
  encoding: "utf-8"
107661
107680
  });
@@ -107671,7 +107690,7 @@ var WindowsService = class {
107671
107690
  }
107672
107691
  async isInstalled() {
107673
107692
  try {
107674
- execSync3(`sc.exe query ${SERVICE_NAME2}`, { stdio: "pipe" });
107693
+ execSync3(`sc.exe query ${SERVICE_SCM_NAME}`, { stdio: "pipe" });
107675
107694
  return true;
107676
107695
  } catch {
107677
107696
  return false;
@@ -107698,28 +107717,24 @@ function getPlatform() {
107698
107717
  // tools/cli/src/commands/service/logs.ts
107699
107718
  import fs14 from "node:fs";
107700
107719
  import path20 from "node:path";
107701
- import os7 from "node:os";
107720
+ import os8 from "node:os";
107702
107721
  var MAX_LOG_SIZE2 = 10 * 1024 * 1024;
107703
107722
  var MAX_LOG_FILES2 = 5;
107704
107723
  var POLL_INTERVAL_MS = 500;
107705
- var KNOWN_LOG_FILENAMES = [
107706
- "dashboard.log",
107707
- "dashboard.out.log",
107708
- "dashboard.err.log",
107709
- "dashboard.wrapper.log",
107710
- // Legacy (pre-unified-layout) — kept for backward compatibility with
107711
- // existing macOS/Linux service registrations.
107712
- "truecourse.log",
107713
- "truecourse.error.log"
107724
+ var ROTATABLE_LOG_NAMES = [
107725
+ // Server's structured log — always rotated as a known target so we don't
107726
+ // have to detect it among arbitrary other .log files in the dir.
107727
+ "dashboard.log"
107714
107728
  ];
107715
107729
  function getLogDir() {
107716
- return path20.join(os7.homedir(), ".truecourse", "logs");
107730
+ return path20.join(os8.homedir(), ".truecourse", "logs");
107717
107731
  }
107718
107732
  function getLogPath() {
107719
107733
  return path20.join(getLogDir(), "dashboard.log");
107720
107734
  }
107721
107735
  function existingLogFiles(logDir) {
107722
- return KNOWN_LOG_FILENAMES.map((name) => path20.join(logDir, name)).filter((p2) => fs14.existsSync(p2));
107736
+ if (!fs14.existsSync(logDir)) return [];
107737
+ return fs14.readdirSync(logDir).filter((name) => /\.log$/.test(name)).sort().map((name) => path20.join(logDir, name));
107723
107738
  }
107724
107739
  function rotateOne(logFile) {
107725
107740
  if (!fs14.existsSync(logFile)) return;
@@ -107736,9 +107751,11 @@ function rotateOne(logFile) {
107736
107751
  fs14.renameSync(logFile, `${logFile}.1`);
107737
107752
  }
107738
107753
  function rotateLogs(logDir) {
107739
- for (const name of KNOWN_LOG_FILENAMES) {
107754
+ for (const name of ROTATABLE_LOG_NAMES) {
107740
107755
  rotateOne(path20.join(logDir, name));
107741
107756
  }
107757
+ if (!fs14.existsSync(logDir)) return;
107758
+ for (const file of existingLogFiles(logDir)) rotateOne(file);
107742
107759
  }
107743
107760
  function readLastLines(filePath, maxLines) {
107744
107761
  const content = fs14.readFileSync(filePath, "utf-8");
@@ -108065,6 +108082,16 @@ init_dist4();
108065
108082
 
108066
108083
  // packages/core/dist/services/violation-query.service.js
108067
108084
  init_analysis_store();
108085
+ init_project_config();
108086
+ function getDisabledRuleKeys(repoPath) {
108087
+ return new Set(readProjectConfig(repoPath).disabledRules ?? []);
108088
+ }
108089
+ function filterDisabled(repoPath, violations) {
108090
+ const disabled = getDisabledRuleKeys(repoPath);
108091
+ if (disabled.size === 0)
108092
+ return violations;
108093
+ return violations.filter((v) => !disabled.has(v.ruleKey));
108094
+ }
108068
108095
  var SEVERITY_ORDER = {
108069
108096
  critical: 0,
108070
108097
  high: 1,
@@ -108086,6 +108113,7 @@ function listViolations(repoPath, options = {}) {
108086
108113
  return { violations: [], total: 0 };
108087
108114
  violations = historical;
108088
108115
  }
108116
+ violations = filterDisabled(repoPath, violations);
108089
108117
  const statusMode = options.status ?? "active";
108090
108118
  let filtered;
108091
108119
  if (statusMode === "resolved") {
@@ -108159,7 +108187,22 @@ function getDiffResult(repoPath) {
108159
108187
  return null;
108160
108188
  const latest = readLatest(repoPath);
108161
108189
  const isStale = latest ? latest.analysis.id !== diff.baseAnalysisId : false;
108162
- return { diff, isStale };
108190
+ const disabled = getDisabledRuleKeys(repoPath);
108191
+ if (disabled.size === 0)
108192
+ return { diff, isStale };
108193
+ const newViolations = diff.newViolations.filter((v) => !disabled.has(v.ruleKey));
108194
+ const resolvedViolations = diff.resolvedViolations.filter((v) => !disabled.has(v.ruleKey));
108195
+ const filteredDiff = {
108196
+ ...diff,
108197
+ newViolations,
108198
+ resolvedViolations,
108199
+ summary: {
108200
+ ...diff.summary,
108201
+ newCount: newViolations.length,
108202
+ resolvedCount: resolvedViolations.length
108203
+ }
108204
+ };
108205
+ return { diff: filteredDiff, isStale };
108163
108206
  }
108164
108207
 
108165
108208
  // tools/cli/src/commands/list.ts
@@ -108221,6 +108264,7 @@ async function runListDiff() {
108221
108264
  init_dist4();
108222
108265
  init_dist7();
108223
108266
  init_project_config();
108267
+ init_rules_service();
108224
108268
  init_helpers();
108225
108269
  var ALL_CATEGORIES = [...DOMAIN_ORDER];
108226
108270
  async function runRulesCategories(options) {
@@ -108284,6 +108328,82 @@ async function runRulesLlm(options) {
108284
108328
  O2.info("Override with: truecourse rules llm --enable/--disable");
108285
108329
  }
108286
108330
  }
108331
+ var COLOR_ENABLED = "\x1B[32menabled\x1B[0m";
108332
+ var COLOR_DISABLED = "\x1B[31mdisabled\x1B[0m";
108333
+ var COLOR_DIM = (text) => `\x1B[2m${text}\x1B[0m`;
108334
+ function setRuleEnabled(repoPath, ruleKey, enabled) {
108335
+ const current = readProjectConfig(repoPath);
108336
+ const set2 = new Set(current.disabledRules ?? []);
108337
+ if (enabled) set2.delete(ruleKey);
108338
+ else set2.add(ruleKey);
108339
+ updateProjectConfig(repoPath, { disabledRules: [...set2].sort() });
108340
+ }
108341
+ async function requireRuleKey(ruleKey) {
108342
+ const all = await getRules();
108343
+ if (!all.some((r) => r.key === ruleKey)) {
108344
+ O2.error(`Unknown rule: ${ruleKey}. Run 'truecourse rules list' to see available rules.`);
108345
+ process.exit(1);
108346
+ }
108347
+ }
108348
+ async function runRulesEnable({ ruleKey }) {
108349
+ const repo = requireRegisteredRepo();
108350
+ await requireRuleKey(ruleKey);
108351
+ setRuleEnabled(repo.path, ruleKey, true);
108352
+ O2.success(`Enabled rule '${ruleKey}' for ${repo.name}.`);
108353
+ }
108354
+ async function runRulesDisable({ ruleKey }) {
108355
+ const repo = requireRegisteredRepo();
108356
+ await requireRuleKey(ruleKey);
108357
+ setRuleEnabled(repo.path, ruleKey, false);
108358
+ O2.success(`Disabled rule '${ruleKey}' for ${repo.name}.`);
108359
+ }
108360
+ async function runRulesList(options) {
108361
+ const repo = requireRegisteredRepo();
108362
+ const rules = await getRules(repo.path);
108363
+ const search = options.search?.toLowerCase();
108364
+ let filtered = rules;
108365
+ if (options.domain) {
108366
+ filtered = filtered.filter((r) => (r.domain ?? r.category) === options.domain);
108367
+ }
108368
+ if (options.enabled) filtered = filtered.filter((r) => r.enabled);
108369
+ if (options.disabled) filtered = filtered.filter((r) => !r.enabled);
108370
+ if (search) {
108371
+ filtered = filtered.filter(
108372
+ (r) => r.key.toLowerCase().includes(search) || r.name.toLowerCase().includes(search) || r.description?.toLowerCase().includes(search)
108373
+ );
108374
+ }
108375
+ if (filtered.length === 0) {
108376
+ O2.info("No rules match the given filters.");
108377
+ return;
108378
+ }
108379
+ const enabledCount = filtered.filter((r) => r.enabled).length;
108380
+ const disabledCount = filtered.length - enabledCount;
108381
+ O2.info(
108382
+ `Rules for ${repo.name}: ${filtered.length} shown (${enabledCount} enabled, ${disabledCount} disabled).`
108383
+ );
108384
+ const keyWidth = Math.min(
108385
+ 60,
108386
+ filtered.reduce((max, r) => Math.max(max, r.key.length), 0)
108387
+ );
108388
+ for (const r of filtered) {
108389
+ const status = r.enabled ? COLOR_ENABLED : COLOR_DISABLED;
108390
+ const domain = r.domain ?? r.category;
108391
+ console.log(` ${r.key.padEnd(keyWidth)} ${status} ${COLOR_DIM(`[${domain}/${r.severity}]`)} ${r.name}`);
108392
+ }
108393
+ console.log("");
108394
+ O2.info("Toggle with: truecourse rules enable <key> | truecourse rules disable <key>");
108395
+ }
108396
+ async function runRulesReset({ ruleKey }) {
108397
+ const repo = requireRegisteredRepo();
108398
+ if (ruleKey) {
108399
+ await requireRuleKey(ruleKey);
108400
+ setRuleEnabled(repo.path, ruleKey, true);
108401
+ O2.success(`Re-enabled '${ruleKey}' for ${repo.name}.`);
108402
+ return;
108403
+ }
108404
+ updateProjectConfig(repo.path, { disabledRules: [] });
108405
+ O2.success(`Cleared per-rule overrides for ${repo.name}.`);
108406
+ }
108287
108407
 
108288
108408
  // tools/cli/src/commands/hooks.ts
108289
108409
  import { execSync as execSync4 } from "node:child_process";
@@ -111191,7 +111311,7 @@ async function runHooksRun() {
111191
111311
 
111192
111312
  // tools/cli/src/index.ts
111193
111313
  var program2 = new Command();
111194
- program2.name("truecourse").version("0.5.8-windows.3").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
111314
+ program2.name("truecourse").version("0.5.9").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
111195
111315
  var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").option("--reconfigure", "Re-prompt for console vs background service mode").option("--service", "Run as a background service (skips mode prompt)").option("--console", "Run in this terminal (skips mode prompt)").action(async (options) => {
111196
111316
  if (options.service && options.console) {
111197
111317
  console.error("error: --service and --console are mutually exclusive");
@@ -111251,6 +111371,18 @@ rulesCmd.command("categories").description("View or override rule categories for
111251
111371
  rulesCmd.command("llm").description("Enable or disable LLM-powered rules for this repository").option("--enable", "Enable LLM rules").option("--disable", "Disable LLM rules").option("--reset", "Reset to global default").action(async (options) => {
111252
111372
  await runRulesLlm(options);
111253
111373
  });
111374
+ rulesCmd.command("list").description("List rules with their enabled/disabled status for this repository").option("--domain <name>", "Only show rules in this domain (e.g. security, bugs)").option("--enabled", "Only show enabled rules").option("--disabled", "Only show disabled rules").option("--search <text>", "Filter by key, name, or description").action(async (options) => {
111375
+ await runRulesList(options);
111376
+ });
111377
+ rulesCmd.command("enable <ruleKey>").description("Enable a single rule for this repository").action(async (ruleKey) => {
111378
+ await runRulesEnable({ ruleKey });
111379
+ });
111380
+ rulesCmd.command("disable <ruleKey>").description("Disable a single rule for this repository").action(async (ruleKey) => {
111381
+ await runRulesDisable({ ruleKey });
111382
+ });
111383
+ rulesCmd.command("reset [ruleKey]").description("Clear per-rule overrides (one rule, or all if no key given)").action(async (ruleKey) => {
111384
+ await runRulesReset({ ruleKey });
111385
+ });
111254
111386
  var telemetryCmd = program2.command("telemetry").description("Manage anonymous usage telemetry");
111255
111387
  telemetryCmd.command("enable").description("Enable anonymous usage telemetry").action(() => {
111256
111388
  writeTelemetryConfig({ enabled: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecourse",
3
- "version": "0.5.8-windows.3",
3
+ "version": "0.5.9",
4
4
  "description": "Visualize your codebase architecture as an interactive graph",
5
5
  "type": "module",
6
6
  "bin": {