truecourse 0.5.1 → 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 +138 -123
- package/package.json +5 -1
- package/skills/truecourse/truecourse-analyze/SKILL.md +2 -2
- package/skills/truecourse/truecourse-hooks/SKILL.md +86 -0
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
|
@@ -130434,6 +130434,11 @@ var jsYaml = {
|
|
|
130434
130434
|
};
|
|
130435
130435
|
|
|
130436
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();
|
|
130437
130442
|
init_helpers();
|
|
130438
130443
|
var HOOK_IDENTIFIER = "# TrueCourse pre-commit hook";
|
|
130439
130444
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
@@ -130441,29 +130446,35 @@ ${HOOK_IDENTIFIER}
|
|
|
130441
130446
|
# Installed by: truecourse hooks install
|
|
130442
130447
|
# Bypass with: git commit --no-verify
|
|
130443
130448
|
|
|
130444
|
-
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
|
|
130445
130466
|
`;
|
|
130446
|
-
var DEFAULT_BLOCK_ON = [
|
|
130447
|
-
"security/deterministic/hardcoded-secret",
|
|
130448
|
-
{ severity: "critical" }
|
|
130449
|
-
];
|
|
130450
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
130451
130467
|
function findGitDir(from) {
|
|
130452
130468
|
let dir = from;
|
|
130453
130469
|
while (true) {
|
|
130454
130470
|
const gitPath = path21.join(dir, ".git");
|
|
130455
130471
|
if (fs16.existsSync(gitPath)) {
|
|
130456
130472
|
const stat = fs16.statSync(gitPath);
|
|
130457
|
-
if (stat.isDirectory())
|
|
130458
|
-
return gitPath;
|
|
130459
|
-
}
|
|
130473
|
+
if (stat.isDirectory()) return gitPath;
|
|
130460
130474
|
if (stat.isFile()) {
|
|
130461
130475
|
const content = fs16.readFileSync(gitPath, "utf-8").trim();
|
|
130462
130476
|
const match2 = content.match(/^gitdir:\s*(.+)$/);
|
|
130463
|
-
if (match2)
|
|
130464
|
-
const resolved = path21.resolve(dir, match2[1]);
|
|
130465
|
-
return resolved;
|
|
130466
|
-
}
|
|
130477
|
+
if (match2) return path21.resolve(dir, match2[1]);
|
|
130467
130478
|
}
|
|
130468
130479
|
}
|
|
130469
130480
|
const parent = path21.dirname(dir);
|
|
@@ -130474,46 +130485,68 @@ function findGitDir(from) {
|
|
|
130474
130485
|
function findProjectRoot(from) {
|
|
130475
130486
|
let dir = from;
|
|
130476
130487
|
while (true) {
|
|
130477
|
-
if (fs16.existsSync(path21.join(dir, ".git")))
|
|
130478
|
-
return dir;
|
|
130479
|
-
}
|
|
130488
|
+
if (fs16.existsSync(path21.join(dir, ".git"))) return dir;
|
|
130480
130489
|
const parent = path21.dirname(dir);
|
|
130481
130490
|
if (parent === dir) return null;
|
|
130482
130491
|
dir = parent;
|
|
130483
130492
|
}
|
|
130484
130493
|
}
|
|
130485
|
-
function parseTimeout(value) {
|
|
130486
|
-
const match2 = value.match(/^(\d+)\s*(s|ms|m)?$/);
|
|
130487
|
-
if (!match2) return DEFAULT_TIMEOUT_MS;
|
|
130488
|
-
const num = parseInt(match2[1], 10);
|
|
130489
|
-
const unit = match2[2] || "s";
|
|
130490
|
-
if (unit === "ms") return num;
|
|
130491
|
-
if (unit === "m") return num * 6e4;
|
|
130492
|
-
return num * 1e3;
|
|
130493
|
-
}
|
|
130494
130494
|
function loadConfig(projectRoot) {
|
|
130495
130495
|
const configPath = path21.join(projectRoot, ".truecourse", "hooks.yaml");
|
|
130496
|
-
if (!fs16.existsSync(configPath)) return
|
|
130496
|
+
if (!fs16.existsSync(configPath)) return null;
|
|
130497
|
+
let parsed;
|
|
130497
130498
|
try {
|
|
130498
130499
|
const raw = fs16.readFileSync(configPath, "utf-8");
|
|
130499
|
-
|
|
130500
|
-
} catch {
|
|
130501
|
-
|
|
130500
|
+
parsed = jsYaml.load(raw) || {};
|
|
130501
|
+
} catch (err) {
|
|
130502
|
+
console.error(`Error parsing ${configPath}: ${err.message}`);
|
|
130503
|
+
process.exit(1);
|
|
130502
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
|
+
};
|
|
130503
130529
|
}
|
|
130504
|
-
|
|
130505
|
-
|
|
130506
|
-
}
|
|
130507
|
-
function getTimeoutMs(config2) {
|
|
130508
|
-
const raw = config2["pre-commit"]?.timeout;
|
|
130509
|
-
return raw ? parseTimeout(raw) : DEFAULT_TIMEOUT_MS;
|
|
130510
|
-
}
|
|
130511
|
-
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() {
|
|
130512
130532
|
const gitDir = findGitDir(process.cwd());
|
|
130513
130533
|
if (!gitDir) {
|
|
130514
130534
|
console.error("Error: Not a git repository.");
|
|
130515
130535
|
process.exit(1);
|
|
130516
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
|
+
}
|
|
130517
130550
|
const hooksDir = path21.join(gitDir, "hooks");
|
|
130518
130551
|
fs16.mkdirSync(hooksDir, { recursive: true });
|
|
130519
130552
|
const hookPath = path21.join(hooksDir, "pre-commit");
|
|
@@ -130531,6 +130564,16 @@ function runHooksInstall() {
|
|
|
130531
130564
|
fs16.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 493 });
|
|
130532
130565
|
console.log("TrueCourse pre-commit hook installed.");
|
|
130533
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
|
+
}
|
|
130534
130577
|
}
|
|
130535
130578
|
function runHooksUninstall() {
|
|
130536
130579
|
const gitDir = findGitDir(process.cwd());
|
|
@@ -130568,125 +130611,97 @@ function runHooksStatus() {
|
|
|
130568
130611
|
}
|
|
130569
130612
|
const projectRoot = findProjectRoot(process.cwd());
|
|
130570
130613
|
if (projectRoot) {
|
|
130571
|
-
const
|
|
130572
|
-
if (
|
|
130573
|
-
console.log(
|
|
130574
|
-
|
|
130575
|
-
|
|
130576
|
-
const blockRules = getBlockRules(config2);
|
|
130577
|
-
console.log("Block on:");
|
|
130578
|
-
for (const rule of blockRules) {
|
|
130579
|
-
if (typeof rule === "string") {
|
|
130580
|
-
console.log(` - ${rule}`);
|
|
130581
|
-
} else {
|
|
130582
|
-
console.log(` - severity: ${rule.severity}`);
|
|
130583
|
-
}
|
|
130584
|
-
}
|
|
130585
|
-
const timeoutMs = getTimeoutMs(config2);
|
|
130586
|
-
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
|
+
);
|
|
130587
130619
|
} else {
|
|
130588
|
-
console.log(
|
|
130589
|
-
|
|
130590
|
-
|
|
130591
|
-
}
|
|
130592
|
-
function shouldBlock(violation, blockRules) {
|
|
130593
|
-
for (const rule of blockRules) {
|
|
130594
|
-
if (typeof rule === "string") {
|
|
130595
|
-
if (violation.ruleKey === rule || violation.ruleKey.endsWith(`/${rule}`)) {
|
|
130596
|
-
return true;
|
|
130597
|
-
}
|
|
130598
|
-
} else if (rule.severity) {
|
|
130599
|
-
if (violation.severity.toLowerCase() === rule.severity.toLowerCase()) {
|
|
130600
|
-
return true;
|
|
130601
|
-
}
|
|
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"}`);
|
|
130602
130624
|
}
|
|
130603
130625
|
}
|
|
130604
|
-
return false;
|
|
130605
130626
|
}
|
|
130606
130627
|
async function runHooksRun() {
|
|
130607
|
-
const startTime = Date.now();
|
|
130608
130628
|
process.stdout.write("TrueCourse pre-commit check...");
|
|
130609
130629
|
const projectRoot = findProjectRoot(process.cwd());
|
|
130610
130630
|
if (!projectRoot) {
|
|
130611
130631
|
console.log(" skipped (not a git repository)");
|
|
130612
130632
|
process.exit(0);
|
|
130613
130633
|
}
|
|
130614
|
-
|
|
130615
|
-
const blockRules = getBlockRules(config2);
|
|
130616
|
-
const timeoutMs = getTimeoutMs(config2);
|
|
130617
|
-
let stagedFiles;
|
|
130634
|
+
let hasStaged = false;
|
|
130618
130635
|
try {
|
|
130619
130636
|
const output = execSync5("git diff --cached --name-only --diff-filter=ACM", {
|
|
130620
130637
|
encoding: "utf-8",
|
|
130621
130638
|
cwd: projectRoot
|
|
130622
130639
|
}).trim();
|
|
130623
|
-
|
|
130640
|
+
hasStaged = output.length > 0;
|
|
130624
130641
|
} catch {
|
|
130625
130642
|
console.log(" skipped (git error)");
|
|
130626
130643
|
process.exit(0);
|
|
130627
130644
|
}
|
|
130628
|
-
if (
|
|
130645
|
+
if (!hasStaged) {
|
|
130629
130646
|
console.log(" \u2714 passed (no staged files)");
|
|
130630
130647
|
process.exit(0);
|
|
130631
130648
|
}
|
|
130632
|
-
|
|
130633
|
-
|
|
130634
|
-
|
|
130635
|
-
|
|
130636
|
-
|
|
130637
|
-
|
|
130638
|
-
parseCode2 = analyzer.parseCode;
|
|
130639
|
-
detectLanguage2 = analyzer.detectLanguage;
|
|
130640
|
-
checkCodeRules2 = analyzer.checkCodeRules;
|
|
130641
|
-
CODE_RULES2 = analyzer.CODE_RULES;
|
|
130642
|
-
await analyzer.initParsers();
|
|
130643
|
-
} catch {
|
|
130644
|
-
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
|
+
);
|
|
130645
130655
|
process.exit(0);
|
|
130646
130656
|
}
|
|
130647
|
-
const
|
|
130648
|
-
|
|
130649
|
-
|
|
130650
|
-
|
|
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);
|
|
130651
130665
|
}
|
|
130652
|
-
const
|
|
130653
|
-
|
|
130654
|
-
|
|
130655
|
-
|
|
130656
|
-
|
|
130657
|
-
|
|
130658
|
-
const
|
|
130659
|
-
|
|
130660
|
-
|
|
130661
|
-
|
|
130662
|
-
|
|
130663
|
-
|
|
130664
|
-
|
|
130665
|
-
|
|
130666
|
-
|
|
130667
|
-
|
|
130668
|
-
|
|
130669
|
-
|
|
130670
|
-
|
|
130671
|
-
try {
|
|
130672
|
-
const tree = parseCode2(content, language);
|
|
130673
|
-
const violations = checkCodeRules2(tree, filePath, content, CODE_RULES2, language);
|
|
130674
|
-
allViolations.push(...violations);
|
|
130675
|
-
} catch {
|
|
130676
|
-
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);
|
|
130677
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);
|
|
130678
130690
|
}
|
|
130679
|
-
const
|
|
130691
|
+
const blockSet = new Set(cfg.blockOn);
|
|
130692
|
+
const blocking = newViolations.filter((v) => blockSet.has(v.severity.toLowerCase()));
|
|
130680
130693
|
if (blocking.length === 0) {
|
|
130681
|
-
console.log(
|
|
130694
|
+
console.log(` \u2714 passed (${newViolations.length} new violations, none at or above ${cfg.blockOn.join("/")})`);
|
|
130682
130695
|
process.exit(0);
|
|
130683
130696
|
}
|
|
130684
130697
|
console.log("\n");
|
|
130685
130698
|
for (const v of blocking) {
|
|
130686
130699
|
const icon = severityIcon(v.severity);
|
|
130687
130700
|
const color = severityColor(v.severity);
|
|
130701
|
+
const location = v.filePath ? `${v.filePath}${v.lineStart ? `:${v.lineStart}` : ""}` : "(no file)";
|
|
130688
130702
|
console.log(` ${color(`${icon} BLOCKED`)}: ${v.title}`);
|
|
130689
|
-
console.log(` ${
|
|
130703
|
+
console.log(` ${location} \u2014 ${v.content}`);
|
|
130704
|
+
if (v.fixPrompt) console.log(` Fix: ${v.fixPrompt}`);
|
|
130690
130705
|
console.log("");
|
|
130691
130706
|
}
|
|
130692
130707
|
console.log("Commit blocked. Fix the issue or bypass with --no-verify.");
|
|
@@ -130695,7 +130710,7 @@ async function runHooksRun() {
|
|
|
130695
130710
|
|
|
130696
130711
|
// tools/cli/src/index.ts
|
|
130697
130712
|
var program2 = new Command();
|
|
130698
|
-
program2.name("truecourse").version("0.5.
|
|
130713
|
+
program2.name("truecourse").version("0.5.2").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
|
|
130699
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) => {
|
|
130700
130715
|
if (options.service && options.console) {
|
|
130701
130716
|
console.error("error: --service and --console are mutually exclusive");
|
|
@@ -130774,8 +130789,8 @@ telemetryCmd.command("status").description("Show current telemetry status").acti
|
|
|
130774
130789
|
}
|
|
130775
130790
|
});
|
|
130776
130791
|
var hooksCmd = program2.command("hooks").description("Manage git hooks");
|
|
130777
|
-
hooksCmd.command("install").description("Install pre-commit hook").action(() => {
|
|
130778
|
-
runHooksInstall();
|
|
130792
|
+
hooksCmd.command("install").description("Install pre-commit hook").action(async () => {
|
|
130793
|
+
await runHooksInstall();
|
|
130779
130794
|
});
|
|
130780
130795
|
hooksCmd.command("uninstall").description("Remove pre-commit hook").action(() => {
|
|
130781
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"
|
|
@@ -18,7 +18,7 @@ Run architecture analysis on the current repository using TrueCourse.
|
|
|
18
18
|
- **Full analysis** stashes any uncommitted changes, analyzes the clean working tree, then unstashes. The user's uncommitted work is preserved.
|
|
19
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
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
|
|
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.
|
|
22
22
|
|
|
23
23
|
## Instructions
|
|
24
24
|
|
|
@@ -30,7 +30,7 @@ Ask the user whether they want a **full analysis** or a **diff check**. If they
|
|
|
30
30
|
|
|
31
31
|
### 2. Decide on LLM rules
|
|
32
32
|
|
|
33
|
-
LLM rules add higher-value insights but cost
|
|
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.
|
|
34
34
|
|
|
35
35
|
- If **user approves LLM**: append `--llm` to the command.
|
|
36
36
|
- If **user declines LLM or wants a free run**: append `--no-llm`.
|
|
@@ -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).
|