truecourse 0.2.1 → 0.2.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.
Files changed (4) hide show
  1. package/README.md +33 -0
  2. package/cli.mjs +132 -42
  3. package/package.json +3 -1
  4. package/server.mjs +41029 -32184
package/README.md CHANGED
@@ -178,6 +178,39 @@ All rules are visible in the **Rules** tab in the web UI. Custom rule generation
178
178
  | Rust | Planned |
179
179
  | PHP | Planned |
180
180
 
181
+ ## Telemetry
182
+
183
+ TrueCourse collects anonymous usage data to help us understand adoption and improve the product. Telemetry is **enabled by default** and can be disabled at any time.
184
+
185
+ ### What is collected
186
+
187
+ - Event type (`analyze` or `diff-check`)
188
+ - Tool version
189
+ - Languages detected (e.g., TypeScript, Python)
190
+ - File count range (bucketed: 1-50, 50-200, etc.)
191
+ - Service count
192
+ - Analysis duration range (bucketed)
193
+ - OS and architecture (e.g., `darwin-arm64`)
194
+ - Random anonymous session ID (not tied to user identity)
195
+
196
+ ### What is NOT collected
197
+
198
+ - Source code, file paths, repo names, or git URLs
199
+ - Violation details, rule results, or LLM outputs
200
+ - IP addresses, user identity, or machine hostname
201
+
202
+ ### Opt out
203
+
204
+ ```bash
205
+ npx truecourse telemetry disable # Disable telemetry
206
+ npx truecourse telemetry enable # Re-enable telemetry
207
+ npx truecourse telemetry status # Check current status
208
+ ```
209
+
210
+ Telemetry is automatically disabled in CI environments (`CI=true`) and can also be disabled by setting `TRUECOURSE_TELEMETRY=0`.
211
+
212
+ Data is sent to [PostHog](https://posthog.com) for aggregation.
213
+
181
214
  ## License
182
215
 
183
216
  MIT
package/cli.mjs CHANGED
@@ -972,8 +972,8 @@ var require_command = __commonJS({
972
972
  "node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports) {
973
973
  var EventEmitter = __require("node:events").EventEmitter;
974
974
  var childProcess = __require("node:child_process");
975
- var path9 = __require("node:path");
976
- var fs8 = __require("node:fs");
975
+ var path10 = __require("node:path");
976
+ var fs9 = __require("node:fs");
977
977
  var process2 = __require("node:process");
978
978
  var { Argument: Argument2, humanReadableArgName } = require_argument();
979
979
  var { CommanderError: CommanderError2 } = require_error();
@@ -1905,11 +1905,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1905
1905
  let launchWithNode = false;
1906
1906
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1907
1907
  function findFile(baseDir, baseName) {
1908
- const localBin = path9.resolve(baseDir, baseName);
1909
- if (fs8.existsSync(localBin)) return localBin;
1910
- if (sourceExt.includes(path9.extname(baseName))) return void 0;
1908
+ const localBin = path10.resolve(baseDir, baseName);
1909
+ if (fs9.existsSync(localBin)) return localBin;
1910
+ if (sourceExt.includes(path10.extname(baseName))) return void 0;
1911
1911
  const foundExt = sourceExt.find(
1912
- (ext) => fs8.existsSync(`${localBin}${ext}`)
1912
+ (ext) => fs9.existsSync(`${localBin}${ext}`)
1913
1913
  );
1914
1914
  if (foundExt) return `${localBin}${foundExt}`;
1915
1915
  return void 0;
@@ -1921,21 +1921,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1921
1921
  if (this._scriptPath) {
1922
1922
  let resolvedScriptPath;
1923
1923
  try {
1924
- resolvedScriptPath = fs8.realpathSync(this._scriptPath);
1924
+ resolvedScriptPath = fs9.realpathSync(this._scriptPath);
1925
1925
  } catch (err) {
1926
1926
  resolvedScriptPath = this._scriptPath;
1927
1927
  }
1928
- executableDir = path9.resolve(
1929
- path9.dirname(resolvedScriptPath),
1928
+ executableDir = path10.resolve(
1929
+ path10.dirname(resolvedScriptPath),
1930
1930
  executableDir
1931
1931
  );
1932
1932
  }
1933
1933
  if (executableDir) {
1934
1934
  let localFile = findFile(executableDir, executableFile);
1935
1935
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1936
- const legacyName = path9.basename(
1936
+ const legacyName = path10.basename(
1937
1937
  this._scriptPath,
1938
- path9.extname(this._scriptPath)
1938
+ path10.extname(this._scriptPath)
1939
1939
  );
1940
1940
  if (legacyName !== this._name) {
1941
1941
  localFile = findFile(
@@ -1946,7 +1946,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1946
1946
  }
1947
1947
  executableFile = localFile || executableFile;
1948
1948
  }
1949
- launchWithNode = sourceExt.includes(path9.extname(executableFile));
1949
+ launchWithNode = sourceExt.includes(path10.extname(executableFile));
1950
1950
  let proc;
1951
1951
  if (process2.platform !== "win32") {
1952
1952
  if (launchWithNode) {
@@ -2786,7 +2786,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2786
2786
  * @return {Command}
2787
2787
  */
2788
2788
  nameFromFilename(filename) {
2789
- this._name = path9.basename(filename, path9.extname(filename));
2789
+ this._name = path10.basename(filename, path10.extname(filename));
2790
2790
  return this;
2791
2791
  }
2792
2792
  /**
@@ -2800,9 +2800,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2800
2800
  * @param {string} [path]
2801
2801
  * @return {(string|null|Command)}
2802
2802
  */
2803
- executableDir(path10) {
2804
- if (path10 === void 0) return this._executableDir;
2805
- this._executableDir = path10;
2803
+ executableDir(path11) {
2804
+ if (path11 === void 0) return this._executableDir;
2805
+ this._executableDir = path11;
2806
2806
  return this;
2807
2807
  }
2808
2808
  /**
@@ -3766,7 +3766,7 @@ ${import_picocolors2.default.gray(m2)} ${s}
3766
3766
  // node_modules/.pnpm/xmlhttprequest-ssl@2.1.2/node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js
3767
3767
  var require_XMLHttpRequest = __commonJS({
3768
3768
  "node_modules/.pnpm/xmlhttprequest-ssl@2.1.2/node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js"(exports, module) {
3769
- var fs8 = __require("fs");
3769
+ var fs9 = __require("fs");
3770
3770
  var Url = __require("url");
3771
3771
  var spawn3 = __require("child_process").spawn;
3772
3772
  module.exports = XMLHttpRequest3;
@@ -3924,7 +3924,7 @@ var require_XMLHttpRequest = __commonJS({
3924
3924
  throw new Error("XMLHttpRequest: Only GET method is supported");
3925
3925
  }
3926
3926
  if (settings.async) {
3927
- fs8.readFile(unescape(url2.pathname), function(error, data2) {
3927
+ fs9.readFile(unescape(url2.pathname), function(error, data2) {
3928
3928
  if (error) {
3929
3929
  self.handleError(error, error.errno || -1);
3930
3930
  } else {
@@ -3936,7 +3936,7 @@ var require_XMLHttpRequest = __commonJS({
3936
3936
  });
3937
3937
  } else {
3938
3938
  try {
3939
- this.response = fs8.readFileSync(unescape(url2.pathname));
3939
+ this.response = fs9.readFileSync(unescape(url2.pathname));
3940
3940
  this.responseText = this.response.toString("utf8");
3941
3941
  this.status = 200;
3942
3942
  setState(self.DONE);
@@ -4062,15 +4062,15 @@ var require_XMLHttpRequest = __commonJS({
4062
4062
  } else {
4063
4063
  var contentFile = ".node-xmlhttprequest-content-" + process.pid;
4064
4064
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
4065
- fs8.writeFileSync(syncFile, "", "utf8");
4065
+ fs9.writeFileSync(syncFile, "", "utf8");
4066
4066
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
4067
4067
  var syncProc = spawn3(process.argv[0], ["-e", execString]);
4068
4068
  var statusText;
4069
- while (fs8.existsSync(syncFile)) {
4069
+ while (fs9.existsSync(syncFile)) {
4070
4070
  }
4071
- self.responseText = fs8.readFileSync(contentFile, "utf8");
4071
+ self.responseText = fs9.readFileSync(contentFile, "utf8");
4072
4072
  syncProc.stdin.end();
4073
- fs8.unlinkSync(contentFile);
4073
+ fs9.unlinkSync(contentFile);
4074
4074
  if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
4075
4075
  var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
4076
4076
  self.handleError(errorObj, 503);
@@ -9762,12 +9762,12 @@ function parse2(str) {
9762
9762
  uri.queryKey = queryKey(uri, uri["query"]);
9763
9763
  return uri;
9764
9764
  }
9765
- function pathNames(obj, path9) {
9766
- const regx = /\/{2,9}/g, names = path9.replace(regx, "/").split("/");
9767
- if (path9.slice(0, 1) == "/" || path9.length === 0) {
9765
+ function pathNames(obj, path10) {
9766
+ const regx = /\/{2,9}/g, names = path10.replace(regx, "/").split("/");
9767
+ if (path10.slice(0, 1) == "/" || path10.length === 0) {
9768
9768
  names.splice(0, 1);
9769
9769
  }
9770
- if (path9.slice(-1) == "/") {
9770
+ if (path10.slice(-1) == "/") {
9771
9771
  names.splice(names.length - 1, 1);
9772
9772
  }
9773
9773
  return names;
@@ -10441,7 +10441,7 @@ var init_esm_debug = __esm({
10441
10441
  });
10442
10442
 
10443
10443
  // node_modules/.pnpm/socket.io-client@4.8.3/node_modules/socket.io-client/build/esm-debug/url.js
10444
- function url(uri, path9 = "", loc) {
10444
+ function url(uri, path10 = "", loc) {
10445
10445
  let obj = uri;
10446
10446
  loc = loc || typeof location !== "undefined" && location;
10447
10447
  if (null == uri)
@@ -10475,7 +10475,7 @@ function url(uri, path9 = "", loc) {
10475
10475
  obj.path = obj.path || "/";
10476
10476
  const ipv6 = obj.host.indexOf(":") !== -1;
10477
10477
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
10478
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path9;
10478
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path10;
10479
10479
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
10480
10480
  return obj;
10481
10481
  }
@@ -12146,8 +12146,8 @@ function lookup(uri, opts) {
12146
12146
  const parsed = url(uri, opts.path || "/socket.io");
12147
12147
  const source = parsed.source;
12148
12148
  const id = parsed.id;
12149
- const path9 = parsed.path;
12150
- const sameNamespace = cache[id] && path9 in cache[id]["nsps"];
12149
+ const path10 = parsed.path;
12150
+ const sameNamespace = cache[id] && path10 in cache[id]["nsps"];
12151
12151
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
12152
12152
  let io;
12153
12153
  if (newConnection) {
@@ -12683,10 +12683,16 @@ function startConsoleMode(openBrowser) {
12683
12683
  process.execPath,
12684
12684
  [serverPath],
12685
12685
  {
12686
- stdio: "inherit",
12686
+ stdio: ["ignore", "pipe", "pipe"],
12687
12687
  env: { ...process.env }
12688
12688
  }
12689
12689
  );
12690
+ const forwardOutput = (data) => {
12691
+ process.stdout.write("\r\x1B[K");
12692
+ process.stderr.write(data);
12693
+ };
12694
+ serverProcess.stdout?.on("data", forwardOutput);
12695
+ serverProcess.stderr?.on("data", forwardOutput);
12690
12696
  serverProcess.on("error", (error) => {
12691
12697
  v2.error(`Failed to start server: ${error.message}`);
12692
12698
  process.exit(1);
@@ -13364,9 +13370,9 @@ var {
13364
13370
  init_dist2();
13365
13371
  init_setup();
13366
13372
  init_start();
13367
- import fs7 from "node:fs";
13368
- import path8 from "node:path";
13369
- import os6 from "node:os";
13373
+ import fs8 from "node:fs";
13374
+ import path9 from "node:path";
13375
+ import os7 from "node:os";
13370
13376
 
13371
13377
  // tools/cli/src/commands/add.ts
13372
13378
  init_dist2();
@@ -13415,9 +13421,73 @@ async function runAdd() {
13415
13421
  // tools/cli/src/commands/analyze.ts
13416
13422
  init_dist2();
13417
13423
  init_helpers();
13424
+
13425
+ // tools/cli/src/telemetry.ts
13426
+ init_dist2();
13427
+ import fs7 from "node:fs";
13428
+ import path7 from "node:path";
13429
+ import os6 from "node:os";
13430
+ import crypto from "node:crypto";
13431
+ var DEFAULT_CONFIG2 = {
13432
+ enabled: true,
13433
+ anonymousId: "",
13434
+ noticeShown: false
13435
+ };
13436
+ function getTelemetryConfigPath() {
13437
+ return path7.join(os6.homedir(), ".truecourse", "telemetry.json");
13438
+ }
13439
+ function readTelemetryConfig() {
13440
+ const configPath = getTelemetryConfigPath();
13441
+ try {
13442
+ const raw = fs7.readFileSync(configPath, "utf-8");
13443
+ const parsed = JSON.parse(raw);
13444
+ const config = { ...DEFAULT_CONFIG2, ...parsed };
13445
+ if (!config.anonymousId) {
13446
+ config.anonymousId = crypto.randomUUID();
13447
+ writeTelemetryConfig(config);
13448
+ }
13449
+ return config;
13450
+ } catch {
13451
+ const config = {
13452
+ enabled: true,
13453
+ anonymousId: crypto.randomUUID(),
13454
+ noticeShown: false
13455
+ };
13456
+ writeTelemetryConfig(config);
13457
+ return config;
13458
+ }
13459
+ }
13460
+ function writeTelemetryConfig(partial) {
13461
+ const configPath = getTelemetryConfigPath();
13462
+ const dir = path7.dirname(configPath);
13463
+ fs7.mkdirSync(dir, { recursive: true });
13464
+ let current;
13465
+ try {
13466
+ const raw = fs7.readFileSync(configPath, "utf-8");
13467
+ current = { ...DEFAULT_CONFIG2, ...JSON.parse(raw) };
13468
+ } catch {
13469
+ current = { ...DEFAULT_CONFIG2 };
13470
+ }
13471
+ const merged = { ...current, ...partial };
13472
+ fs7.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
13473
+ }
13474
+ function showFirstRunNotice() {
13475
+ try {
13476
+ const config = readTelemetryConfig();
13477
+ if (!config.enabled || config.noticeShown) return;
13478
+ v2.info(
13479
+ "TrueCourse collects anonymous usage data to improve the product. Run `npx truecourse telemetry disable` to opt out."
13480
+ );
13481
+ writeTelemetryConfig({ noticeShown: true });
13482
+ } catch {
13483
+ }
13484
+ }
13485
+
13486
+ // tools/cli/src/commands/analyze.ts
13418
13487
  var TIMEOUT_MS = 15 * 60 * 1e3;
13419
13488
  async function runAnalyze({ noAutostart = false, codeReview = false, deterministicOnly = false } = {}) {
13420
13489
  we("Analyzing repository");
13490
+ showFirstRunNotice();
13421
13491
  if (noAutostart) {
13422
13492
  const url2 = getServerUrl();
13423
13493
  try {
@@ -13535,6 +13605,7 @@ async function runAnalyze({ noAutostart = false, codeReview = false, determinist
13535
13605
  }
13536
13606
  async function runAnalyzeDiff({ noAutostart = false } = {}) {
13537
13607
  we("Running diff check");
13608
+ showFirstRunNotice();
13538
13609
  if (noAutostart) {
13539
13610
  const url2 = getServerUrl();
13540
13611
  try {
@@ -13629,11 +13700,11 @@ init_dist2();
13629
13700
  init_platform();
13630
13701
  init_logs();
13631
13702
  init_helpers();
13632
- import path7 from "node:path";
13703
+ import path8 from "node:path";
13633
13704
  import { fileURLToPath as fileURLToPath3 } from "node:url";
13634
- var __dirname2 = path7.dirname(fileURLToPath3(import.meta.url));
13705
+ var __dirname2 = path8.dirname(fileURLToPath3(import.meta.url));
13635
13706
  function getServerPath2() {
13636
- return path7.join(__dirname2, "..", "server.mjs");
13707
+ return path8.join(__dirname2, "..", "server.mjs");
13637
13708
  }
13638
13709
  async function healthcheck2() {
13639
13710
  const url2 = getServerUrl();
@@ -13788,7 +13859,7 @@ function registerServiceCommand(program3) {
13788
13859
  init_helpers();
13789
13860
  init_platform();
13790
13861
  var program2 = new Command();
13791
- program2.name("truecourse").version("0.2.1").description("TrueCourse CLI - Setup and manage your TrueCourse instance");
13862
+ program2.name("truecourse").version("0.2.2").description("TrueCourse CLI - Setup and manage your TrueCourse instance");
13792
13863
  program2.command("setup").description("Run the setup wizard to configure TrueCourse").action(async () => {
13793
13864
  await runSetup();
13794
13865
  });
@@ -13834,10 +13905,29 @@ program2.command("stop").description("Stop the TrueCourse background service").a
13834
13905
  v2.info("Press Ctrl+C in the terminal where TrueCourse is running.");
13835
13906
  }
13836
13907
  });
13908
+ var telemetryCmd = program2.command("telemetry").description("Manage anonymous usage telemetry");
13909
+ telemetryCmd.command("enable").description("Enable anonymous usage telemetry").action(() => {
13910
+ writeTelemetryConfig({ enabled: true });
13911
+ v2.success("Telemetry enabled. Thank you for helping improve TrueCourse!");
13912
+ });
13913
+ telemetryCmd.command("disable").description("Disable anonymous usage telemetry").action(() => {
13914
+ writeTelemetryConfig({ enabled: false });
13915
+ v2.success("Telemetry disabled. No data will be collected.");
13916
+ });
13917
+ telemetryCmd.command("status").description("Show current telemetry status").action(() => {
13918
+ const config = readTelemetryConfig();
13919
+ if (process.env.CI === "true") {
13920
+ v2.info("Telemetry is automatically disabled in CI environments.");
13921
+ } else if (config.enabled) {
13922
+ v2.info("Telemetry is enabled.");
13923
+ } else {
13924
+ v2.info("Telemetry is disabled.");
13925
+ }
13926
+ });
13837
13927
  program2.action(async () => {
13838
- const configDir = path8.join(os6.homedir(), ".truecourse");
13839
- const envPath = path8.join(configDir, ".env");
13840
- const isFirstRun = !fs7.existsSync(envPath);
13928
+ const configDir = path9.join(os7.homedir(), ".truecourse");
13929
+ const envPath = path9.join(configDir, ".env");
13930
+ const isFirstRun = !fs8.existsSync(envPath);
13841
13931
  if (isFirstRun) {
13842
13932
  await runSetup();
13843
13933
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecourse",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Visualize your codebase architecture as an interactive graph",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,8 +15,10 @@
15
15
  "dotenv": "^16.4.0",
16
16
  "embedded-postgres": "18.3.0-beta.16",
17
17
  "postgres": "^3.4.0",
18
+ "pyright": "^1.1.408",
18
19
  "tree-sitter": "^0.25.0",
19
20
  "tree-sitter-javascript": "^0.25.0",
21
+ "tree-sitter-python": "^0.25.0",
20
22
  "tree-sitter-typescript": "^0.23.2"
21
23
  },
22
24
  "optionalDependencies": {