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
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
127384
|
-
|
|
127385
|
-
if (
|
|
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
|
|
127400
|
-
|
|
127401
|
-
|
|
127402
|
-
|
|
127403
|
-
|
|
127404
|
-
|
|
127405
|
-
|
|
127406
|
-
|
|
127407
|
-
|
|
127408
|
-
|
|
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
|
|
127527
|
+
const state = await detectRunningState();
|
|
127416
127528
|
const url = getServerUrl();
|
|
127417
|
-
|
|
127418
|
-
|
|
127419
|
-
const
|
|
127420
|
-
|
|
127421
|
-
|
|
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.
|
|
127536
|
+
O2.warn(`Service process is running but server is not responding at ${url}.`);
|
|
127424
127537
|
}
|
|
127425
|
-
|
|
127426
|
-
O2.info("Dashboard is not running.");
|
|
127538
|
+
return;
|
|
127427
127539
|
}
|
|
127428
|
-
|
|
127429
|
-
|
|
127430
|
-
|
|
127431
|
-
|
|
127432
|
-
|
|
127433
|
-
|
|
127434
|
-
|
|
127435
|
-
|
|
127436
|
-
|
|
127437
|
-
|
|
127438
|
-
|
|
127439
|
-
|
|
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
|
|
127456
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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").
|
|
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
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
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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.
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
+
### 2. Decide on LLM rules
|
|
31
32
|
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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.
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
47
|
+
### 4. Apply
|
|
23
48
|
|
|
24
|
-
|
|
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
|
-
|
|
55
|
+
### 5. Re-verify
|
|
27
56
|
|
|
28
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
35
|
+
### 3. Offer the next step
|
|
25
36
|
|
|
26
|
-
|
|
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).
|