truecourse 0.5.0 → 0.5.2
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 +22 -3
- package/cli.mjs +357 -202
- package/package.json +5 -1
- package/server.mjs +9 -0
- package/skills/truecourse/truecourse-analyze/SKILL.md +31 -14
- package/skills/truecourse/truecourse-fix/SKILL.md +39 -13
- package/skills/truecourse/truecourse-hooks/SKILL.md +86 -0
- package/skills/truecourse/truecourse-list/SKILL.md +21 -6
package/README.md
CHANGED
|
@@ -90,12 +90,26 @@ truecourse rules llm --disable # Disable LLM rules
|
|
|
90
90
|
|
|
91
91
|
### Git Hooks
|
|
92
92
|
|
|
93
|
-
TrueCourse can install a pre-commit hook that blocks commits
|
|
93
|
+
TrueCourse can install a pre-commit hook that blocks commits introducing new violations at or above a configured severity:
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
truecourse hooks install # Install pre-commit hook
|
|
97
97
|
truecourse hooks uninstall # Remove pre-commit hook
|
|
98
|
-
truecourse hooks status # Show hook
|
|
98
|
+
truecourse hooks status # Show hook status + config
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
On every commit the hook runs `truecourse analyze --diff` against the repo's last full analysis and blocks if any newly-introduced violation matches the configured block severities. **Commits will take as long as a full diff analysis** — on large repos that can be tens of seconds per commit. `truecourse hooks install` warns you and requires confirmation before writing the hook.
|
|
102
|
+
|
|
103
|
+
**First-time setup:** run `truecourse analyze` once to establish a baseline. Without it the hook can't diff.
|
|
104
|
+
|
|
105
|
+
**Bypass:** `git commit --no-verify` (standard git).
|
|
106
|
+
|
|
107
|
+
**Configuration** — `hooks install` seeds `<repo>/.truecourse/hooks.yaml` with starter defaults; commit the file so your team shares one policy. The hook reads only from this file — if you delete it, the hook warns and passes every commit (no hidden code-level defaults). Current shape:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
pre-commit:
|
|
111
|
+
block-on: [critical, high] # severities. Valid: info|low|medium|high|critical
|
|
112
|
+
llm: false # run LLM rules on every commit (tokens per commit)
|
|
99
113
|
```
|
|
100
114
|
|
|
101
115
|
### Telemetry
|
|
@@ -131,13 +145,14 @@ TrueCourse ships with **1,200+ deterministic rules** and **100 LLM rules** acros
|
|
|
131
145
|
|
|
132
146
|
TrueCourse includes [Claude Code skills](https://docs.anthropic.com/en/docs/claude-code/skills) for conversational analysis from within Claude Code.
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
The first `truecourse analyze` (or `truecourse add`) in a fresh repo asks whether to install skills into `.claude/skills/truecourse/`. Re-runs skip the prompt if skills are already present. Pass `--install-skills` / `--no-skills` to bypass the prompt explicitly.
|
|
135
149
|
|
|
136
150
|
| Skill | What it does |
|
|
137
151
|
|---|---|
|
|
138
152
|
| `/truecourse-analyze` | Runs analysis or diff check, summarizes results |
|
|
139
153
|
| `/truecourse-list` | Shows full violation details |
|
|
140
154
|
| `/truecourse-fix` | Lists fixable violations, applies changes |
|
|
155
|
+
| `/truecourse-hooks` | Installs, configures, or removes the pre-commit hook |
|
|
141
156
|
|
|
142
157
|
## Language Support
|
|
143
158
|
|
|
@@ -172,6 +187,10 @@ pnpm build # Build all packages
|
|
|
172
187
|
|
|
173
188
|
TrueCourse collects anonymous usage data (event type, language, file count range, OS). No source code, file paths, or violation details are collected. Opt out with `truecourse telemetry disable` or `TRUECOURSE_TELEMETRY=0`.
|
|
174
189
|
|
|
190
|
+
## Contact
|
|
191
|
+
|
|
192
|
+
Questions, feedback, or security reports: **Mushegh Gevorgyan** — [mushegh@truecourse.dev](mailto:mushegh@truecourse.dev).
|
|
193
|
+
|
|
175
194
|
## License
|
|
176
195
|
|
|
177
196
|
MIT
|
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();
|
|
@@ -130310,6 +130434,11 @@ var jsYaml = {
|
|
|
130310
130434
|
};
|
|
130311
130435
|
|
|
130312
130436
|
// tools/cli/src/commands/hooks.ts
|
|
130437
|
+
init_dist4();
|
|
130438
|
+
init_paths();
|
|
130439
|
+
init_registry();
|
|
130440
|
+
init_analysis_store();
|
|
130441
|
+
init_diff_in_process();
|
|
130313
130442
|
init_helpers();
|
|
130314
130443
|
var HOOK_IDENTIFIER = "# TrueCourse pre-commit hook";
|
|
130315
130444
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
@@ -130317,29 +130446,35 @@ ${HOOK_IDENTIFIER}
|
|
|
130317
130446
|
# Installed by: truecourse hooks install
|
|
130318
130447
|
# Bypass with: git commit --no-verify
|
|
130319
130448
|
|
|
130320
|
-
exec truecourse hooks run
|
|
130449
|
+
exec npx -y truecourse hooks run
|
|
130450
|
+
`;
|
|
130451
|
+
var SEVERITIES2 = ["info", "low", "medium", "high", "critical"];
|
|
130452
|
+
var HOOKS_YAML_TEMPLATE = `# TrueCourse pre-commit hook config.
|
|
130453
|
+
# Commit this file \u2014 it's the team-shared policy for what blocks a commit.
|
|
130454
|
+
# Check the live values with \`truecourse hooks status\`.
|
|
130455
|
+
pre-commit:
|
|
130456
|
+
# Severities that block a commit when the diff surfaces NEW violations
|
|
130457
|
+
# at that level. Valid: info, low, medium, high, critical.
|
|
130458
|
+
block-on:
|
|
130459
|
+
- critical
|
|
130460
|
+
- high
|
|
130461
|
+
|
|
130462
|
+
# Run LLM-powered rules on every commit? Off by default (no tokens per
|
|
130463
|
+
# commit). Set to true for deeper semantic checks at the commit gate \u2014
|
|
130464
|
+
# each commit will then cost tokens.
|
|
130465
|
+
llm: false
|
|
130321
130466
|
`;
|
|
130322
|
-
var DEFAULT_BLOCK_ON = [
|
|
130323
|
-
"security/deterministic/hardcoded-secret",
|
|
130324
|
-
{ severity: "critical" }
|
|
130325
|
-
];
|
|
130326
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
130327
130467
|
function findGitDir(from) {
|
|
130328
130468
|
let dir = from;
|
|
130329
130469
|
while (true) {
|
|
130330
130470
|
const gitPath = path21.join(dir, ".git");
|
|
130331
130471
|
if (fs16.existsSync(gitPath)) {
|
|
130332
130472
|
const stat = fs16.statSync(gitPath);
|
|
130333
|
-
if (stat.isDirectory())
|
|
130334
|
-
return gitPath;
|
|
130335
|
-
}
|
|
130473
|
+
if (stat.isDirectory()) return gitPath;
|
|
130336
130474
|
if (stat.isFile()) {
|
|
130337
130475
|
const content = fs16.readFileSync(gitPath, "utf-8").trim();
|
|
130338
130476
|
const match2 = content.match(/^gitdir:\s*(.+)$/);
|
|
130339
|
-
if (match2)
|
|
130340
|
-
const resolved = path21.resolve(dir, match2[1]);
|
|
130341
|
-
return resolved;
|
|
130342
|
-
}
|
|
130477
|
+
if (match2) return path21.resolve(dir, match2[1]);
|
|
130343
130478
|
}
|
|
130344
130479
|
}
|
|
130345
130480
|
const parent = path21.dirname(dir);
|
|
@@ -130350,46 +130485,68 @@ function findGitDir(from) {
|
|
|
130350
130485
|
function findProjectRoot(from) {
|
|
130351
130486
|
let dir = from;
|
|
130352
130487
|
while (true) {
|
|
130353
|
-
if (fs16.existsSync(path21.join(dir, ".git")))
|
|
130354
|
-
return dir;
|
|
130355
|
-
}
|
|
130488
|
+
if (fs16.existsSync(path21.join(dir, ".git"))) return dir;
|
|
130356
130489
|
const parent = path21.dirname(dir);
|
|
130357
130490
|
if (parent === dir) return null;
|
|
130358
130491
|
dir = parent;
|
|
130359
130492
|
}
|
|
130360
130493
|
}
|
|
130361
|
-
function parseTimeout(value) {
|
|
130362
|
-
const match2 = value.match(/^(\d+)\s*(s|ms|m)?$/);
|
|
130363
|
-
if (!match2) return DEFAULT_TIMEOUT_MS;
|
|
130364
|
-
const num = parseInt(match2[1], 10);
|
|
130365
|
-
const unit = match2[2] || "s";
|
|
130366
|
-
if (unit === "ms") return num;
|
|
130367
|
-
if (unit === "m") return num * 6e4;
|
|
130368
|
-
return num * 1e3;
|
|
130369
|
-
}
|
|
130370
130494
|
function loadConfig(projectRoot) {
|
|
130371
130495
|
const configPath = path21.join(projectRoot, ".truecourse", "hooks.yaml");
|
|
130372
|
-
if (!fs16.existsSync(configPath)) return
|
|
130496
|
+
if (!fs16.existsSync(configPath)) return null;
|
|
130497
|
+
let parsed;
|
|
130373
130498
|
try {
|
|
130374
130499
|
const raw = fs16.readFileSync(configPath, "utf-8");
|
|
130375
|
-
|
|
130376
|
-
} catch {
|
|
130377
|
-
|
|
130500
|
+
parsed = jsYaml.load(raw) || {};
|
|
130501
|
+
} catch (err) {
|
|
130502
|
+
console.error(`Error parsing ${configPath}: ${err.message}`);
|
|
130503
|
+
process.exit(1);
|
|
130378
130504
|
}
|
|
130505
|
+
const preCommit = parsed["pre-commit"] ?? {};
|
|
130506
|
+
const rawBlockOn = preCommit["block-on"];
|
|
130507
|
+
if (!Array.isArray(rawBlockOn)) {
|
|
130508
|
+
console.error(
|
|
130509
|
+
`Invalid ${configPath}: \`pre-commit.block-on\` must be an array of severity names.`
|
|
130510
|
+
);
|
|
130511
|
+
console.error(` Valid severities: ${SEVERITIES2.join(", ")}`);
|
|
130512
|
+
process.exit(1);
|
|
130513
|
+
}
|
|
130514
|
+
const invalid = rawBlockOn.filter(
|
|
130515
|
+
(s) => typeof s !== "string" || !SEVERITIES2.includes(s)
|
|
130516
|
+
);
|
|
130517
|
+
if (invalid.length > 0) {
|
|
130518
|
+
console.error(
|
|
130519
|
+
`Invalid ${configPath}: unknown value(s) in \`pre-commit.block-on\`: ${invalid.map((v) => JSON.stringify(v)).join(", ")}`
|
|
130520
|
+
);
|
|
130521
|
+
console.error(` Valid severities: ${SEVERITIES2.join(", ")}`);
|
|
130522
|
+
process.exit(1);
|
|
130523
|
+
}
|
|
130524
|
+
return {
|
|
130525
|
+
blockOn: rawBlockOn,
|
|
130526
|
+
llm: preCommit.llm === true,
|
|
130527
|
+
configPath
|
|
130528
|
+
};
|
|
130379
130529
|
}
|
|
130380
|
-
|
|
130381
|
-
|
|
130382
|
-
}
|
|
130383
|
-
function getTimeoutMs(config2) {
|
|
130384
|
-
const raw = config2["pre-commit"]?.timeout;
|
|
130385
|
-
return raw ? parseTimeout(raw) : DEFAULT_TIMEOUT_MS;
|
|
130386
|
-
}
|
|
130387
|
-
function runHooksInstall() {
|
|
130530
|
+
var INSTALL_WARNING = "The pre-commit hook runs `truecourse analyze --diff` on every commit.\nCommits will take as long as a full diff analysis of this repo \u2014\non large repos that can be tens of seconds per commit.";
|
|
130531
|
+
async function runHooksInstall() {
|
|
130388
130532
|
const gitDir = findGitDir(process.cwd());
|
|
130389
130533
|
if (!gitDir) {
|
|
130390
130534
|
console.error("Error: Not a git repository.");
|
|
130391
130535
|
process.exit(1);
|
|
130392
130536
|
}
|
|
130537
|
+
if (isInteractive()) {
|
|
130538
|
+
O2.warn(INSTALL_WARNING);
|
|
130539
|
+
const proceed = await ot2({
|
|
130540
|
+
message: "Install the pre-commit hook?",
|
|
130541
|
+
initialValue: false
|
|
130542
|
+
});
|
|
130543
|
+
if (q(proceed) || !proceed) {
|
|
130544
|
+
pt("Install cancelled.");
|
|
130545
|
+
process.exit(0);
|
|
130546
|
+
}
|
|
130547
|
+
} else {
|
|
130548
|
+
console.log(INSTALL_WARNING);
|
|
130549
|
+
}
|
|
130393
130550
|
const hooksDir = path21.join(gitDir, "hooks");
|
|
130394
130551
|
fs16.mkdirSync(hooksDir, { recursive: true });
|
|
130395
130552
|
const hookPath = path21.join(hooksDir, "pre-commit");
|
|
@@ -130407,6 +130564,16 @@ function runHooksInstall() {
|
|
|
130407
130564
|
fs16.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 493 });
|
|
130408
130565
|
console.log("TrueCourse pre-commit hook installed.");
|
|
130409
130566
|
console.log(` ${hookPath}`);
|
|
130567
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
130568
|
+
if (projectRoot) {
|
|
130569
|
+
const cfgDir = path21.join(projectRoot, ".truecourse");
|
|
130570
|
+
const cfgPath = path21.join(cfgDir, "hooks.yaml");
|
|
130571
|
+
if (!fs16.existsSync(cfgPath)) {
|
|
130572
|
+
fs16.mkdirSync(cfgDir, { recursive: true });
|
|
130573
|
+
fs16.writeFileSync(cfgPath, HOOKS_YAML_TEMPLATE);
|
|
130574
|
+
console.log(` ${cfgPath} (starter config \u2014 edit to customize, commit to share with the team)`);
|
|
130575
|
+
}
|
|
130576
|
+
}
|
|
130410
130577
|
}
|
|
130411
130578
|
function runHooksUninstall() {
|
|
130412
130579
|
const gitDir = findGitDir(process.cwd());
|
|
@@ -130444,125 +130611,97 @@ function runHooksStatus() {
|
|
|
130444
130611
|
}
|
|
130445
130612
|
const projectRoot = findProjectRoot(process.cwd());
|
|
130446
130613
|
if (projectRoot) {
|
|
130447
|
-
const
|
|
130448
|
-
if (
|
|
130449
|
-
console.log(
|
|
130450
|
-
|
|
130451
|
-
|
|
130452
|
-
const blockRules = getBlockRules(config2);
|
|
130453
|
-
console.log("Block on:");
|
|
130454
|
-
for (const rule of blockRules) {
|
|
130455
|
-
if (typeof rule === "string") {
|
|
130456
|
-
console.log(` - ${rule}`);
|
|
130457
|
-
} else {
|
|
130458
|
-
console.log(` - severity: ${rule.severity}`);
|
|
130459
|
-
}
|
|
130460
|
-
}
|
|
130461
|
-
const timeoutMs = getTimeoutMs(config2);
|
|
130462
|
-
console.log(`Timeout: ${timeoutMs / 1e3}s`);
|
|
130614
|
+
const cfg = loadConfig(projectRoot);
|
|
130615
|
+
if (!cfg) {
|
|
130616
|
+
console.log(
|
|
130617
|
+
"\nNo `.truecourse/hooks.yaml` \u2014 hook has no policy. Run `truecourse hooks install` to generate one."
|
|
130618
|
+
);
|
|
130463
130619
|
} else {
|
|
130464
|
-
console.log(
|
|
130465
|
-
|
|
130466
|
-
|
|
130467
|
-
}
|
|
130468
|
-
function shouldBlock(violation, blockRules) {
|
|
130469
|
-
for (const rule of blockRules) {
|
|
130470
|
-
if (typeof rule === "string") {
|
|
130471
|
-
if (violation.ruleKey === rule || violation.ruleKey.endsWith(`/${rule}`)) {
|
|
130472
|
-
return true;
|
|
130473
|
-
}
|
|
130474
|
-
} else if (rule.severity) {
|
|
130475
|
-
if (violation.severity.toLowerCase() === rule.severity.toLowerCase()) {
|
|
130476
|
-
return true;
|
|
130477
|
-
}
|
|
130620
|
+
console.log(`
|
|
130621
|
+
Config: ${cfg.configPath}`);
|
|
130622
|
+
console.log(`Block on severities: ${cfg.blockOn.join(", ")}`);
|
|
130623
|
+
console.log(`LLM rules on commit: ${cfg.llm ? "enabled (tokens per commit)" : "disabled"}`);
|
|
130478
130624
|
}
|
|
130479
130625
|
}
|
|
130480
|
-
return false;
|
|
130481
130626
|
}
|
|
130482
130627
|
async function runHooksRun() {
|
|
130483
|
-
const startTime = Date.now();
|
|
130484
130628
|
process.stdout.write("TrueCourse pre-commit check...");
|
|
130485
130629
|
const projectRoot = findProjectRoot(process.cwd());
|
|
130486
130630
|
if (!projectRoot) {
|
|
130487
130631
|
console.log(" skipped (not a git repository)");
|
|
130488
130632
|
process.exit(0);
|
|
130489
130633
|
}
|
|
130490
|
-
|
|
130491
|
-
const blockRules = getBlockRules(config2);
|
|
130492
|
-
const timeoutMs = getTimeoutMs(config2);
|
|
130493
|
-
let stagedFiles;
|
|
130634
|
+
let hasStaged = false;
|
|
130494
130635
|
try {
|
|
130495
130636
|
const output = execSync5("git diff --cached --name-only --diff-filter=ACM", {
|
|
130496
130637
|
encoding: "utf-8",
|
|
130497
130638
|
cwd: projectRoot
|
|
130498
130639
|
}).trim();
|
|
130499
|
-
|
|
130640
|
+
hasStaged = output.length > 0;
|
|
130500
130641
|
} catch {
|
|
130501
130642
|
console.log(" skipped (git error)");
|
|
130502
130643
|
process.exit(0);
|
|
130503
130644
|
}
|
|
130504
|
-
if (
|
|
130645
|
+
if (!hasStaged) {
|
|
130505
130646
|
console.log(" \u2714 passed (no staged files)");
|
|
130506
130647
|
process.exit(0);
|
|
130507
130648
|
}
|
|
130508
|
-
|
|
130509
|
-
|
|
130510
|
-
|
|
130511
|
-
|
|
130512
|
-
|
|
130513
|
-
|
|
130514
|
-
parseCode2 = analyzer.parseCode;
|
|
130515
|
-
detectLanguage2 = analyzer.detectLanguage;
|
|
130516
|
-
checkCodeRules2 = analyzer.checkCodeRules;
|
|
130517
|
-
CODE_RULES2 = analyzer.CODE_RULES;
|
|
130518
|
-
await analyzer.initParsers();
|
|
130519
|
-
} catch {
|
|
130520
|
-
console.log(" skipped (analyzer not available)");
|
|
130649
|
+
const cfg = loadConfig(projectRoot);
|
|
130650
|
+
if (!cfg) {
|
|
130651
|
+
console.log(" skipped");
|
|
130652
|
+
console.error(
|
|
130653
|
+
"No `.truecourse/hooks.yaml` found. The pre-commit hook has no policy to\nenforce \u2014 run `truecourse hooks install` to generate one."
|
|
130654
|
+
);
|
|
130521
130655
|
process.exit(0);
|
|
130522
130656
|
}
|
|
130523
|
-
const
|
|
130524
|
-
|
|
130525
|
-
|
|
130526
|
-
|
|
130657
|
+
const repoDir = resolveRepoDir(process.cwd());
|
|
130658
|
+
const project = repoDir ? getProjectByPath(repoDir) ?? registerProject(repoDir) : null;
|
|
130659
|
+
if (!project || !readLatest(project.path)) {
|
|
130660
|
+
console.log("");
|
|
130661
|
+
console.error(
|
|
130662
|
+
"No baseline analysis yet. Run `truecourse analyze` once in this repo before\nthe pre-commit hook can block new violations. Or bypass this commit with\n`git commit --no-verify`."
|
|
130663
|
+
);
|
|
130664
|
+
process.exit(1);
|
|
130527
130665
|
}
|
|
130528
|
-
const
|
|
130529
|
-
|
|
130530
|
-
|
|
130531
|
-
|
|
130532
|
-
|
|
130533
|
-
|
|
130534
|
-
const
|
|
130535
|
-
|
|
130536
|
-
|
|
130537
|
-
|
|
130538
|
-
|
|
130539
|
-
|
|
130540
|
-
|
|
130541
|
-
|
|
130542
|
-
|
|
130543
|
-
|
|
130544
|
-
|
|
130545
|
-
|
|
130546
|
-
|
|
130547
|
-
try {
|
|
130548
|
-
const tree = parseCode2(content, language);
|
|
130549
|
-
const violations = checkCodeRules2(tree, filePath, content, CODE_RULES2, language);
|
|
130550
|
-
allViolations.push(...violations);
|
|
130551
|
-
} catch {
|
|
130552
|
-
continue;
|
|
130666
|
+
const abortController = new AbortController();
|
|
130667
|
+
const onSigint = () => abortController.abort();
|
|
130668
|
+
process.on("SIGINT", onSigint);
|
|
130669
|
+
process.stdout.write(" running analysis...");
|
|
130670
|
+
let newViolations;
|
|
130671
|
+
try {
|
|
130672
|
+
const { diff } = await diffInProcess(project, {
|
|
130673
|
+
signal: abortController.signal,
|
|
130674
|
+
enableLlmRulesOverride: cfg.llm,
|
|
130675
|
+
// Pre-approved: the user opted into LLM-in-hook by setting llm: true
|
|
130676
|
+
// in hooks.yaml, so we don't re-prompt for the token cost estimate.
|
|
130677
|
+
onLlmEstimate: async () => true
|
|
130678
|
+
});
|
|
130679
|
+
newViolations = diff.newViolations;
|
|
130680
|
+
} catch (err) {
|
|
130681
|
+
console.log("");
|
|
130682
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
130683
|
+
console.error("Pre-commit check cancelled.");
|
|
130684
|
+
process.exit(130);
|
|
130553
130685
|
}
|
|
130686
|
+
console.error(`Pre-commit check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
130687
|
+
process.exit(1);
|
|
130688
|
+
} finally {
|
|
130689
|
+
process.removeListener("SIGINT", onSigint);
|
|
130554
130690
|
}
|
|
130555
|
-
const
|
|
130691
|
+
const blockSet = new Set(cfg.blockOn);
|
|
130692
|
+
const blocking = newViolations.filter((v) => blockSet.has(v.severity.toLowerCase()));
|
|
130556
130693
|
if (blocking.length === 0) {
|
|
130557
|
-
console.log(
|
|
130694
|
+
console.log(` \u2714 passed (${newViolations.length} new violations, none at or above ${cfg.blockOn.join("/")})`);
|
|
130558
130695
|
process.exit(0);
|
|
130559
130696
|
}
|
|
130560
130697
|
console.log("\n");
|
|
130561
130698
|
for (const v of blocking) {
|
|
130562
130699
|
const icon = severityIcon(v.severity);
|
|
130563
130700
|
const color = severityColor(v.severity);
|
|
130701
|
+
const location = v.filePath ? `${v.filePath}${v.lineStart ? `:${v.lineStart}` : ""}` : "(no file)";
|
|
130564
130702
|
console.log(` ${color(`${icon} BLOCKED`)}: ${v.title}`);
|
|
130565
|
-
console.log(` ${
|
|
130703
|
+
console.log(` ${location} \u2014 ${v.content}`);
|
|
130704
|
+
if (v.fixPrompt) console.log(` Fix: ${v.fixPrompt}`);
|
|
130566
130705
|
console.log("");
|
|
130567
130706
|
}
|
|
130568
130707
|
console.log("Commit blocked. Fix the issue or bypass with --no-verify.");
|
|
@@ -130571,9 +130710,14 @@ async function runHooksRun() {
|
|
|
130571
130710
|
|
|
130572
130711
|
// tools/cli/src/index.ts
|
|
130573
130712
|
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
|
-
|
|
130713
|
+
program2.name("truecourse").version("0.5.2").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
|
|
130714
|
+
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) => {
|
|
130715
|
+
if (options.service && options.console) {
|
|
130716
|
+
console.error("error: --service and --console are mutually exclusive");
|
|
130717
|
+
process.exit(1);
|
|
130718
|
+
}
|
|
130719
|
+
const mode = options.service ? "service" : options.console ? "console" : void 0;
|
|
130720
|
+
await runDashboard({ reconfigure: options.reconfigure, mode });
|
|
130577
130721
|
});
|
|
130578
130722
|
dashboardCmd.command("stop").description("Stop the dashboard").action(async () => {
|
|
130579
130723
|
await runDashboardStop();
|
|
@@ -130581,29 +130725,40 @@ dashboardCmd.command("stop").description("Stop the dashboard").action(async () =
|
|
|
130581
130725
|
dashboardCmd.command("status").description("Show dashboard status").action(async () => {
|
|
130582
130726
|
await runDashboardStatus();
|
|
130583
130727
|
});
|
|
130584
|
-
dashboardCmd.command("logs").description("Tail dashboard logs (service mode only)").action(() => {
|
|
130585
|
-
runDashboardLogs();
|
|
130728
|
+
dashboardCmd.command("logs").description("Tail dashboard logs (service mode only)").action(async () => {
|
|
130729
|
+
await runDashboardLogs();
|
|
130586
130730
|
});
|
|
130587
130731
|
dashboardCmd.command("uninstall").description("Remove the background service and revert to console mode").action(async () => {
|
|
130588
130732
|
await runDashboardUninstall();
|
|
130589
130733
|
});
|
|
130590
|
-
|
|
130734
|
+
function resolveInstallSkills(options) {
|
|
130735
|
+
if (options.installSkills === true) return true;
|
|
130736
|
+
if (options.skills === false) return false;
|
|
130737
|
+
return void 0;
|
|
130738
|
+
}
|
|
130739
|
+
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) => {
|
|
130740
|
+
const llm = typeof options.llm === "boolean" ? options.llm : void 0;
|
|
130741
|
+
const installSkills = resolveInstallSkills(options);
|
|
130591
130742
|
if (options.diff) {
|
|
130592
|
-
await runAnalyzeDiff();
|
|
130743
|
+
await runAnalyzeDiff({ llm, installSkills });
|
|
130593
130744
|
} else {
|
|
130594
|
-
await runAnalyze();
|
|
130745
|
+
await runAnalyze({ llm, installSkills });
|
|
130595
130746
|
}
|
|
130596
130747
|
});
|
|
130597
|
-
program2.command("add").description("Register the current directory with TrueCourse").action(async () => {
|
|
130598
|
-
await runAdd();
|
|
130748
|
+
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) => {
|
|
130749
|
+
await runAdd({ installSkills: resolveInstallSkills(options) });
|
|
130599
130750
|
});
|
|
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").
|
|
130751
|
+
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(
|
|
130752
|
+
"--severity <list>",
|
|
130753
|
+
"Comma-separated severities to include (critical,high,medium,low,info)"
|
|
130754
|
+
).action(async (options) => {
|
|
130601
130755
|
if (options.diff) {
|
|
130602
130756
|
await runListDiff();
|
|
130603
130757
|
} else {
|
|
130604
130758
|
await runList({
|
|
130605
130759
|
limit: options.all ? Infinity : options.limit ?? 20,
|
|
130606
|
-
offset: options.offset ?? 0
|
|
130760
|
+
offset: options.offset ?? 0,
|
|
130761
|
+
severity: parseSeverityFlag(options.severity)
|
|
130607
130762
|
});
|
|
130608
130763
|
}
|
|
130609
130764
|
});
|
|
@@ -130634,8 +130789,8 @@ telemetryCmd.command("status").description("Show current telemetry status").acti
|
|
|
130634
130789
|
}
|
|
130635
130790
|
});
|
|
130636
130791
|
var hooksCmd = program2.command("hooks").description("Manage git hooks");
|
|
130637
|
-
hooksCmd.command("install").description("Install pre-commit hook").action(() => {
|
|
130638
|
-
runHooksInstall();
|
|
130792
|
+
hooksCmd.command("install").description("Install pre-commit hook").action(async () => {
|
|
130793
|
+
await runHooksInstall();
|
|
130639
130794
|
});
|
|
130640
130795
|
hooksCmd.command("uninstall").description("Remove pre-commit hook").action(() => {
|
|
130641
130796
|
runHooksUninstall();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "truecourse",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Visualize your codebase architecture as an interactive graph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"node-windows": "^1.0.0-beta.8"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "Mushegh Gevorgyan",
|
|
26
|
+
"email": "mushegh@truecourse.dev"
|
|
27
|
+
},
|
|
24
28
|
"repository": {
|
|
25
29
|
"type": "git",
|
|
26
30
|
"url": "https://github.com/truecourse-ai/truecourse"
|
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 tokens.** Never pass `--llm` without first relaying the token 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 tokens per run. Ask the user one question: **"Run LLM-powered rules this time?"** If the user is unsure, offer to run deterministic-only first (no tokens, 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.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: truecourse-hooks
|
|
3
|
+
description: Install, configure, or remove the TrueCourse pre-commit hook
|
|
4
|
+
user_invocable: true
|
|
5
|
+
triggers:
|
|
6
|
+
- install the pre-commit hook
|
|
7
|
+
- set up truecourse hooks
|
|
8
|
+
- enable truecourse hook
|
|
9
|
+
- check hook status
|
|
10
|
+
- remove pre-commit hook
|
|
11
|
+
- change what the hook blocks
|
|
12
|
+
- edit hook config
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# TrueCourse Hooks
|
|
16
|
+
|
|
17
|
+
Install, configure, and manage the pre-commit hook that blocks new violations before they land in git.
|
|
18
|
+
|
|
19
|
+
## Important
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
- **The hook makes commits slower.** Every commit runs `truecourse analyze --diff`. On large repos that can be tens of seconds per commit. Make sure the user knows before you install.
|
|
23
|
+
- **Baseline required.** The hook needs a `truecourse analyze` to have run at least once in the repo — otherwise every commit is blocked with "run analyze first". If the user hasn't, suggest running `/truecourse-analyze` first (or `npx -y truecourse analyze`).
|
|
24
|
+
- **`hooks.yaml` is the single source of truth.** Installation creates `<repo>/.truecourse/hooks.yaml` with defaults; edit it to change policy. The file is meant to be committed so the whole team shares one hook config.
|
|
25
|
+
|
|
26
|
+
## Instructions
|
|
27
|
+
|
|
28
|
+
### 1. Figure out what the user wants
|
|
29
|
+
|
|
30
|
+
- "install", "set up", "enable" → **Install flow**
|
|
31
|
+
- "status", "is the hook active", "what does it block" → **Status flow**
|
|
32
|
+
- "uninstall", "remove", "disable" → **Uninstall flow**
|
|
33
|
+
- "change what blocks", "make it stricter/looser", "add/remove severities", "enable LLM" → **Configure flow**
|
|
34
|
+
|
|
35
|
+
### 2. Install flow
|
|
36
|
+
|
|
37
|
+
1. Tell the user the tradeoff upfront: commits will be slower; this repo needs a `truecourse analyze` baseline; policy lives in `.truecourse/hooks.yaml` which they should commit.
|
|
38
|
+
2. Run:
|
|
39
|
+
```
|
|
40
|
+
npx -y truecourse hooks install
|
|
41
|
+
```
|
|
42
|
+
3. Relay the output. Two files get created:
|
|
43
|
+
- `.git/hooks/pre-commit` (the script git invokes)
|
|
44
|
+
- `.truecourse/hooks.yaml` (starter policy, blocks `critical` and `high` by default, LLM off)
|
|
45
|
+
4. If the user hasn't run a full analysis in this repo, suggest `/truecourse-analyze` — without it, every commit will be blocked with "no baseline" until they do.
|
|
46
|
+
|
|
47
|
+
### 3. Status flow
|
|
48
|
+
|
|
49
|
+
Run:
|
|
50
|
+
```
|
|
51
|
+
npx -y truecourse hooks status
|
|
52
|
+
```
|
|
53
|
+
Relay the output. It reports whether the hook is installed, the config path, the block severities, and whether LLM is on.
|
|
54
|
+
|
|
55
|
+
### 4. Uninstall flow
|
|
56
|
+
|
|
57
|
+
Run:
|
|
58
|
+
```
|
|
59
|
+
npx -y truecourse hooks uninstall
|
|
60
|
+
```
|
|
61
|
+
Only removes the git hook script. `hooks.yaml` is preserved (it's team policy, not install state).
|
|
62
|
+
|
|
63
|
+
### 5. Configure flow
|
|
64
|
+
|
|
65
|
+
The config lives at `<repo>/.truecourse/hooks.yaml`. Use the Read and Edit tools — do not shell out through `truecourse` for edits.
|
|
66
|
+
|
|
67
|
+
Schema:
|
|
68
|
+
```yaml
|
|
69
|
+
pre-commit:
|
|
70
|
+
block-on: [critical, high] # valid: info, low, medium, high, critical
|
|
71
|
+
llm: false # true = LLM rules on every commit (tokens per commit)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Common edits the user might ask for:
|
|
75
|
+
- **Stricter** ("block medium too"): `block-on: [critical, high, medium]`
|
|
76
|
+
- **Permissive** ("only block criticals"): `block-on: [critical]`
|
|
77
|
+
- **Enable LLM** ("run full checks on commit"): set `llm: true`. Warn the user this spends tokens on every commit — confirm before flipping it.
|
|
78
|
+
|
|
79
|
+
After editing, run `npx -y truecourse hooks status` so they can verify the parsed values match their intent.
|
|
80
|
+
|
|
81
|
+
### 6. When the user hits a blocked commit
|
|
82
|
+
|
|
83
|
+
If a user comes to you saying "my commit got blocked" or similar:
|
|
84
|
+
- The hook's stdout already listed the blocking violations (file, line, title, severity).
|
|
85
|
+
- Offer to run `/truecourse-fix` to apply fix suggestions to those violations.
|
|
86
|
+
- If they want to ship anyway, remind them of `git commit --no-verify` (standard git bypass).
|
|
@@ -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).
|