truecourse 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -12610,6 +12610,7 @@ var init_logs = __esm({
12610
12610
  // tools/cli/src/commands/start.ts
12611
12611
  var start_exports = {};
12612
12612
  __export(start_exports, {
12613
+ getServerProcess: () => getServerProcess,
12613
12614
  runStart: () => runStart
12614
12615
  });
12615
12616
  import { spawn as spawn2 } from "node:child_process";
@@ -12662,10 +12663,14 @@ async function startServiceMode(openBrowser) {
12662
12663
  v2.info("Check logs with: truecourse service logs");
12663
12664
  }
12664
12665
  }
12665
- function startConsoleMode() {
12666
+ function getServerProcess() {
12667
+ return _serverProcess;
12668
+ }
12669
+ function startConsoleMode(openBrowser) {
12666
12670
  const serverPath = getServerPath();
12671
+ const url2 = getServerUrl();
12667
12672
  v2.step("Starting server (embedded PostgreSQL starts automatically)...");
12668
- const serverProcess = spawn2(
12673
+ const serverProcess = _serverProcess = spawn2(
12669
12674
  process.execPath,
12670
12675
  [serverPath],
12671
12676
  {
@@ -12687,6 +12692,11 @@ function startConsoleMode() {
12687
12692
  };
12688
12693
  process.on("SIGINT", cleanup);
12689
12694
  process.on("SIGTERM", cleanup);
12695
+ if (openBrowser) {
12696
+ healthcheck().then((healthy) => {
12697
+ if (healthy) openInBrowser(url2);
12698
+ });
12699
+ }
12690
12700
  }
12691
12701
  async function runStart({ openBrowser = true } = {}) {
12692
12702
  we("Starting TrueCourse");
@@ -12697,13 +12707,13 @@ async function runStart({ openBrowser = true } = {}) {
12697
12707
  } catch (error) {
12698
12708
  v2.error(`Service mode failed: ${error.message}`);
12699
12709
  v2.info("Falling back to console mode. Reconfigure with: truecourse setup");
12700
- startConsoleMode();
12710
+ startConsoleMode(openBrowser);
12701
12711
  }
12702
12712
  } else {
12703
- startConsoleMode();
12713
+ startConsoleMode(openBrowser);
12704
12714
  }
12705
12715
  }
12706
- var __dirname;
12716
+ var __dirname, _serverProcess;
12707
12717
  var init_start = __esm({
12708
12718
  "tools/cli/src/commands/start.ts"() {
12709
12719
  "use strict";
@@ -12712,6 +12722,7 @@ var init_start = __esm({
12712
12722
  init_platform();
12713
12723
  init_logs();
12714
12724
  __dirname = path4.dirname(fileURLToPath(import.meta.url));
12725
+ _serverProcess = null;
12715
12726
  }
12716
12727
  });
12717
12728
 
@@ -12724,6 +12735,7 @@ __export(helpers_exports, {
12724
12735
  getConfigPath: () => getConfigPath,
12725
12736
  getServerUrl: () => getServerUrl,
12726
12737
  openInBrowser: () => openInBrowser,
12738
+ promptInstallSkills: () => promptInstallSkills,
12727
12739
  readConfig: () => readConfig,
12728
12740
  renderDiffResults: () => renderDiffResults,
12729
12741
  renderDiffResultsSummary: () => renderDiffResultsSummary,
@@ -12734,8 +12746,11 @@ __export(helpers_exports, {
12734
12746
  writeConfig: () => writeConfig
12735
12747
  });
12736
12748
  import { exec } from "node:child_process";
12749
+ import { cpSync, existsSync } from "node:fs";
12737
12750
  import fs5 from "node:fs";
12738
12751
  import path5 from "node:path";
12752
+ import { dirname, resolve } from "node:path";
12753
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
12739
12754
  import os4 from "node:os";
12740
12755
  function getConfigPath() {
12741
12756
  return path5.join(os4.homedir(), ".truecourse", "config.json");
@@ -12769,16 +12784,28 @@ async function ensureServer() {
12769
12784
  if (!res.ok) throw new Error(`Server returned ${res.status}`);
12770
12785
  return false;
12771
12786
  } catch {
12772
- const { runStart: runStart2 } = await Promise.resolve().then(() => (init_start(), start_exports));
12787
+ const envPath = path5.join(os4.homedir(), ".truecourse", ".env");
12788
+ if (!fs5.existsSync(envPath)) {
12789
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
12790
+ await runSetup2();
12791
+ }
12792
+ const { runStart: runStart2, getServerProcess: getServerProcess2 } = await Promise.resolve().then(() => (init_start(), start_exports));
12773
12793
  await runStart2({ openBrowser: false });
12774
- try {
12775
- const res = await fetch(`${url2}/api/health`);
12776
- if (!res.ok) throw new Error();
12777
- } catch {
12778
- v2.error("Server failed to start. Check logs with: truecourse service logs");
12779
- process.exit(1);
12794
+ const killServer = () => {
12795
+ const proc = getServerProcess2();
12796
+ if (proc && !proc.killed) proc.kill("SIGTERM");
12797
+ };
12798
+ process.on("exit", killServer);
12799
+ for (let i = 0; i < 120; i++) {
12800
+ try {
12801
+ const res = await fetch(`${url2}/api/health`);
12802
+ if (res.ok) return true;
12803
+ } catch {
12804
+ }
12805
+ await new Promise((r2) => setTimeout(r2, 500));
12780
12806
  }
12781
- return true;
12807
+ v2.error("Server failed to start. Check logs with: truecourse service logs");
12808
+ process.exit(1);
12782
12809
  }
12783
12810
  }
12784
12811
  async function ensureRepo() {
@@ -12801,7 +12828,11 @@ async function ensureRepo() {
12801
12828
  v2.error(message);
12802
12829
  process.exit(1);
12803
12830
  }
12804
- return await res.json();
12831
+ const repo = await res.json();
12832
+ if (res.status === 201) {
12833
+ await promptInstallSkills(process.cwd());
12834
+ }
12835
+ return repo;
12805
12836
  }
12806
12837
  function connectSocket(repoId) {
12807
12838
  const url2 = getServerUrl();
@@ -12995,9 +13026,30 @@ function renderDiffResultsSummary(result) {
12995
13026
  v2.info("Run `truecourse list --diff` to see full details.");
12996
13027
  }
12997
13028
  function openInBrowser(url2) {
13029
+ console.log(` Opening ${url2}`);
12998
13030
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
12999
13031
  exec(`${cmd} ${url2}`);
13000
13032
  }
13033
+ async function promptInstallSkills(repoPath) {
13034
+ const installSkills = await me({
13035
+ message: "Would you like to install Claude Code skills?"
13036
+ });
13037
+ if (BD(installSkills) || !installSkills) return;
13038
+ const __dirname3 = dirname(fileURLToPath2(import.meta.url));
13039
+ const srcPath = resolve(__dirname3, "..", "..", "skills", "truecourse");
13040
+ const distPath = resolve(__dirname3, "skills", "truecourse");
13041
+ const skillsSrc = existsSync(srcPath) ? srcPath : distPath;
13042
+ if (!existsSync(skillsSrc)) {
13043
+ v2.warn("Skills directory not found in package \u2014 skipping.");
13044
+ return;
13045
+ }
13046
+ const skillsDest = resolve(repoPath, ".claude", "skills");
13047
+ cpSync(skillsSrc, skillsDest, { recursive: true });
13048
+ v2.success("Installed Claude Code skills:");
13049
+ v2.message(" - truecourse-analyze (run analysis)");
13050
+ v2.message(" - truecourse-list (list violations)");
13051
+ v2.message(" - truecourse-fix (apply fixes)");
13052
+ }
13001
13053
  var DEFAULT_PORT, DEFAULT_CONFIG;
13002
13054
  var init_helpers = __esm({
13003
13055
  "tools/cli/src/commands/helpers.ts"() {
@@ -13009,38 +13061,15 @@ var init_helpers = __esm({
13009
13061
  }
13010
13062
  });
13011
13063
 
13012
- // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
13013
- var import_index = __toESM(require_commander(), 1);
13014
- var {
13015
- program,
13016
- createCommand,
13017
- createArgument,
13018
- createOption,
13019
- CommanderError,
13020
- InvalidArgumentError,
13021
- InvalidOptionArgumentError,
13022
- // deprecated old name
13023
- Command,
13024
- Argument,
13025
- Option,
13026
- Help
13027
- } = import_index.default;
13028
-
13029
- // tools/cli/src/index.ts
13030
- init_dist2();
13031
- import fs7 from "node:fs";
13032
- import path8 from "node:path";
13033
- import os6 from "node:os";
13034
-
13035
13064
  // tools/cli/src/commands/setup.ts
13036
- init_dist2();
13065
+ var setup_exports = {};
13066
+ __export(setup_exports, {
13067
+ runSetup: () => runSetup
13068
+ });
13069
+ import { execSync as execSync4 } from "node:child_process";
13037
13070
  import fs6 from "node:fs";
13038
13071
  import path6 from "node:path";
13039
13072
  import os5 from "node:os";
13040
- var DEFAULT_MODELS = {
13041
- anthropic: "claude-sonnet-4-20250514",
13042
- openai: "gpt-5.3-codex"
13043
- };
13044
13073
  function buildEnvContents(config) {
13045
13074
  const lines = [
13046
13075
  "# TrueCourse Environment Configuration",
@@ -13073,8 +13102,9 @@ async function runSetup() {
13073
13102
  const provider = await de({
13074
13103
  message: "Which LLM provider would you like to use?",
13075
13104
  options: [
13076
- { value: "anthropic", label: "Anthropic (Claude)" },
13077
- { value: "openai", label: "OpenAI (GPT)" },
13105
+ { value: "claude-code", label: "Claude Code CLI \u2014 no API key needed (Recommended)" },
13106
+ { value: "anthropic", label: "Anthropic (Claude API)" },
13107
+ { value: "openai", label: "OpenAI (GPT API)" },
13078
13108
  { value: "skip", label: "Skip for now" }
13079
13109
  ]
13080
13110
  });
@@ -13083,9 +13113,18 @@ async function runSetup() {
13083
13113
  process.exit(0);
13084
13114
  }
13085
13115
  const config = {};
13086
- if (provider === "anthropic" || provider === "openai") {
13116
+ if (provider === "anthropic" || provider === "openai" || provider === "claude-code") {
13087
13117
  config.provider = provider;
13088
13118
  }
13119
+ if (provider === "claude-code") {
13120
+ try {
13121
+ execSync4("which claude", { stdio: "ignore" });
13122
+ } catch {
13123
+ v2.error("Claude Code CLI not found on PATH. Install it first: https://docs.anthropic.com/en/docs/claude-code");
13124
+ process.exit(1);
13125
+ }
13126
+ v2.success("Claude Code CLI detected.");
13127
+ }
13089
13128
  if (provider === "anthropic") {
13090
13129
  const anthropicKey = await ue({
13091
13130
  message: "Enter your Anthropic API key:",
@@ -13130,7 +13169,7 @@ async function runSetup() {
13130
13169
  }
13131
13170
  config.model = model?.trim() || defaultModel;
13132
13171
  }
13133
- const useLangfuse = await me({
13172
+ const useLangfuse = provider !== "claude-code" && await me({
13134
13173
  message: "Would you like to enable Langfuse tracing?",
13135
13174
  initialValue: false
13136
13175
  });
@@ -13176,8 +13215,8 @@ async function runSetup() {
13176
13215
  const runMode = await de({
13177
13216
  message: "How would you like to run TrueCourse?",
13178
13217
  options: [
13179
- { value: "console", label: "Console (keep terminal open)" },
13180
- { value: "service", label: "Background service (runs automatically, no terminal needed)" }
13218
+ { value: "service", label: "Background service (Recommended)" },
13219
+ { value: "console", label: "Console (keep terminal open)" }
13181
13220
  ]
13182
13221
  });
13183
13222
  if (BD(runMode)) {
@@ -13193,16 +13232,46 @@ async function runSetup() {
13193
13232
  v2.info("Database migrations are applied on server startup.");
13194
13233
  fe("Setup complete!");
13195
13234
  }
13235
+ var DEFAULT_MODELS;
13236
+ var init_setup = __esm({
13237
+ "tools/cli/src/commands/setup.ts"() {
13238
+ "use strict";
13239
+ init_dist2();
13240
+ DEFAULT_MODELS = {
13241
+ anthropic: "claude-sonnet-4-20250514",
13242
+ openai: "gpt-5.3-codex"
13243
+ };
13244
+ }
13245
+ });
13246
+
13247
+ // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
13248
+ var import_index = __toESM(require_commander(), 1);
13249
+ var {
13250
+ program,
13251
+ createCommand,
13252
+ createArgument,
13253
+ createOption,
13254
+ CommanderError,
13255
+ InvalidArgumentError,
13256
+ InvalidOptionArgumentError,
13257
+ // deprecated old name
13258
+ Command,
13259
+ Argument,
13260
+ Option,
13261
+ Help
13262
+ } = import_index.default;
13196
13263
 
13197
13264
  // tools/cli/src/index.ts
13265
+ init_dist2();
13266
+ init_setup();
13198
13267
  init_start();
13268
+ import fs7 from "node:fs";
13269
+ import path8 from "node:path";
13270
+ import os6 from "node:os";
13199
13271
 
13200
13272
  // tools/cli/src/commands/add.ts
13201
13273
  init_dist2();
13202
13274
  init_helpers();
13203
- import { cpSync, existsSync } from "node:fs";
13204
- import { resolve, dirname } from "node:path";
13205
- import { fileURLToPath as fileURLToPath2 } from "node:url";
13206
13275
  async function runAdd() {
13207
13276
  const repoPath = process.cwd();
13208
13277
  const serverUrl = getServerUrl();
@@ -13229,30 +13298,8 @@ async function runAdd() {
13229
13298
  const repo = await res.json();
13230
13299
  const repoUrl = `${serverUrl}/repos/${repo.id}`;
13231
13300
  v2.success(`Repository "${repo.name}" added`);
13232
- const installSkills = await me({
13233
- message: "Would you like to install Claude Code skills?"
13234
- });
13235
- if (BD(installSkills)) {
13236
- openInBrowser(repoUrl);
13237
- fe("Opened in browser");
13238
- return;
13239
- }
13240
- if (installSkills) {
13241
- const cliDir = dirname(fileURLToPath2(import.meta.url));
13242
- const skillsSrc = resolve(cliDir, "..", "..", "skills", "truecourse");
13243
- if (!existsSync(skillsSrc)) {
13244
- v2.warn("Skills directory not found in package \u2014 skipping.");
13245
- } else {
13246
- const skillsDest = resolve(repoPath, ".claude", "skills", "truecourse");
13247
- cpSync(skillsSrc, skillsDest, { recursive: true });
13248
- v2.success("Installed Claude Code skills:");
13249
- v2.message(" - truecourse-analyze (run analysis)");
13250
- v2.message(" - truecourse-list (list violations)");
13251
- v2.message(" - truecourse-fix (apply fixes)");
13252
- }
13253
- }
13254
- openInBrowser(repoUrl);
13255
- fe("Opened in browser");
13301
+ await promptInstallSkills(repoPath);
13302
+ fe(`Open ${repoUrl}`);
13256
13303
  } catch (err) {
13257
13304
  const message = err instanceof Error ? err.message : String(err);
13258
13305
  if (message.includes("ECONNREFUSED") || message.includes("fetch failed")) {
@@ -13270,15 +13317,37 @@ async function runAdd() {
13270
13317
  init_dist2();
13271
13318
  init_helpers();
13272
13319
  var TIMEOUT_MS = 15 * 60 * 1e3;
13273
- async function runAnalyze() {
13320
+ async function runAnalyze({ noAutostart = false } = {}) {
13274
13321
  we("Analyzing repository");
13275
- const firstRun = await ensureServer();
13322
+ if (noAutostart) {
13323
+ const url2 = getServerUrl();
13324
+ try {
13325
+ const res = await fetch(`${url2}/api/health`);
13326
+ if (!res.ok) throw new Error();
13327
+ } catch {
13328
+ v2.error("TrueCourse server is not running. Start it with: npx truecourse start");
13329
+ process.exit(1);
13330
+ }
13331
+ }
13332
+ const firstRun = noAutostart ? false : await ensureServer();
13276
13333
  const repo = await ensureRepo();
13277
13334
  v2.step(`Repository: ${repo.name}`);
13278
13335
  const serverUrl = getServerUrl();
13279
13336
  const socket = connectSocket(repo.id);
13280
13337
  const spinner = L2();
13281
13338
  spinner.start("Starting analysis...");
13339
+ let canceled = false;
13340
+ const onSigint = () => {
13341
+ if (canceled) return;
13342
+ canceled = true;
13343
+ spinner.stop("Cancelling analysis...");
13344
+ fetch(`${serverUrl}/api/repos/${repo.id}/analyze/cancel`, { method: "POST" }).catch(() => {
13345
+ }).finally(() => {
13346
+ socket.disconnect();
13347
+ process.exit(130);
13348
+ });
13349
+ };
13350
+ process.on("SIGINT", onSigint);
13282
13351
  try {
13283
13352
  await new Promise((resolve2, reject) => {
13284
13353
  const timeout = setTimeout(() => {
@@ -13305,6 +13374,10 @@ async function runAnalyze() {
13305
13374
  violationsReady = true;
13306
13375
  checkDone();
13307
13376
  });
13377
+ socket.on("analysis:canceled", () => {
13378
+ clearTimeout(timeout);
13379
+ reject(new Error("CANCELED"));
13380
+ });
13308
13381
  fetch(`${serverUrl}/api/repos/${repo.id}/analyze`, {
13309
13382
  method: "POST",
13310
13383
  headers: { "Content-Type": "application/json" },
@@ -13336,23 +13409,42 @@ async function runAnalyze() {
13336
13409
  }
13337
13410
  const violations = await res.json();
13338
13411
  renderViolationsSummary(violations);
13412
+ const repoUrl = `${serverUrl}/repos/${repo.id}`;
13339
13413
  if (firstRun) {
13340
- const repoUrl = `${serverUrl}/repos/${repo.id}`;
13341
13414
  openInBrowser(repoUrl);
13415
+ fe("Analysis complete \u2014 opened in browser");
13416
+ } else {
13417
+ fe(`Analysis complete \u2014 open ${repoUrl}`);
13342
13418
  }
13343
- fe("Analysis complete");
13344
13419
  } catch (err) {
13345
- spinner.stop("Analysis failed");
13346
13420
  const message = err instanceof Error ? err.message : String(err);
13347
- v2.error(message);
13348
- process.exit(1);
13421
+ if (message === "CANCELED") {
13422
+ spinner.stop("Analysis cancelled");
13423
+ fe("Analysis cancelled");
13424
+ } else {
13425
+ spinner.stop("Analysis failed");
13426
+ v2.error(message);
13427
+ process.exit(1);
13428
+ }
13349
13429
  } finally {
13430
+ process.removeListener("SIGINT", onSigint);
13350
13431
  socket.disconnect();
13351
13432
  }
13352
13433
  }
13353
- async function runAnalyzeDiff() {
13434
+ async function runAnalyzeDiff({ noAutostart = false } = {}) {
13354
13435
  we("Running diff check");
13355
- await ensureServer();
13436
+ if (noAutostart) {
13437
+ const url2 = getServerUrl();
13438
+ try {
13439
+ const res = await fetch(`${url2}/api/health`);
13440
+ if (!res.ok) throw new Error();
13441
+ } catch {
13442
+ v2.error("TrueCourse server is not running. Start it with: npx truecourse start");
13443
+ process.exit(1);
13444
+ }
13445
+ } else {
13446
+ await ensureServer();
13447
+ }
13356
13448
  const repo = await ensureRepo();
13357
13449
  v2.step(`Repository: ${repo.name}`);
13358
13450
  const serverUrl = getServerUrl();
@@ -13604,11 +13696,11 @@ program2.command("start").description("Start TrueCourse services").action(async
13604
13696
  program2.command("add").description("Add the current directory as a repository").action(async () => {
13605
13697
  await runAdd();
13606
13698
  });
13607
- program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").action(async (options) => {
13699
+ program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").option("--no-autostart", "Don't auto-start the server (for use from Claude Code skills)").action(async (options) => {
13608
13700
  if (options.diff) {
13609
- await runAnalyzeDiff();
13701
+ await runAnalyzeDiff({ noAutostart: !options.autostart });
13610
13702
  } else {
13611
- await runAnalyze();
13703
+ await runAnalyze({ noAutostart: !options.autostart });
13612
13704
  }
13613
13705
  });
13614
13706
  program2.command("list").description("List violations from the latest analysis").option("--diff", "Show diff check results (new and resolved)").action(async (options) => {
@@ -0,0 +1,16 @@
1
+ CREATE TABLE "analysis_usage" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "analysis_id" uuid NOT NULL,
4
+ "provider" text NOT NULL,
5
+ "call_type" text NOT NULL,
6
+ "input_tokens" integer DEFAULT 0 NOT NULL,
7
+ "output_tokens" integer DEFAULT 0 NOT NULL,
8
+ "cache_read_tokens" integer DEFAULT 0 NOT NULL,
9
+ "cache_write_tokens" integer DEFAULT 0 NOT NULL,
10
+ "total_tokens" integer DEFAULT 0 NOT NULL,
11
+ "cost_usd" text,
12
+ "duration_ms" integer DEFAULT 0 NOT NULL,
13
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
14
+ );
15
+ --> statement-breakpoint
16
+ ALTER TABLE "analysis_usage" ADD CONSTRAINT "analysis_usage_analysis_id_analyses_id_fk" FOREIGN KEY ("analysis_id") REFERENCES "public"."analyses"("id") ON DELETE cascade ON UPDATE no action;