truecourse 0.6.9 → 0.6.10

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.
Files changed (4) hide show
  1. package/README.md +15 -3
  2. package/cli.mjs +78 -28
  3. package/package.json +1 -1
  4. package/server.mjs +77 -27
package/README.md CHANGED
@@ -28,12 +28,24 @@ Both store their results under `.truecourse/` and surface them in a shared [dash
28
28
  <img src="assets/demo.gif" alt="TrueCourse Screenshot" width="100%" />
29
29
  </p>
30
30
 
31
- Jump to: **[1. Analyze](#1-analyze--code-intelligence)** · **[2. Spec → Verify](#2-spec--verify--business-logic-drift)** · **[Dashboard](#dashboard-web-ui)**
31
+ Jump to: **[Install](#install)** · **[1. Analyze](#1-analyze--code-intelligence)** · **[2. Spec → Verify](#2-spec--verify--business-logic-drift)** · **[Dashboard](#dashboard-web-ui)**
32
32
 
33
33
  No setup step and no database: TrueCourse creates `.truecourse/` in your repo on first use and stores everything there as plain JSON files. It uses the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) for LLM-powered work — if `claude` isn't on your PATH, deterministic analysis still runs and LLM-dependent features are skipped.
34
34
 
35
35
  ---
36
36
 
37
+ # Install
38
+
39
+ ```bash
40
+ npm install -g truecourse
41
+ ```
42
+
43
+ This puts the `truecourse` command on your PATH — every example below uses it. Prefer not to install globally? Run any command one-off with `npx truecourse <command>` instead.
44
+
45
+ See [Prerequisites](#prerequisites) for Node, Claude Code, and C# requirements.
46
+
47
+ ---
48
+
37
49
  # 1. Analyze — code intelligence
38
50
 
39
51
  Static + LLM analysis of your code: architecture, security, bugs, performance, and more.
@@ -42,8 +54,8 @@ Static + LLM analysis of your code: architecture, security, bugs, performance, a
42
54
 
43
55
  ```bash
44
56
  cd <your-repo>
45
- npx truecourse analyze # Runs the full analysis in-process
46
- npx truecourse list # Show the violations it found
57
+ truecourse analyze # Runs the full analysis in-process
58
+ truecourse list # Show the violations it found
47
59
  ```
48
60
 
49
61
  The first analyze creates `.truecourse/` and stores results as plain JSON. View them visually with [`truecourse dashboard`](#dashboard-web-ui).
package/cli.mjs CHANGED
@@ -26795,12 +26795,9 @@ var init_lsp_client = __esm({
26795
26795
  this.process.stdout.on("data", (chunk) => this.onData(chunk));
26796
26796
  this.process.stderr.on("data", (chunk) => {
26797
26797
  });
26798
- this.process.on("error", (err) => {
26799
- for (const pending of this.pendingRequests.values()) {
26800
- pending.reject(err);
26801
- }
26802
- this.pendingRequests.clear();
26803
- });
26798
+ this.process.on("error", (err) => this.rejectAllPending(err));
26799
+ this.process.stdin.on("error", (err) => this.rejectAllPending(new Error(`${this.config.name} LSP stdin error: ${err.message}`)));
26800
+ this.process.on("exit", (code, signal) => this.rejectAllPending(new Error(`${this.config.name} LSP server exited (${signal ?? `code ${code}`}) before responding`)));
26804
26801
  const initResult = await this.sendRequest("initialize", {
26805
26802
  processId: process.pid,
26806
26803
  capabilities: {
@@ -26979,17 +26976,33 @@ var init_lsp_client = __esm({
26979
26976
  // -------------------------------------------------------------------------
26980
26977
  // Internal: JSON-RPC transport
26981
26978
  // -------------------------------------------------------------------------
26979
+ /** Reject and clear every in-flight request — used when the server dies. */
26980
+ rejectAllPending(err) {
26981
+ if (this.pendingRequests.size === 0)
26982
+ return;
26983
+ for (const pending of this.pendingRequests.values())
26984
+ pending.reject(err);
26985
+ this.pendingRequests.clear();
26986
+ }
26982
26987
  sendRequest(method, params) {
26983
26988
  return new Promise((resolve12, reject) => {
26984
26989
  const id = ++this.requestId;
26985
26990
  this.pendingRequests.set(id, { resolve: resolve12, reject });
26986
26991
  const msg = { jsonrpc: "2.0", id, method, params };
26987
- this.process.stdin.write(encodeMessage(msg));
26992
+ try {
26993
+ this.process.stdin.write(encodeMessage(msg));
26994
+ } catch (err) {
26995
+ this.pendingRequests.delete(id);
26996
+ reject(err instanceof Error ? err : new Error(String(err)));
26997
+ }
26988
26998
  });
26989
26999
  }
26990
27000
  sendNotification(method, params) {
26991
27001
  const msg = { jsonrpc: "2.0", method, params };
26992
- this.process.stdin.write(encodeMessage(msg));
27002
+ try {
27003
+ this.process.stdin.write(encodeMessage(msg));
27004
+ } catch {
27005
+ }
26993
27006
  }
26994
27007
  onData(chunk) {
26995
27008
  this.buffer = Buffer.concat([this.buffer, chunk]);
@@ -144749,6 +144762,7 @@ var init_pyproject_checker = __esm({
144749
144762
  // packages/analyzer/dist/roslyn-host-client.js
144750
144763
  import { spawn as spawn4 } from "child_process";
144751
144764
  import { existsSync as existsSync10 } from "fs";
144765
+ import { tmpdir } from "os";
144752
144766
  import { dirname as dirname9, resolve as resolve9 } from "path";
144753
144767
  import { fileURLToPath as fileURLToPath5 } from "url";
144754
144768
  function resolveRoslynHostBinary() {
@@ -144766,35 +144780,66 @@ function resolveRoslynHostBinary() {
144766
144780
  }
144767
144781
  function invokeHost(bin, request) {
144768
144782
  return new Promise((resolvePromise, reject) => {
144769
- const child = spawn4(bin, [], { stdio: ["pipe", "pipe", "pipe"] });
144783
+ const child = spawn4(bin, [], { stdio: ["pipe", "pipe", "pipe"], cwd: tmpdir() });
144770
144784
  let stdout = "";
144771
144785
  let stderr = "";
144786
+ let settled = false;
144787
+ const fail4 = (err) => {
144788
+ if (settled)
144789
+ return;
144790
+ settled = true;
144791
+ reject(err);
144792
+ };
144793
+ const succeed = (violations) => {
144794
+ if (settled)
144795
+ return;
144796
+ settled = true;
144797
+ resolvePromise(violations);
144798
+ };
144772
144799
  child.stdout.setEncoding("utf8");
144773
144800
  child.stderr.setEncoding("utf8");
144774
144801
  child.stdout.on("data", (d3) => stdout += d3);
144775
144802
  child.stderr.on("data", (d3) => stderr += d3);
144776
- child.on("error", (e) => reject(new RoslynHostUnavailableError(`Failed to start the Roslyn host (${bin}): ${e.message}. Is the .NET runtime installed?`)));
144777
- child.on("close", () => {
144778
- const line = stdout.split("\n").find((l) => l.trim());
144779
- if (!line) {
144780
- reject(new Error(`Roslyn host produced no output.${stderr ? ` stderr: ${stderr}` : ""}`));
144803
+ child.on("error", (e) => fail4(new RoslynHostUnavailableError(`Failed to start the Roslyn host (${bin}): ${e.message}. Is the .NET runtime installed?`)));
144804
+ child.stdin.on("error", () => {
144805
+ });
144806
+ child.on("close", (code, signalName) => {
144807
+ if (code !== 0 || signalName) {
144808
+ const reason = stderr.trim() || stdout.trim() || "(no output)";
144809
+ fail4(new RoslynHostUnavailableError(`The Roslyn host exited ${signalName ? `via ${signalName}` : `with code ${code}`} before responding. Is a compatible .NET runtime installed? Host output: ${reason}`));
144810
+ return;
144811
+ }
144812
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
144813
+ if (lines.length === 0) {
144814
+ fail4(new Error(`Roslyn host produced no output.${stderr ? ` stderr: ${stderr}` : ""}`));
144781
144815
  return;
144782
144816
  }
144783
144817
  let resp;
144784
- try {
144785
- resp = JSON.parse(line);
144786
- } catch {
144787
- reject(new Error(`Roslyn host returned invalid JSON: ${line}`));
144818
+ for (const l of lines) {
144819
+ try {
144820
+ const parsed = JSON.parse(l);
144821
+ if (parsed && typeof parsed.ok === "boolean") {
144822
+ resp = parsed;
144823
+ break;
144824
+ }
144825
+ } catch {
144826
+ }
144827
+ }
144828
+ if (!resp) {
144829
+ fail4(new Error(`Roslyn host returned invalid JSON: ${lines[0]}`));
144788
144830
  return;
144789
144831
  }
144790
144832
  if (!resp.ok) {
144791
- reject(new Error(`Roslyn host error: ${resp.error ?? "unknown"}`));
144833
+ fail4(new Error(`Roslyn host error: ${resp.error ?? "unknown"}`));
144792
144834
  return;
144793
144835
  }
144794
- resolvePromise(resp.violations ?? []);
144836
+ succeed(resp.violations ?? []);
144795
144837
  });
144796
- child.stdin.write(JSON.stringify(request) + "\n");
144797
- child.stdin.end();
144838
+ try {
144839
+ child.stdin.write(JSON.stringify(request) + "\n");
144840
+ child.stdin.end();
144841
+ } catch {
144842
+ }
144798
144843
  });
144799
144844
  }
144800
144845
  function runRoslynWorkspace(projectPath, rules) {
@@ -147477,7 +147522,7 @@ var init_schemas2 = __esm({
147477
147522
  // packages/core/dist/services/llm/cli-provider.js
147478
147523
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
147479
147524
  import { join as join10 } from "node:path";
147480
- import { tmpdir } from "node:os";
147525
+ import { tmpdir as tmpdir2 } from "node:os";
147481
147526
  import { randomUUID as randomUUID4 } from "node:crypto";
147482
147527
  var import_cross_spawn, BaseCLIProvider, ClaudeCodeProvider;
147483
147528
  var init_cli_provider = __esm({
@@ -147547,7 +147592,7 @@ var init_cli_provider = __esm({
147547
147592
  constructor(transport) {
147548
147593
  this.transport = transport;
147549
147594
  if (process.env.TRUECOURSE_CLI_DEBUG) {
147550
- this.debugDir = join10(tmpdir(), "truecourse-cli-debug");
147595
+ this.debugDir = join10(tmpdir2(), "truecourse-cli-debug");
147551
147596
  mkdirSync2(this.debugDir, { recursive: true });
147552
147597
  log.info(`[CLI] Debug output: ${this.debugDir}`);
147553
147598
  }
@@ -147660,8 +147705,13 @@ var init_cli_provider = __esm({
147660
147705
  unregisterChildProcess(this._repoId, child);
147661
147706
  reject(new Error(`[CLI] Failed to spawn ${this.binaryName}: ${err.message}`));
147662
147707
  });
147663
- child.stdin.write(prompt);
147664
- child.stdin.end();
147708
+ child.stdin.on("error", () => {
147709
+ });
147710
+ try {
147711
+ child.stdin.write(prompt);
147712
+ child.stdin.end();
147713
+ } catch {
147714
+ }
147665
147715
  });
147666
147716
  }
147667
147717
  /** Extract usage data from CLI JSON envelope if present. */
@@ -169400,7 +169450,7 @@ function readToolVersion() {
169400
169450
  if (cachedVersion)
169401
169451
  return cachedVersion;
169402
169452
  if (true) {
169403
- cachedVersion = "0.6.9";
169453
+ cachedVersion = "0.6.10";
169404
169454
  return cachedVersion;
169405
169455
  }
169406
169456
  try {
@@ -206318,7 +206368,7 @@ async function runHooksRun() {
206318
206368
 
206319
206369
  // tools/cli/src/index.ts
206320
206370
  var program2 = new Command();
206321
- program2.name("truecourse").version("0.6.9").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
206371
+ program2.name("truecourse").version("0.6.10").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
206322
206372
  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) => {
206323
206373
  if (options.service && options.console) {
206324
206374
  console.error("error: --service and --console are mutually exclusive");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecourse",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "Visualize your codebase architecture as an interactive graph",
5
5
  "type": "module",
6
6
  "bin": {
package/server.mjs CHANGED
@@ -51597,12 +51597,9 @@ var init_lsp_client = __esm({
51597
51597
  this.process.stdout.on("data", (chunk) => this.onData(chunk));
51598
51598
  this.process.stderr.on("data", (chunk) => {
51599
51599
  });
51600
- this.process.on("error", (err) => {
51601
- for (const pending of this.pendingRequests.values()) {
51602
- pending.reject(err);
51603
- }
51604
- this.pendingRequests.clear();
51605
- });
51600
+ this.process.on("error", (err) => this.rejectAllPending(err));
51601
+ this.process.stdin.on("error", (err) => this.rejectAllPending(new Error(`${this.config.name} LSP stdin error: ${err.message}`)));
51602
+ this.process.on("exit", (code, signal) => this.rejectAllPending(new Error(`${this.config.name} LSP server exited (${signal ?? `code ${code}`}) before responding`)));
51606
51603
  const initResult = await this.sendRequest("initialize", {
51607
51604
  processId: process.pid,
51608
51605
  capabilities: {
@@ -51781,17 +51778,33 @@ var init_lsp_client = __esm({
51781
51778
  // -------------------------------------------------------------------------
51782
51779
  // Internal: JSON-RPC transport
51783
51780
  // -------------------------------------------------------------------------
51781
+ /** Reject and clear every in-flight request — used when the server dies. */
51782
+ rejectAllPending(err) {
51783
+ if (this.pendingRequests.size === 0)
51784
+ return;
51785
+ for (const pending of this.pendingRequests.values())
51786
+ pending.reject(err);
51787
+ this.pendingRequests.clear();
51788
+ }
51784
51789
  sendRequest(method, params) {
51785
51790
  return new Promise((resolve11, reject) => {
51786
51791
  const id = ++this.requestId;
51787
51792
  this.pendingRequests.set(id, { resolve: resolve11, reject });
51788
51793
  const msg = { jsonrpc: "2.0", id, method, params };
51789
- this.process.stdin.write(encodeMessage(msg));
51794
+ try {
51795
+ this.process.stdin.write(encodeMessage(msg));
51796
+ } catch (err) {
51797
+ this.pendingRequests.delete(id);
51798
+ reject(err instanceof Error ? err : new Error(String(err)));
51799
+ }
51790
51800
  });
51791
51801
  }
51792
51802
  sendNotification(method, params) {
51793
51803
  const msg = { jsonrpc: "2.0", method, params };
51794
- this.process.stdin.write(encodeMessage(msg));
51804
+ try {
51805
+ this.process.stdin.write(encodeMessage(msg));
51806
+ } catch {
51807
+ }
51795
51808
  }
51796
51809
  onData(chunk) {
51797
51810
  this.buffer = Buffer.concat([this.buffer, chunk]);
@@ -168808,6 +168821,7 @@ var init_pyproject_checker = __esm({
168808
168821
  // packages/analyzer/dist/roslyn-host-client.js
168809
168822
  import { spawn as spawn3 } from "child_process";
168810
168823
  import { existsSync as existsSync9 } from "fs";
168824
+ import { tmpdir } from "os";
168811
168825
  import { dirname as dirname8, resolve as resolve8 } from "path";
168812
168826
  import { fileURLToPath as fileURLToPath4 } from "url";
168813
168827
  function resolveRoslynHostBinary() {
@@ -168825,35 +168839,66 @@ function resolveRoslynHostBinary() {
168825
168839
  }
168826
168840
  function invokeHost(bin, request) {
168827
168841
  return new Promise((resolvePromise, reject) => {
168828
- const child = spawn3(bin, [], { stdio: ["pipe", "pipe", "pipe"] });
168842
+ const child = spawn3(bin, [], { stdio: ["pipe", "pipe", "pipe"], cwd: tmpdir() });
168829
168843
  let stdout = "";
168830
168844
  let stderr = "";
168845
+ let settled = false;
168846
+ const fail = (err) => {
168847
+ if (settled)
168848
+ return;
168849
+ settled = true;
168850
+ reject(err);
168851
+ };
168852
+ const succeed = (violations) => {
168853
+ if (settled)
168854
+ return;
168855
+ settled = true;
168856
+ resolvePromise(violations);
168857
+ };
168831
168858
  child.stdout.setEncoding("utf8");
168832
168859
  child.stderr.setEncoding("utf8");
168833
168860
  child.stdout.on("data", (d) => stdout += d);
168834
168861
  child.stderr.on("data", (d) => stderr += d);
168835
- child.on("error", (e) => reject(new RoslynHostUnavailableError(`Failed to start the Roslyn host (${bin}): ${e.message}. Is the .NET runtime installed?`)));
168836
- child.on("close", () => {
168837
- const line = stdout.split("\n").find((l) => l.trim());
168838
- if (!line) {
168839
- reject(new Error(`Roslyn host produced no output.${stderr ? ` stderr: ${stderr}` : ""}`));
168862
+ child.on("error", (e) => fail(new RoslynHostUnavailableError(`Failed to start the Roslyn host (${bin}): ${e.message}. Is the .NET runtime installed?`)));
168863
+ child.stdin.on("error", () => {
168864
+ });
168865
+ child.on("close", (code, signalName) => {
168866
+ if (code !== 0 || signalName) {
168867
+ const reason = stderr.trim() || stdout.trim() || "(no output)";
168868
+ fail(new RoslynHostUnavailableError(`The Roslyn host exited ${signalName ? `via ${signalName}` : `with code ${code}`} before responding. Is a compatible .NET runtime installed? Host output: ${reason}`));
168869
+ return;
168870
+ }
168871
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
168872
+ if (lines.length === 0) {
168873
+ fail(new Error(`Roslyn host produced no output.${stderr ? ` stderr: ${stderr}` : ""}`));
168840
168874
  return;
168841
168875
  }
168842
168876
  let resp;
168843
- try {
168844
- resp = JSON.parse(line);
168845
- } catch {
168846
- reject(new Error(`Roslyn host returned invalid JSON: ${line}`));
168877
+ for (const l of lines) {
168878
+ try {
168879
+ const parsed = JSON.parse(l);
168880
+ if (parsed && typeof parsed.ok === "boolean") {
168881
+ resp = parsed;
168882
+ break;
168883
+ }
168884
+ } catch {
168885
+ }
168886
+ }
168887
+ if (!resp) {
168888
+ fail(new Error(`Roslyn host returned invalid JSON: ${lines[0]}`));
168847
168889
  return;
168848
168890
  }
168849
168891
  if (!resp.ok) {
168850
- reject(new Error(`Roslyn host error: ${resp.error ?? "unknown"}`));
168892
+ fail(new Error(`Roslyn host error: ${resp.error ?? "unknown"}`));
168851
168893
  return;
168852
168894
  }
168853
- resolvePromise(resp.violations ?? []);
168895
+ succeed(resp.violations ?? []);
168854
168896
  });
168855
- child.stdin.write(JSON.stringify(request) + "\n");
168856
- child.stdin.end();
168897
+ try {
168898
+ child.stdin.write(JSON.stringify(request) + "\n");
168899
+ child.stdin.end();
168900
+ } catch {
168901
+ }
168857
168902
  });
168858
168903
  }
168859
168904
  function runRoslynWorkspace(projectPath, rules) {
@@ -172082,7 +172127,7 @@ var init_schemas2 = __esm({
172082
172127
  // packages/core/dist/services/llm/cli-provider.js
172083
172128
  import { mkdirSync, writeFileSync } from "node:fs";
172084
172129
  import { join as join10 } from "node:path";
172085
- import { tmpdir } from "node:os";
172130
+ import { tmpdir as tmpdir2 } from "node:os";
172086
172131
  import { randomUUID as randomUUID2 } from "node:crypto";
172087
172132
  var import_cross_spawn, BaseCLIProvider, ClaudeCodeProvider;
172088
172133
  var init_cli_provider = __esm({
@@ -172152,7 +172197,7 @@ var init_cli_provider = __esm({
172152
172197
  constructor(transport) {
172153
172198
  this.transport = transport;
172154
172199
  if (process.env.TRUECOURSE_CLI_DEBUG) {
172155
- this.debugDir = join10(tmpdir(), "truecourse-cli-debug");
172200
+ this.debugDir = join10(tmpdir2(), "truecourse-cli-debug");
172156
172201
  mkdirSync(this.debugDir, { recursive: true });
172157
172202
  log.info(`[CLI] Debug output: ${this.debugDir}`);
172158
172203
  }
@@ -172265,8 +172310,13 @@ var init_cli_provider = __esm({
172265
172310
  unregisterChildProcess(this._repoId, child);
172266
172311
  reject(new Error(`[CLI] Failed to spawn ${this.binaryName}: ${err.message}`));
172267
172312
  });
172268
- child.stdin.write(prompt);
172269
- child.stdin.end();
172313
+ child.stdin.on("error", () => {
172314
+ });
172315
+ try {
172316
+ child.stdin.write(prompt);
172317
+ child.stdin.end();
172318
+ } catch {
172319
+ }
172270
172320
  });
172271
172321
  }
172272
172322
  /** Extract usage data from CLI JSON envelope if present. */
@@ -198085,7 +198135,7 @@ function readToolVersion() {
198085
198135
  if (cachedVersion)
198086
198136
  return cachedVersion;
198087
198137
  if (true) {
198088
- cachedVersion = "0.6.9";
198138
+ cachedVersion = "0.6.10";
198089
198139
  return cachedVersion;
198090
198140
  }
198091
198141
  try {