truecourse 0.5.0 → 0.5.1

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/cli.mjs CHANGED
@@ -4062,8 +4062,11 @@ var init_registry = __esm({
4062
4062
  // tools/cli/src/commands/helpers.ts
4063
4063
  var helpers_exports = {};
4064
4064
  __export(helpers_exports, {
4065
+ exitMissingNonInteractiveFlag: () => exitMissingNonInteractiveFlag,
4065
4066
  getConfigPath: () => getConfigPath,
4066
4067
  getServerUrl: () => getServerUrl,
4068
+ hasInstalledSkills: () => hasInstalledSkills,
4069
+ isInteractive: () => isInteractive,
4067
4070
  openInBrowser: () => openInBrowser,
4068
4071
  promptInstallSkills: () => promptInstallSkills,
4069
4072
  readConfig: () => readConfig,
@@ -4316,11 +4319,24 @@ function openInBrowser(url) {
4316
4319
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
4317
4320
  exec2(`${cmd} ${url}`);
4318
4321
  }
4319
- async function promptInstallSkills(repoPath) {
4320
- const installSkills = await ot2({
4321
- message: "Would you like to install Claude Code skills?"
4322
- });
4323
- if (q(installSkills) || !installSkills) return;
4322
+ function skillDestPath(repoPath) {
4323
+ return resolve(repoPath, ".claude", "skills", "truecourse");
4324
+ }
4325
+ function hasInstalledSkills(repoPath) {
4326
+ return existsSync(skillDestPath(repoPath));
4327
+ }
4328
+ function isInteractive() {
4329
+ return !!process.stdin.isTTY;
4330
+ }
4331
+ function exitMissingNonInteractiveFlag(context, flagGuidance) {
4332
+ O2.error(
4333
+ `${context}
4334
+
4335
+ Running non-interactively with no answer. ${flagGuidance}`
4336
+ );
4337
+ process.exit(1);
4338
+ }
4339
+ function copySkillsInto(repoPath) {
4324
4340
  const __dirname4 = dirname(fileURLToPath(import.meta.url));
4325
4341
  const srcPath = resolve(__dirname4, "..", "..", "skills", "truecourse");
4326
4342
  const distPath = resolve(__dirname4, "skills", "truecourse");
@@ -4336,6 +4352,20 @@ async function promptInstallSkills(repoPath) {
4336
4352
  O2.message(" - truecourse-list (list violations)");
4337
4353
  O2.message(" - truecourse-fix (apply fixes)");
4338
4354
  }
4355
+ async function promptInstallSkills(repoPath, { install } = {}) {
4356
+ if (hasInstalledSkills(repoPath)) return;
4357
+ if (install === true) {
4358
+ copySkillsInto(repoPath);
4359
+ return;
4360
+ }
4361
+ if (install === false) return;
4362
+ if (!isInteractive()) return;
4363
+ const answer = await ot2({
4364
+ message: "Would you like to install Claude Code skills?"
4365
+ });
4366
+ if (q(answer) || !answer) return;
4367
+ copySkillsInto(repoPath);
4368
+ }
4339
4369
  var DEFAULT_PORT, DEFAULT_CONFIG;
4340
4370
  var init_helpers = __esm({
4341
4371
  "tools/cli/src/commands/helpers.ts"() {
@@ -126557,7 +126587,7 @@ init_dist4();
126557
126587
  init_paths();
126558
126588
  init_registry();
126559
126589
  init_helpers();
126560
- async function runAdd() {
126590
+ async function runAdd(options = {}) {
126561
126591
  const repoPath = resolveRepoDir(process.cwd()) ?? process.cwd();
126562
126592
  mt("Adding repository to TrueCourse");
126563
126593
  O2.step(repoPath);
@@ -126569,7 +126599,7 @@ async function runAdd() {
126569
126599
  } else {
126570
126600
  O2.success(`Repository "${entry.name}" added.`);
126571
126601
  }
126572
- await promptInstallSkills(repoPath);
126602
+ await promptInstallSkills(repoPath, { install: options.installSkills });
126573
126603
  gt("Run `truecourse analyze` to generate analysis data.");
126574
126604
  }
126575
126605
 
@@ -126598,12 +126628,20 @@ init_helpers();
126598
126628
 
126599
126629
  // tools/cli/src/commands/llm-prompt.ts
126600
126630
  init_dist4();
126601
- async function promptLlmEstimate(estimate) {
126631
+ init_helpers();
126632
+ async function promptLlmEstimate(estimate, { autoApprove } = {}) {
126602
126633
  const totalRules = estimate.uniqueRuleCount ?? estimate.tiers.reduce((s, t2) => s + t2.ruleCount, 0);
126603
126634
  const totalFiles = estimate.uniqueFileCount ?? estimate.tiers.reduce((s, t2) => s + t2.fileCount, 0);
126604
126635
  const tokens = estimate.totalEstimatedTokens;
126605
126636
  const tokenStr = tokens >= 1e6 ? `~${(tokens / 1e6).toFixed(1)}M tokens` : `~${Math.round(tokens / 1e3)}k tokens`;
126606
126637
  O2.step(`LLM will analyze ${totalFiles} files with ${totalRules} rules (${tokenStr})`);
126638
+ if (autoApprove) return true;
126639
+ if (!isInteractive()) {
126640
+ O2.error(
126641
+ "Cannot prompt for LLM-rule confirmation non-interactively. Pass --llm to approve the estimate or --no-llm to skip LLM rules."
126642
+ );
126643
+ return false;
126644
+ }
126607
126645
  const proceed = await ot2({ message: "Run LLM-powered rules?", initialValue: true });
126608
126646
  if (q(proceed)) return false;
126609
126647
  if (!proceed) O2.info("Skipping LLM rules.");
@@ -126751,18 +126789,31 @@ function stopSpinner() {
126751
126789
  spinnerInterval = null;
126752
126790
  }
126753
126791
  }
126754
- async function runAnalyze(_options = {}) {
126792
+ function resolveLlmDecision(options, configDefault) {
126793
+ if (options.llm === true) return { enabled: true, autoApproveEstimate: true };
126794
+ if (options.llm === false) return { enabled: false, autoApproveEstimate: false };
126795
+ if (!isInteractive()) {
126796
+ exitMissingNonInteractiveFlag(
126797
+ "analyze needs a decision on LLM rules before running non-interactively.",
126798
+ "Pass --llm to run with LLM rules (cost) or --no-llm to skip them."
126799
+ );
126800
+ }
126801
+ return { enabled: configDefault, autoApproveEstimate: false };
126802
+ }
126803
+ async function runAnalyze(options = {}) {
126755
126804
  mt("Analyzing repository");
126756
126805
  ensureClaudeCli();
126757
126806
  showFirstRunNotice();
126758
126807
  const project = resolveOrInitProject();
126759
126808
  O2.step(`Repository: ${project.name}`);
126809
+ await promptInstallSkills(project.path, { install: options.installSkills });
126760
126810
  configureLogger({
126761
126811
  filePath: path16.join(project.path, ".truecourse/logs/analyze.log")
126762
126812
  });
126763
126813
  const config2 = readProjectConfig(project.path);
126764
126814
  const enabledCategories = config2.enabledCategories ?? void 0;
126765
- const enableLlmRules = config2.enableLlmRules ?? true;
126815
+ const llmDecision = resolveLlmDecision(options, config2.enableLlmRules ?? true);
126816
+ const enableLlmRules = llmDecision.enabled;
126766
126817
  renderPhase = enableLlmRules ? "pre-llm" : "all";
126767
126818
  if (wipeLegacyPostgresData()) {
126768
126819
  O2.info("Legacy Postgres data wiped. Re-analyze to repopulate.");
@@ -126782,7 +126833,9 @@ async function runAnalyze(_options = {}) {
126782
126833
  enableLlmRulesOverride: enableLlmRules,
126783
126834
  onLlmEstimate: async (estimate) => {
126784
126835
  stopSpinner();
126785
- const proceed = await promptLlmEstimate(estimate);
126836
+ const proceed = await promptLlmEstimate(estimate, {
126837
+ autoApprove: llmDecision.autoApproveEstimate
126838
+ });
126786
126839
  renderPhase = "post-llm";
126787
126840
  return proceed;
126788
126841
  }
@@ -126804,7 +126857,7 @@ async function runAnalyze(_options = {}) {
126804
126857
  await closeLogger();
126805
126858
  }
126806
126859
  }
126807
- async function runAnalyzeDiff(_options = {}) {
126860
+ async function runAnalyzeDiff(options = {}) {
126808
126861
  const { diffInProcess: diffInProcess2 } = await Promise.resolve().then(() => (init_diff_in_process(), diff_in_process_exports));
126809
126862
  const { renderDiffResultsSummary: renderDiffResultsSummary2 } = await Promise.resolve().then(() => (init_helpers(), helpers_exports));
126810
126863
  mt("Running diff check");
@@ -126812,12 +126865,14 @@ async function runAnalyzeDiff(_options = {}) {
126812
126865
  showFirstRunNotice();
126813
126866
  const project = resolveOrInitProject();
126814
126867
  O2.step(`Repository: ${project.name}`);
126868
+ await promptInstallSkills(project.path, { install: options.installSkills });
126815
126869
  configureLogger({
126816
126870
  filePath: path16.join(project.path, ".truecourse/logs/analyze.log")
126817
126871
  });
126818
126872
  const config2 = readProjectConfig(project.path);
126819
126873
  const enabledCategories = config2.enabledCategories ?? void 0;
126820
- const enableLlmRules = config2.enableLlmRules ?? true;
126874
+ const llmDecision = resolveLlmDecision(options, config2.enableLlmRules ?? true);
126875
+ const enableLlmRules = llmDecision.enabled;
126821
126876
  renderPhase = enableLlmRules ? "pre-llm" : "all";
126822
126877
  const stepDefs = buildAnalysisSteps(enabledCategories, enableLlmRules);
126823
126878
  const tracker = new StepTracker((payload) => {
@@ -126834,7 +126889,9 @@ async function runAnalyzeDiff(_options = {}) {
126834
126889
  enableLlmRulesOverride: enableLlmRules,
126835
126890
  onLlmEstimate: async (estimate) => {
126836
126891
  stopSpinner();
126837
- const proceed = await promptLlmEstimate(estimate);
126892
+ const proceed = await promptLlmEstimate(estimate, {
126893
+ autoApprove: llmDecision.autoApproveEstimate
126894
+ });
126838
126895
  renderPhase = "post-llm";
126839
126896
  return proceed;
126840
126897
  }
@@ -127370,6 +127427,27 @@ async function runServiceMode(serverEntry) {
127370
127427
  O2.success(`Dashboard open at ${target}`);
127371
127428
  O2.info("Stop the dashboard with: truecourse dashboard stop");
127372
127429
  }
127430
+ async function probeHealth(url) {
127431
+ try {
127432
+ const res = await fetch(`${url}/api/health`);
127433
+ return res.ok;
127434
+ } catch {
127435
+ return false;
127436
+ }
127437
+ }
127438
+ async function detectRunningState() {
127439
+ const platform = getPlatform();
127440
+ const url = getServerUrl();
127441
+ const healthy = await probeHealth(url);
127442
+ if (await platform.isInstalled()) {
127443
+ const { running, pid } = await platform.status();
127444
+ if (running) return { mode: "service", pid, healthy };
127445
+ if (healthy) return { mode: "console", healthy: true };
127446
+ return { mode: "none" };
127447
+ }
127448
+ if (healthy) return { mode: "console", healthy: true };
127449
+ return { mode: "none" };
127450
+ }
127373
127451
  async function runDashboard(options = {}) {
127374
127452
  mt("Opening TrueCourse dashboard");
127375
127453
  const serverEntry = resolveServerEntry();
@@ -127380,9 +127458,34 @@ async function runDashboard(options = {}) {
127380
127458
  process.exit(1);
127381
127459
  }
127382
127460
  const configured = fs15.existsSync(getConfigPath());
127383
- const shouldPrompt = !configured || options.reconfigure;
127384
- const runMode = shouldPrompt ? await promptRunMode() : readConfig().runMode;
127385
- if (shouldPrompt) writeConfig({ runMode });
127461
+ const needsDecision = !configured || options.reconfigure;
127462
+ let runMode;
127463
+ if (options.mode) {
127464
+ runMode = options.mode;
127465
+ } else if (needsDecision) {
127466
+ if (!isInteractive()) {
127467
+ exitMissingNonInteractiveFlag(
127468
+ "Dashboard run mode is not configured.",
127469
+ "Pass --service for the background service or --console to run in this terminal."
127470
+ );
127471
+ }
127472
+ runMode = await promptRunMode();
127473
+ } else {
127474
+ runMode = readConfig().runMode;
127475
+ }
127476
+ const shouldPersist = needsDecision || options.mode !== void 0;
127477
+ if (runMode !== "service") {
127478
+ const platform = getPlatform();
127479
+ if (await platform.isInstalled()) {
127480
+ const { running } = await platform.status();
127481
+ if (running) {
127482
+ O2.error(
127483
+ "A dashboard service is already installed and running. Stop and remove it first: `truecourse dashboard uninstall`, then rerun `truecourse dashboard`."
127484
+ );
127485
+ process.exit(1);
127486
+ }
127487
+ }
127488
+ }
127386
127489
  if (runMode === "service") {
127387
127490
  try {
127388
127491
  await runServiceMode(serverEntry);
@@ -127390,70 +127493,68 @@ async function runDashboard(options = {}) {
127390
127493
  O2.error(`Service mode failed: ${err.message}`);
127391
127494
  O2.info("Falling back to console mode.");
127392
127495
  await runConsoleMode(serverEntry);
127496
+ if (shouldPersist) writeConfig({ runMode: "console" });
127497
+ return;
127393
127498
  }
127499
+ if (shouldPersist) writeConfig({ runMode: "service" });
127394
127500
  } else {
127395
127501
  await runConsoleMode(serverEntry);
127502
+ if (shouldPersist) writeConfig({ runMode: "console" });
127396
127503
  }
127397
127504
  }
127398
127505
  async function runDashboardStop() {
127399
- const config2 = readConfig();
127400
- if (config2.runMode !== "service") {
127401
- O2.info("Dashboard is running in console mode. Press Ctrl+C in its terminal to stop.");
127402
- return;
127403
- }
127404
- const platform = getPlatform();
127405
- const { running } = await platform.status();
127406
- if (!running) {
127407
- O2.info("Dashboard is not running.");
127408
- return;
127506
+ const state = await detectRunningState();
127507
+ switch (state.mode) {
127508
+ case "service": {
127509
+ O2.step("Stopping dashboard service...");
127510
+ await getPlatform().stop();
127511
+ O2.success("Dashboard service stopped.");
127512
+ return;
127513
+ }
127514
+ case "console": {
127515
+ O2.info(
127516
+ "A dashboard is running in console mode (not managed by the service). Press Ctrl+C in its terminal to stop it."
127517
+ );
127518
+ return;
127519
+ }
127520
+ case "none": {
127521
+ O2.info("Dashboard is not running.");
127522
+ return;
127523
+ }
127409
127524
  }
127410
- O2.step("Stopping dashboard...");
127411
- await platform.stop();
127412
- O2.success("Dashboard stopped.");
127413
127525
  }
127414
127526
  async function runDashboardStatus() {
127415
- const config2 = readConfig();
127527
+ const state = await detectRunningState();
127416
127528
  const url = getServerUrl();
127417
- if (config2.runMode !== "service") {
127418
- try {
127419
- const res = await fetch(`${url}/api/health`);
127420
- if (res.ok) {
127421
- O2.success(`Dashboard is running in console mode at ${url}`);
127529
+ switch (state.mode) {
127530
+ case "service": {
127531
+ const pidInfo = state.pid ? ` (PID: ${state.pid})` : "";
127532
+ O2.success(`Dashboard service is running${pidInfo}`);
127533
+ if (state.healthy) {
127534
+ O2.info(`Server is healthy at ${url}`);
127422
127535
  } else {
127423
- O2.info("Dashboard is not running.");
127536
+ O2.warn(`Service process is running but server is not responding at ${url}.`);
127424
127537
  }
127425
- } catch {
127426
- O2.info("Dashboard is not running.");
127538
+ return;
127427
127539
  }
127428
- return;
127429
- }
127430
- const platform = getPlatform();
127431
- const installed = await platform.isInstalled();
127432
- if (!installed) {
127433
- O2.info("Dashboard service is not installed. Run `truecourse dashboard` to set it up.");
127434
- return;
127435
- }
127436
- const { running, pid } = await platform.status();
127437
- if (!running) {
127438
- O2.info("Dashboard service is installed but not running.");
127439
- return;
127440
- }
127441
- const pidInfo = pid ? ` (PID: ${pid})` : "";
127442
- O2.success(`Dashboard service is running${pidInfo}`);
127443
- try {
127444
- const res = await fetch(`${url}/api/health`);
127445
- if (res.ok) {
127446
- O2.info(`Server is healthy at ${url}`);
127447
- } else {
127448
- O2.warn(`Server returned status ${res.status}`);
127540
+ case "console": {
127541
+ O2.success(`Dashboard is running in console mode at ${url}`);
127542
+ return;
127543
+ }
127544
+ case "none": {
127545
+ const platform = getPlatform();
127546
+ if (await platform.isInstalled()) {
127547
+ O2.info("Dashboard service is installed but not running.");
127548
+ } else {
127549
+ O2.info("Dashboard is not running.");
127550
+ }
127551
+ return;
127449
127552
  }
127450
- } catch {
127451
- O2.warn("Service process is running but server is not responding.");
127452
127553
  }
127453
127554
  }
127454
- function runDashboardLogs() {
127455
- const config2 = readConfig();
127456
- if (config2.runMode !== "service") {
127555
+ async function runDashboardLogs() {
127556
+ const state = await detectRunningState();
127557
+ if (state.mode === "console") {
127457
127558
  O2.info("Dashboard is running in console mode \u2014 logs print to its terminal.");
127458
127559
  return;
127459
127560
  }
@@ -127490,6 +127591,7 @@ var SEVERITY_ORDER = {
127490
127591
  low: 3,
127491
127592
  info: 4
127492
127593
  };
127594
+ var SEVERITIES = ["critical", "high", "medium", "low", "info"];
127493
127595
  function listViolations(repoPath, options = {}) {
127494
127596
  const latest = readLatest(repoPath);
127495
127597
  if (!latest)
@@ -127517,6 +127619,10 @@ function listViolations(repoPath, options = {}) {
127517
127619
  const absPath = options.filePath.startsWith("/") ? options.filePath : `${repoPath}/${options.filePath}`;
127518
127620
  filtered = filtered.filter((v) => v.type === "code" && (v.filePath === absPath || v.filePath === options.filePath));
127519
127621
  }
127622
+ if (options.severity !== void 0) {
127623
+ const allowed = (Array.isArray(options.severity) ? options.severity : [options.severity]).map((s) => s.toLowerCase());
127624
+ filtered = filtered.filter((v) => allowed.includes(v.severity.toLowerCase()));
127625
+ }
127520
127626
  filtered.sort((a, b) => {
127521
127627
  const sa = SEVERITY_ORDER[a.severity] ?? 5;
127522
127628
  const sb = SEVERITY_ORDER[b.severity] ?? 5;
@@ -127577,19 +127683,37 @@ function getDiffResult(repoPath) {
127577
127683
 
127578
127684
  // tools/cli/src/commands/list.ts
127579
127685
  init_helpers();
127580
- async function runList({ limit = 20, offset = 0 } = {}) {
127686
+ async function runList({ limit = 20, offset = 0, severity } = {}) {
127581
127687
  mt("Violations");
127582
127688
  const repo = requireRegisteredRepo();
127583
127689
  const { violations, total } = listViolations(repo.path, {
127584
127690
  limit: isFinite(limit) ? limit : 0,
127585
- offset
127691
+ offset,
127692
+ severity
127586
127693
  });
127587
127694
  if (total === 0 && violations.length === 0) {
127588
- O2.info("No violations. Run `truecourse analyze` if you haven't yet.");
127695
+ if (severity && severity.length > 0) {
127696
+ O2.info(`No violations match severity filter: ${severity.join(", ")}.`);
127697
+ } else {
127698
+ O2.info("No violations. Run `truecourse analyze` if you haven't yet.");
127699
+ }
127589
127700
  return;
127590
127701
  }
127591
127702
  renderViolations(violations, { total, offset });
127592
127703
  }
127704
+ function parseSeverityFlag(raw) {
127705
+ if (!raw) return void 0;
127706
+ const parts = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
127707
+ if (parts.length === 0) return void 0;
127708
+ const invalid = parts.filter((s) => !SEVERITIES.includes(s));
127709
+ if (invalid.length > 0) {
127710
+ console.error(
127711
+ `error: unknown severity value(s): ${invalid.join(", ")}. Valid: ${SEVERITIES.join(", ")}`
127712
+ );
127713
+ process.exit(1);
127714
+ }
127715
+ return parts;
127716
+ }
127593
127717
  async function runListDiff() {
127594
127718
  mt("Diff check results");
127595
127719
  const repo = requireRegisteredRepo();
@@ -130571,9 +130695,14 @@ async function runHooksRun() {
130571
130695
 
130572
130696
  // tools/cli/src/index.ts
130573
130697
  var program2 = new Command();
130574
- program2.name("truecourse").version("0.5.0").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130575
- 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").action(async (options) => {
130576
- await runDashboard({ reconfigure: options.reconfigure });
130698
+ program2.name("truecourse").version("0.5.1").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130699
+ 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) => {
130700
+ if (options.service && options.console) {
130701
+ console.error("error: --service and --console are mutually exclusive");
130702
+ process.exit(1);
130703
+ }
130704
+ const mode = options.service ? "service" : options.console ? "console" : void 0;
130705
+ await runDashboard({ reconfigure: options.reconfigure, mode });
130577
130706
  });
130578
130707
  dashboardCmd.command("stop").description("Stop the dashboard").action(async () => {
130579
130708
  await runDashboardStop();
@@ -130581,29 +130710,40 @@ dashboardCmd.command("stop").description("Stop the dashboard").action(async () =
130581
130710
  dashboardCmd.command("status").description("Show dashboard status").action(async () => {
130582
130711
  await runDashboardStatus();
130583
130712
  });
130584
- dashboardCmd.command("logs").description("Tail dashboard logs (service mode only)").action(() => {
130585
- runDashboardLogs();
130713
+ dashboardCmd.command("logs").description("Tail dashboard logs (service mode only)").action(async () => {
130714
+ await runDashboardLogs();
130586
130715
  });
130587
130716
  dashboardCmd.command("uninstall").description("Remove the background service and revert to console mode").action(async () => {
130588
130717
  await runDashboardUninstall();
130589
130718
  });
130590
- program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").action(async (options) => {
130719
+ function resolveInstallSkills(options) {
130720
+ if (options.installSkills === true) return true;
130721
+ if (options.skills === false) return false;
130722
+ return void 0;
130723
+ }
130724
+ program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").option("--llm", "Run LLM-powered rules (pre-approves the cost estimate)").option("--no-llm", "Skip LLM-powered rules for this run").option("--install-skills", "Install Claude Code skills without prompting").option("--no-skills", "Skip the Claude Code skills prompt").action(async (options) => {
130725
+ const llm = typeof options.llm === "boolean" ? options.llm : void 0;
130726
+ const installSkills = resolveInstallSkills(options);
130591
130727
  if (options.diff) {
130592
- await runAnalyzeDiff();
130728
+ await runAnalyzeDiff({ llm, installSkills });
130593
130729
  } else {
130594
- await runAnalyze();
130730
+ await runAnalyze({ llm, installSkills });
130595
130731
  }
130596
130732
  });
130597
- program2.command("add").description("Register the current directory with TrueCourse").action(async () => {
130598
- await runAdd();
130733
+ program2.command("add").description("Register the current directory with TrueCourse").option("--install-skills", "Install Claude Code skills without prompting").option("--no-skills", "Skip the Claude Code skills prompt").action(async (options) => {
130734
+ await runAdd({ installSkills: resolveInstallSkills(options) });
130599
130735
  });
130600
- program2.command("list").description("List violations from the latest analysis").option("--diff", "Show diff check results (new and resolved)").option("--limit <n>", "Number of violations to show (default: 20)", parseInt).option("--offset <n>", "Skip first N violations", parseInt).option("--all", "Show all violations").action(async (options) => {
130736
+ program2.command("list").description("List violations from the latest analysis").option("--diff", "Show diff check results (new and resolved)").option("--limit <n>", "Number of violations to show (default: 20)", parseInt).option("--offset <n>", "Skip first N violations", parseInt).option("--all", "Show all violations").option(
130737
+ "--severity <list>",
130738
+ "Comma-separated severities to include (critical,high,medium,low,info)"
130739
+ ).action(async (options) => {
130601
130740
  if (options.diff) {
130602
130741
  await runListDiff();
130603
130742
  } else {
130604
130743
  await runList({
130605
130744
  limit: options.all ? Infinity : options.limit ?? 20,
130606
- offset: options.offset ?? 0
130745
+ offset: options.offset ?? 0,
130746
+ severity: parseSeverityFlag(options.severity)
130607
130747
  });
130608
130748
  }
130609
130749
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecourse",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Visualize your codebase architecture as an interactive graph",
5
5
  "type": "module",
6
6
  "bin": {
package/server.mjs CHANGED
@@ -152216,6 +152216,10 @@ function listViolations(repoPath, options = {}) {
152216
152216
  (v) => v.type === "code" && (v.filePath === absPath || v.filePath === options.filePath)
152217
152217
  );
152218
152218
  }
152219
+ if (options.severity !== void 0) {
152220
+ const allowed = (Array.isArray(options.severity) ? options.severity : [options.severity]).map((s) => s.toLowerCase());
152221
+ filtered = filtered.filter((v) => allowed.includes(v.severity.toLowerCase()));
152222
+ }
152219
152223
  filtered.sort((a, b) => {
152220
152224
  const sa = SEVERITY_ORDER2[a.severity] ?? 5;
152221
152225
  const sb = SEVERITY_ORDER2[b.severity] ?? 5;
@@ -153596,10 +153600,15 @@ router5.get(
153596
153600
  const offsetParam = parseInt(req.query.offset) || 0;
153597
153601
  const statusParam = req.query.status;
153598
153602
  const status = statusParam === "resolved" ? "resolved" : statusParam === "all" ? "all" : "active";
153603
+ const severityParam = req.query.severity;
153604
+ const severity = severityParam ? severityParam.split(",").map((s) => s.trim().toLowerCase()).filter(
153605
+ (s) => ["critical", "high", "medium", "low", "info"].includes(s)
153606
+ ) : void 0;
153599
153607
  const { violations, total } = listViolations(repo.path, {
153600
153608
  analysisId: req.query.analysisId,
153601
153609
  filePath: req.query.file,
153602
153610
  status,
153611
+ severity: severity && severity.length > 0 ? severity : void 0,
153603
153612
  limit: limitParam,
153604
153613
  offset: offsetParam
153605
153614
  });
@@ -15,25 +15,42 @@ Run architecture analysis on the current repository using TrueCourse.
15
15
 
16
16
  ## Important
17
17
 
18
- - The TrueCourse server must be running before using this skill. If it's not running, tell the user to start it with `npx truecourse start` and try again.
19
- - Full analysis stashes any uncommitted changes, analyzes the clean working tree, then unstashes. The user's uncommitted work is preserved.
20
- - Diff check analyzes only files changed since the last analysis it does NOT stash.
18
+ - **Full analysis** stashes any uncommitted changes, analyzes the clean working tree, then unstashes. The user's uncommitted work is preserved.
19
+ - **Diff check** analyzes the full working tree (including uncommitted changes it does NOT stash) and compares the result against the last full analysis baseline. The report lists violations newly introduced and violations resolved since that baseline. Prefer diff for in-progress work where the user is iterating on changes.
20
+ - **Always invoke via `npx -y`** without `-y`, npx will hang on the "Ok to proceed?" prompt whenever the user hasn't cached the latest `truecourse` version (which happens every time we publish a new release).
21
+ - **LLM rules cost real money.** Never pass `--llm` without first relaying the cost estimate to the user and getting approval. See the LLM flow below.
21
22
 
22
23
  ## Instructions
23
24
 
24
- 1. Ask the user whether they want a **full analysis** or a **diff check** (changes only). If they mentioned "diff" in their request, default to diff mode.
25
+ ### 1. Pick mode
26
+ Ask the user whether they want a **full analysis** or a **diff check**. If they said "diff" in their request, default to diff.
25
27
 
26
- 2. Run the appropriate command using the Bash tool:
27
- - **Full analysis:** `npx truecourse analyze --no-autostart`
28
- - **Diff check:** `npx truecourse analyze --diff --no-autostart`
28
+ - Full: `npx -y truecourse analyze`
29
+ - Diff: `npx -y truecourse analyze --diff`
29
30
 
30
- 3. This is a long-running command (can take several minutes). Let it run to completion — do NOT set a short timeout. Use a timeout of at least 600000ms (10 minutes).
31
+ ### 2. Decide on LLM rules
31
32
 
32
- 4. If the command fails with "Could not connect to TrueCourse server", tell the user to run `npx truecourse start` first.
33
+ LLM rules add higher-value insights but cost money per run. Ask the user one question: **"Run LLM-powered rules this time?"** If the user is unsure, offer to run deterministic-only first (free, fast) and add LLM later.
33
34
 
34
- 5. When the command finishes, summarize the output for the user:
35
- - Number of violations found (by severity)
36
- - Number of changed files (for diff mode)
37
- - Any errors that occurred
35
+ - If **user approves LLM**: append `--llm` to the command.
36
+ - If **user declines LLM or wants a free run**: append `--no-llm`.
38
37
 
39
- 6. After summarizing, tell the user they can run `/truecourse-list` to see the full violation details, or `/truecourse-fix` to apply suggested fixes.
38
+ You MUST pass either `--llm` or `--no-llm` running without either in a non-interactive shell will exit with an error naming the flags.
39
+
40
+ ### 3. Run the command
41
+
42
+ Use the Bash tool. This is long-running (minutes, especially with `--llm`) — use a timeout of at least 600000ms (10 minutes).
43
+
44
+ ### 4. Summarize
45
+
46
+ When the command finishes, read the printed summary and relay the key numbers:
47
+
48
+ - **Full analyze**: one line with the total violation count and per-severity breakdown, e.g. `15 violations (2 critical, 5 high, 8 medium)`.
49
+ - **Diff analyze**: `Changed files: N (X modified, Y new, Z deleted)` and `Summary: N new issues, N resolved`. If you see `⚠ Results may be stale — baseline analysis has changed.`, surface that warning to the user and suggest running a full `npx -y truecourse analyze` to refresh the baseline.
50
+ - If the command errored, relay the error message.
51
+
52
+ ### 5. Next steps
53
+
54
+ Tell the user they can:
55
+ - Run `/truecourse-list` to see the full violation list.
56
+ - Run `/truecourse-fix` to apply suggested fixes.
@@ -6,29 +6,55 @@ triggers:
6
6
  - fix violations
7
7
  - apply fixes
8
8
  - fix my code
9
+ - fix diff violations
9
10
  ---
10
11
 
11
12
  # TrueCourse Fix
12
13
 
13
14
  Apply fixes for TrueCourse violations that have fix suggestions.
14
15
 
16
+ ## Important
17
+
18
+ - **Always invoke via `npx -y`** — without `-y`, npx will hang on the "Ok to proceed?" prompt whenever the user hasn't cached the latest `truecourse` version.
19
+ - **Default to the diff flow.** Users usually want to fix the violations introduced by the changes they're currently iterating on — not the whole repo. Start by asking which set to fix.
20
+ - **Only violations with a `Fix:` block can be auto-fixed.** Violations without one require human design decisions; mention them but don't attempt them.
21
+
15
22
  ## Instructions
16
23
 
17
- 1. First, fetch the current violations by running:
18
- ```
19
- npx truecourse list
20
- ```
24
+ ### 1. Pick the violation set
25
+
26
+ Ask: *"Do you want to fix violations from the latest full analysis, or just the changes you're working on right now (diff)?"*
27
+
28
+ - **Diff mode** (recommended default): `npx -y truecourse list --diff` — small set, usually fine to load in one call.
29
+ - **Full mode**: start with a paged view, `npx -y truecourse list` (first 20). If the summary line shows a large total and the user hasn't narrowed scope, ask them to narrow before pulling more pages:
30
+ - by severity — use `--severity <list>`, e.g. `npx -y truecourse list --severity critical,high`. Valid values: `critical,high,medium,low,info`.
31
+ - by a specific file / module / service they care about — no server-side filter yet, so page through and keep only matches.
32
+
33
+ Avoid `--all` on large repos — it dumps every violation into context even though most won't be fixable.
34
+
35
+ If `list --diff` returns "no diff results yet" or "stale diff", suggest the user first run `/truecourse-analyze` in diff mode.
36
+
37
+ ### 2. Identify fixable violations
38
+
39
+ From the output in hand, keep only violations that contain a `Fix:` block — those are the ones with actionable fix suggestions. If you loaded only one page in full mode, tell the user how many were fixable on this page and offer to continue to the next page (`--offset 20`, `--offset 40`, …) if they want more.
40
+
41
+ If none on the loaded page(s) are fixable, tell the user and stop (or offer to page further).
42
+
43
+ ### 3. Present and select
44
+
45
+ Show the fixable violations as a numbered list with title, severity, and target location. Ask which ones to fix (they can pick numbers or say "all").
21
46
 
22
- 2. Parse the output to identify violations that have a **Fix:** suggestion. These are the only violations that can be automatically fixed.
47
+ ### 4. Apply
23
48
 
24
- 3. If no violations have fix suggestions, tell the user and stop.
49
+ For each selected violation:
50
+ - Read the `Fix:` text.
51
+ - Use the Read tool to load the relevant source file(s).
52
+ - Use the Edit tool to apply the change.
53
+ - Briefly describe what you changed.
25
54
 
26
- 4. Present the fixable violations to the user as a numbered list with their title, severity, and target location. Ask which ones they'd like to fix (they can pick specific numbers or say "all").
55
+ ### 5. Re-verify
27
56
 
28
- 5. For each selected violation, read the fix suggestion and apply it to the codebase:
29
- - The fix suggestion describes what code change to make
30
- - Use the Read tool to read the relevant source file(s)
31
- - Use the Edit tool to apply the fix
32
- - Explain what you changed
57
+ After fixes, suggest the user re-run the appropriate analysis to confirm the violations are resolved:
33
58
 
34
- 6. After applying fixes, suggest the user run `/truecourse-analyze` again to verify the fixes resolved the violations.
59
+ - If you worked in **diff mode**: run `npx -y truecourse analyze --diff --no-llm` (fast, free). If they want LLM rules re-checked too, use `--llm` and relay the cost estimate first.
60
+ - If you worked in **full mode**: suggest `/truecourse-analyze` so the user picks the LLM/no-LLM decision fresh.
@@ -13,14 +13,29 @@ triggers:
13
13
 
14
14
  Show violations and analysis results from TrueCourse.
15
15
 
16
+ ## Important
17
+
18
+ - **Always invoke via `npx -y`** — without `-y`, npx will hang on the "Ok to proceed?" prompt whenever the user hasn't cached the latest `truecourse` version.
19
+ - **Don't dump everything at once.** Plain `list` shows the first 20 violations — use that by default. Large repos can have hundreds; pasting them all wastes context and the user can't read that much in one go. Page or filter instead.
20
+
16
21
  ## Instructions
17
22
 
18
- 1. Determine whether to show **full violations** or **diff results**. If the user mentioned "diff" in their request, use diff mode.
23
+ ### 1. Pick mode
24
+ Determine whether the user wants **full violations** (from the last full analysis) or **diff results** (changes since the last full analysis). If they said "diff" in their request, use diff mode.
25
+
26
+ - Full: `npx -y truecourse list`
27
+ - Diff: `npx -y truecourse list --diff`
28
+
29
+ ### 2. Run and present
30
+
31
+ Use the Bash tool. The output is already formatted — show it as-is.
19
32
 
20
- 2. Run the appropriate command using the Bash tool:
21
- - **Full violations:** `npx truecourse list`
22
- - **Diff results:** `npx truecourse list --diff`
33
+ The final line summarises totals, e.g. `Showing 1–20 of 287 violations (12 critical, 45 high, ...)`. Lead your reply with that total + severity breakdown so the user knows the scope before scanning the first page.
23
34
 
24
- 3. Present the output to the user. The command output is already formatted — show it as-is.
35
+ ### 3. Offer the next step
25
36
 
26
- 4. If violations with fix suggestions are found, tell the user they can run `/truecourse-fix` to apply fixes.
37
+ After showing the first page, ask the user what they want:
38
+ - **Filter by severity** — `--severity <list>`, e.g. `--severity critical,high`. Valid values: `critical,high,medium,low,info`.
39
+ - **Page further** — use `--offset <n>` (next page is `--offset 20`, then `--offset 40`, …) or widen with `--limit <n>`.
40
+ - **Everything in one view** — `--all` (only when the user explicitly asks for the full dump, e.g. "show all of them", "give me the whole list"). Avoid by default.
41
+ - **Start fixing** — `/truecourse-fix` (only for violations with a `Fix:` block attached).