truecourse 0.1.11 → 0.1.12

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 (2) hide show
  1. package/cli.mjs +939 -871
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -12185,957 +12185,1018 @@ var init_esm_debug3 = __esm({
12185
12185
  }
12186
12186
  });
12187
12187
 
12188
- // tools/cli/src/commands/helpers.ts
12189
- var helpers_exports = {};
12190
- __export(helpers_exports, {
12191
- connectSocket: () => connectSocket,
12192
- ensureRepo: () => ensureRepo,
12193
- ensureServer: () => ensureServer,
12194
- getConfigPath: () => getConfigPath,
12195
- getServerUrl: () => getServerUrl,
12196
- readConfig: () => readConfig,
12197
- renderDiffResults: () => renderDiffResults,
12198
- renderDiffResultsSummary: () => renderDiffResultsSummary,
12199
- renderViolations: () => renderViolations,
12200
- renderViolationsSummary: () => renderViolationsSummary,
12201
- severityColor: () => severityColor,
12202
- severityIcon: () => severityIcon,
12203
- writeConfig: () => writeConfig
12204
- });
12188
+ // tools/cli/src/commands/service/env.ts
12205
12189
  import fs from "node:fs";
12206
- import path from "node:path";
12207
- import os from "node:os";
12208
- function getConfigPath() {
12209
- return path.join(os.homedir(), ".truecourse", "config.json");
12210
- }
12211
- function readConfig() {
12212
- const configPath = getConfigPath();
12213
- try {
12214
- const raw = fs.readFileSync(configPath, "utf-8");
12215
- const parsed = JSON.parse(raw);
12216
- return { ...DEFAULT_CONFIG, ...parsed };
12217
- } catch {
12218
- return { ...DEFAULT_CONFIG };
12190
+ function parseEnvFile(filePath) {
12191
+ const vars = {};
12192
+ if (!fs.existsSync(filePath)) {
12193
+ return vars;
12219
12194
  }
12220
- }
12221
- function writeConfig(partial) {
12222
- const configPath = getConfigPath();
12223
- const dir = path.dirname(configPath);
12224
- fs.mkdirSync(dir, { recursive: true });
12225
- const current = readConfig();
12226
- const merged = { ...current, ...partial };
12227
- fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
12228
- }
12229
- function getServerUrl() {
12230
- const port = process.env.PORT || DEFAULT_PORT;
12231
- return `http://localhost:${port}`;
12232
- }
12233
- async function ensureServer() {
12234
- const url2 = getServerUrl();
12235
- try {
12236
- const res = await fetch(`${url2}/api/health`);
12237
- if (!res.ok) {
12238
- throw new Error(`Server returned ${res.status}`);
12195
+ const content = fs.readFileSync(filePath, "utf-8");
12196
+ for (const line of content.split("\n")) {
12197
+ const trimmed = line.trim();
12198
+ if (!trimmed || trimmed.startsWith("#")) continue;
12199
+ const eqIndex = trimmed.indexOf("=");
12200
+ if (eqIndex === -1) continue;
12201
+ const key = trimmed.slice(0, eqIndex).trim();
12202
+ let value2 = trimmed.slice(eqIndex + 1).trim();
12203
+ if (value2.startsWith('"') && value2.endsWith('"') || value2.startsWith("'") && value2.endsWith("'")) {
12204
+ value2 = value2.slice(1, -1);
12239
12205
  }
12240
- } catch {
12241
- v2.error(
12242
- "Could not connect to TrueCourse server. Is it running?\n Start it with: npx truecourse start"
12243
- );
12244
- process.exit(1);
12245
- }
12246
- }
12247
- async function ensureRepo() {
12248
- const url2 = getServerUrl();
12249
- const repoPath = process.cwd();
12250
- const res = await fetch(`${url2}/api/repos`, {
12251
- method: "POST",
12252
- headers: { "Content-Type": "application/json" },
12253
- body: JSON.stringify({ path: repoPath })
12254
- });
12255
- if (!res.ok) {
12256
- const body = await res.text().catch(() => "");
12257
- let message = `Server returned ${res.status}`;
12258
- try {
12259
- const json = JSON.parse(body);
12260
- if (json.error) message = json.error;
12261
- } catch {
12262
- if (body) message = body;
12206
+ if (key) {
12207
+ vars[key] = value2;
12263
12208
  }
12264
- v2.error(message);
12265
- process.exit(1);
12266
- }
12267
- return await res.json();
12268
- }
12269
- function connectSocket(repoId) {
12270
- const url2 = getServerUrl();
12271
- const socket = lookup(url2, {
12272
- autoConnect: false,
12273
- reconnection: true,
12274
- reconnectionAttempts: 5,
12275
- transports: ["websocket", "polling"]
12276
- });
12277
- socket.connect();
12278
- socket.on("connect", () => {
12279
- socket.emit("joinRepo", repoId);
12280
- });
12281
- if (socket.connected) {
12282
- socket.emit("joinRepo", repoId);
12283
12209
  }
12284
- return socket;
12285
- }
12286
- function severityIcon(severity) {
12287
- const s = severity.toLowerCase();
12288
- if (s === "critical" || s === "high") return "\u2716";
12289
- if (s === "medium") return "\u26A0";
12290
- return "\u2139";
12291
- }
12292
- function severityColor(severity) {
12293
- const s = severity.toLowerCase();
12294
- if (s === "critical") return (t) => `\x1B[91m${t}\x1B[0m`;
12295
- if (s === "high") return (t) => `\x1B[31m${t}\x1B[0m`;
12296
- if (s === "medium") return (t) => `\x1B[33m${t}\x1B[0m`;
12297
- return (t) => `\x1B[36m${t}\x1B[0m`;
12298
- }
12299
- function buildTargetPath(v3) {
12300
- const parts2 = [];
12301
- if (v3.targetServiceName) parts2.push(v3.targetServiceName);
12302
- if (v3.targetDatabaseName) parts2.push(v3.targetDatabaseName);
12303
- if (v3.targetModuleName) parts2.push(v3.targetModuleName);
12304
- if (v3.targetMethodName) parts2.push(v3.targetMethodName);
12305
- if (v3.targetTable) parts2.push(`table: ${v3.targetTable}`);
12306
- return parts2.join(" :: ");
12210
+ return vars;
12307
12211
  }
12308
- function wrapText(text, indent, maxWidth) {
12309
- const words = text.split(/\s+/);
12310
- const lines = [];
12311
- let line = "";
12312
- for (const word of words) {
12313
- if (line && line.length + 1 + word.length > maxWidth) {
12314
- lines.push(line);
12315
- line = word;
12316
- } else {
12317
- line = line ? `${line} ${word}` : word;
12318
- }
12212
+ var init_env = __esm({
12213
+ "tools/cli/src/commands/service/env.ts"() {
12214
+ "use strict";
12319
12215
  }
12320
- if (line) lines.push(line);
12321
- return lines.map((l2, i) => i === 0 ? l2 : `${indent}${l2}`).join("\n");
12216
+ });
12217
+
12218
+ // tools/cli/src/commands/service/macos.ts
12219
+ import fs2 from "node:fs";
12220
+ import path from "node:path";
12221
+ import os from "node:os";
12222
+ import { execSync } from "node:child_process";
12223
+ function escapeXml(s) {
12224
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
12322
12225
  }
12323
- function renderViolations(violations) {
12324
- if (violations.length === 0) {
12325
- v2.info("No violations found. Run `truecourse analyze` first.");
12326
- return;
12327
- }
12328
- console.log("");
12329
- const counts = {};
12330
- for (const v3 of violations) {
12331
- const sev = v3.severity.toLowerCase();
12332
- counts[sev] = (counts[sev] || 0) + 1;
12333
- const icon = severityIcon(v3.severity);
12334
- const color = severityColor(v3.severity);
12335
- const label = v3.severity.toUpperCase();
12336
- const target = buildTargetPath(v3);
12337
- console.log(` ${color(`${icon} ${label}`)} ${v3.title}`);
12338
- if (target) {
12339
- console.log(` ${target}`);
12340
- }
12341
- if (v3.fixPrompt) {
12342
- const indent = " ";
12343
- console.log("");
12344
- console.log(` Fix: ${wrapText(v3.fixPrompt, indent + " ", 60)}`);
12345
- }
12346
- console.log("");
12347
- }
12348
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12349
- const parts2 = [];
12350
- for (const sev of ["critical", "high", "medium", "low", "info"]) {
12351
- if (counts[sev]) parts2.push(`${counts[sev]} ${sev}`);
12226
+ function buildPlist(serverPath, logPath, envVars) {
12227
+ const stdoutPath = path.join(path.dirname(logPath), "truecourse.log");
12228
+ const stderrPath = path.join(path.dirname(logPath), "truecourse.error.log");
12229
+ let envSection = "";
12230
+ if (Object.keys(envVars).length > 0) {
12231
+ const entries = Object.entries(envVars).map(([k3, v3]) => ` <key>${escapeXml(k3)}</key>
12232
+ <string>${escapeXml(v3)}</string>`).join("\n");
12233
+ envSection = `
12234
+ <key>EnvironmentVariables</key>
12235
+ <dict>
12236
+ ${entries}
12237
+ </dict>`;
12352
12238
  }
12353
- console.log(` ${violations.length} violations (${parts2.join(", ")})`);
12354
- console.log("");
12239
+ return `<?xml version="1.0" encoding="UTF-8"?>
12240
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12241
+ <plist version="1.0">
12242
+ <dict>
12243
+ <key>Label</key>
12244
+ <string>${SERVICE_LABEL}</string>
12245
+ <key>ProgramArguments</key>
12246
+ <array>
12247
+ <string>${escapeXml(process.execPath)}</string>
12248
+ <string>${escapeXml(serverPath)}</string>
12249
+ </array>
12250
+ <key>KeepAlive</key>
12251
+ <true/>
12252
+ <key>RunAtLoad</key>
12253
+ <true/>
12254
+ <key>StandardOutPath</key>
12255
+ <string>${escapeXml(stdoutPath)}</string>
12256
+ <key>StandardErrorPath</key>
12257
+ <string>${escapeXml(stderrPath)}</string>${envSection}
12258
+ </dict>
12259
+ </plist>
12260
+ `;
12355
12261
  }
12356
- function renderViolationsSummary(violations) {
12357
- if (violations.length === 0) {
12358
- v2.info("No violations found.");
12359
- return;
12360
- }
12361
- const counts = {};
12362
- for (const v3 of violations) {
12363
- const sev = v3.severity.toLowerCase();
12364
- counts[sev] = (counts[sev] || 0) + 1;
12365
- }
12366
- const parts2 = [];
12367
- for (const sev of ["critical", "high", "medium", "low", "info"]) {
12368
- if (counts[sev]) {
12369
- const color = severityColor(sev);
12370
- parts2.push(color(`${counts[sev]} ${sev}`));
12371
- }
12262
+ var SERVICE_LABEL, PLIST_DIR, PLIST_PATH, MacOSService;
12263
+ var init_macos = __esm({
12264
+ "tools/cli/src/commands/service/macos.ts"() {
12265
+ "use strict";
12266
+ init_env();
12267
+ SERVICE_LABEL = "com.truecourse.server";
12268
+ PLIST_DIR = path.join(os.homedir(), "Library", "LaunchAgents");
12269
+ PLIST_PATH = path.join(PLIST_DIR, `${SERVICE_LABEL}.plist`);
12270
+ MacOSService = class {
12271
+ async install(serverPath, logPath) {
12272
+ const envFile = path.join(os.homedir(), ".truecourse", ".env");
12273
+ const envVars = parseEnvFile(envFile);
12274
+ fs2.mkdirSync(PLIST_DIR, { recursive: true });
12275
+ fs2.mkdirSync(path.dirname(logPath), { recursive: true });
12276
+ const plist = buildPlist(serverPath, logPath, envVars);
12277
+ fs2.writeFileSync(PLIST_PATH, plist, "utf-8");
12278
+ execSync(`launchctl load -w "${PLIST_PATH}"`, { stdio: "pipe" });
12279
+ }
12280
+ async uninstall() {
12281
+ try {
12282
+ execSync(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
12283
+ } catch {
12284
+ }
12285
+ if (fs2.existsSync(PLIST_PATH)) {
12286
+ fs2.unlinkSync(PLIST_PATH);
12287
+ }
12288
+ }
12289
+ async start() {
12290
+ execSync(`launchctl start ${SERVICE_LABEL}`, { stdio: "pipe" });
12291
+ }
12292
+ async stop() {
12293
+ execSync(`launchctl stop ${SERVICE_LABEL}`, { stdio: "pipe" });
12294
+ }
12295
+ async status() {
12296
+ try {
12297
+ const output = execSync(`launchctl list ${SERVICE_LABEL}`, {
12298
+ stdio: ["pipe", "pipe", "pipe"],
12299
+ encoding: "utf-8"
12300
+ });
12301
+ const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
12302
+ if (pidMatch) {
12303
+ return { running: true, pid: parseInt(pidMatch[1], 10) };
12304
+ }
12305
+ if (output.includes('"PID"')) {
12306
+ return { running: true };
12307
+ }
12308
+ return { running: false };
12309
+ } catch {
12310
+ return { running: false };
12311
+ }
12312
+ }
12313
+ async isInstalled() {
12314
+ return fs2.existsSync(PLIST_PATH);
12315
+ }
12316
+ };
12372
12317
  }
12373
- console.log("");
12374
- console.log(` ${violations.length} violations (${parts2.join(", ")})`);
12375
- console.log("");
12376
- v2.info("Run `truecourse list` to see full details.");
12318
+ });
12319
+
12320
+ // tools/cli/src/commands/service/linux.ts
12321
+ import fs3 from "node:fs";
12322
+ import path2 from "node:path";
12323
+ import os2 from "node:os";
12324
+ import { execSync as execSync2 } from "node:child_process";
12325
+ function buildUnitFile(serverPath, logPath) {
12326
+ const envFile = path2.join(os2.homedir(), ".truecourse", ".env");
12327
+ const logDir = path2.dirname(logPath);
12328
+ return `[Unit]
12329
+ Description=TrueCourse Server
12330
+ After=network.target
12331
+
12332
+ [Service]
12333
+ Type=simple
12334
+ ExecStart=${process.execPath} ${serverPath}
12335
+ Restart=on-failure
12336
+ RestartSec=5
12337
+ EnvironmentFile=${envFile}
12338
+ StandardOutput=append:${path2.join(logDir, "truecourse.log")}
12339
+ StandardError=append:${path2.join(logDir, "truecourse.error.log")}
12340
+
12341
+ [Install]
12342
+ WantedBy=default.target
12343
+ `;
12377
12344
  }
12378
- function renderDiffResults(result) {
12379
- console.log("");
12380
- const modified = result.changedFiles.filter((f2) => f2.status === "modified").length;
12381
- const newFiles = result.changedFiles.filter((f2) => f2.status === "new").length;
12382
- const deleted = result.changedFiles.filter((f2) => f2.status === "deleted").length;
12383
- const fileParts = [];
12384
- if (modified) fileParts.push(`${modified} modified`);
12385
- if (newFiles) fileParts.push(`${newFiles} new`);
12386
- if (deleted) fileParts.push(`${deleted} deleted`);
12387
- console.log(` Changed files: ${result.changedFiles.length} (${fileParts.join(", ")})`);
12388
- console.log("");
12389
- if (result.isStale) {
12390
- console.log(` \x1B[33m\u26A0 Results may be stale \u2014 baseline analysis has changed.\x1B[0m`);
12391
- console.log("");
12392
- }
12393
- if (result.newViolations.length > 0) {
12394
- console.log(` NEW ISSUES (${result.newViolations.length})`);
12395
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12396
- for (const v3 of result.newViolations) {
12397
- const icon = severityIcon(v3.severity);
12398
- const color = severityColor(v3.severity);
12399
- const label = v3.severity.toUpperCase();
12400
- const target = buildTargetPath(v3);
12401
- console.log(` ${color(`${icon} ${label}`)} ${v3.title}`);
12402
- if (target) {
12403
- console.log(` ${target}`);
12345
+ var SERVICE_NAME, UNIT_DIR, UNIT_PATH, LinuxService;
12346
+ var init_linux = __esm({
12347
+ "tools/cli/src/commands/service/linux.ts"() {
12348
+ "use strict";
12349
+ SERVICE_NAME = "truecourse";
12350
+ UNIT_DIR = path2.join(os2.homedir(), ".config", "systemd", "user");
12351
+ UNIT_PATH = path2.join(UNIT_DIR, `${SERVICE_NAME}.service`);
12352
+ LinuxService = class {
12353
+ async install(serverPath, logPath) {
12354
+ fs3.mkdirSync(UNIT_DIR, { recursive: true });
12355
+ fs3.mkdirSync(path2.dirname(logPath), { recursive: true });
12356
+ const unit = buildUnitFile(serverPath, logPath);
12357
+ fs3.writeFileSync(UNIT_PATH, unit, "utf-8");
12358
+ execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
12359
+ execSync2(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "pipe" });
12360
+ }
12361
+ async uninstall() {
12362
+ try {
12363
+ execSync2(`systemctl --user stop ${SERVICE_NAME}`, { stdio: "pipe" });
12364
+ } catch {
12365
+ }
12366
+ try {
12367
+ execSync2(`systemctl --user disable ${SERVICE_NAME}`, { stdio: "pipe" });
12368
+ } catch {
12369
+ }
12370
+ if (fs3.existsSync(UNIT_PATH)) {
12371
+ fs3.unlinkSync(UNIT_PATH);
12372
+ }
12373
+ try {
12374
+ execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
12375
+ } catch {
12376
+ }
12404
12377
  }
12405
- if (v3.fixPrompt) {
12406
- const indent = " ";
12407
- console.log("");
12408
- console.log(` Fix: ${wrapText(v3.fixPrompt, indent + " ", 60)}`);
12378
+ async start() {
12379
+ execSync2(`systemctl --user start ${SERVICE_NAME}`, { stdio: "pipe" });
12409
12380
  }
12410
- console.log("");
12411
- }
12412
- } else {
12413
- console.log(" NEW ISSUES (0)");
12414
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12415
- console.log(" None");
12416
- console.log("");
12417
- }
12418
- if (result.resolvedViolations.length > 0) {
12419
- console.log(` RESOLVED (${result.resolvedViolations.length})`);
12420
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12421
- for (const v3 of result.resolvedViolations) {
12422
- const target = buildTargetPath(v3);
12423
- const color = severityColor(v3.severity);
12424
- const label = v3.severity.toUpperCase();
12425
- console.log(` ${color(`\u2714 ${label}`)} ${v3.title}`);
12426
- if (target) {
12427
- console.log(` ${target}`);
12381
+ async stop() {
12382
+ execSync2(`systemctl --user stop ${SERVICE_NAME}`, { stdio: "pipe" });
12428
12383
  }
12429
- console.log("");
12430
- }
12431
- } else {
12432
- console.log(" RESOLVED (0)");
12433
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12434
- console.log(" None");
12435
- console.log("");
12384
+ async status() {
12385
+ try {
12386
+ const output = execSync2(
12387
+ `systemctl --user show ${SERVICE_NAME} --property=ActiveState,MainPID`,
12388
+ { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" }
12389
+ );
12390
+ const activeMatch = output.match(/ActiveState=(\w+)/);
12391
+ const pidMatch = output.match(/MainPID=(\d+)/);
12392
+ const isActive = activeMatch?.[1] === "active";
12393
+ const pid = pidMatch ? parseInt(pidMatch[1], 10) : void 0;
12394
+ return { running: isActive, pid: isActive && pid ? pid : void 0 };
12395
+ } catch {
12396
+ return { running: false };
12397
+ }
12398
+ }
12399
+ async isInstalled() {
12400
+ return fs3.existsSync(UNIT_PATH);
12401
+ }
12402
+ };
12436
12403
  }
12437
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12438
- console.log(` Summary: ${result.summary.newCount} new issues, ${result.summary.resolvedCount} resolved`);
12439
- console.log("");
12440
- }
12441
- function renderDiffResultsSummary(result) {
12442
- console.log("");
12443
- const modified = result.changedFiles.filter((f2) => f2.status === "modified").length;
12444
- const newFiles = result.changedFiles.filter((f2) => f2.status === "new").length;
12445
- const deleted = result.changedFiles.filter((f2) => f2.status === "deleted").length;
12446
- const fileParts = [];
12447
- if (modified) fileParts.push(`${modified} modified`);
12448
- if (newFiles) fileParts.push(`${newFiles} new`);
12449
- if (deleted) fileParts.push(`${deleted} deleted`);
12450
- console.log(` Changed files: ${result.changedFiles.length} (${fileParts.join(", ")})`);
12451
- console.log("");
12452
- if (result.isStale) {
12453
- console.log(` \x1B[33m\u26A0 Results may be stale \u2014 baseline analysis has changed.\x1B[0m`);
12454
- console.log("");
12404
+ });
12405
+
12406
+ // tools/cli/src/commands/service/windows.ts
12407
+ import { execSync as execSync3 } from "node:child_process";
12408
+ var SERVICE_NAME2, WindowsService;
12409
+ var init_windows = __esm({
12410
+ "tools/cli/src/commands/service/windows.ts"() {
12411
+ "use strict";
12412
+ SERVICE_NAME2 = "TrueCourse";
12413
+ WindowsService = class {
12414
+ svc;
12415
+ async getNodeWindows() {
12416
+ try {
12417
+ return __require("node-windows");
12418
+ } catch {
12419
+ throw new Error(
12420
+ "node-windows is required for background service on Windows.\nInstall it with: npm install -g node-windows"
12421
+ );
12422
+ }
12423
+ }
12424
+ async install(serverPath, logPath) {
12425
+ const nw = await this.getNodeWindows();
12426
+ const { Service } = nw;
12427
+ return new Promise((resolve2, reject) => {
12428
+ const svc = new Service({
12429
+ name: SERVICE_NAME2,
12430
+ description: "TrueCourse Server",
12431
+ script: serverPath,
12432
+ nodeOptions: [],
12433
+ env: [{
12434
+ name: "TRUECOURSE_LOG_DIR",
12435
+ value: logPath
12436
+ }]
12437
+ });
12438
+ svc.on("install", () => {
12439
+ svc.start();
12440
+ resolve2();
12441
+ });
12442
+ svc.on("error", (err) => reject(err));
12443
+ svc.install();
12444
+ });
12445
+ }
12446
+ async uninstall() {
12447
+ const nw = await this.getNodeWindows();
12448
+ const { Service } = nw;
12449
+ return new Promise((resolve2, reject) => {
12450
+ const svc = new Service({
12451
+ name: SERVICE_NAME2,
12452
+ script: ""
12453
+ // Not needed for uninstall
12454
+ });
12455
+ svc.on("uninstall", () => resolve2());
12456
+ svc.on("error", (err) => reject(err));
12457
+ svc.uninstall();
12458
+ });
12459
+ }
12460
+ async start() {
12461
+ execSync3(`sc.exe start ${SERVICE_NAME2}`, { stdio: "pipe" });
12462
+ }
12463
+ async stop() {
12464
+ execSync3(`sc.exe stop ${SERVICE_NAME2}`, { stdio: "pipe" });
12465
+ }
12466
+ async status() {
12467
+ try {
12468
+ const output = execSync3(`sc.exe query ${SERVICE_NAME2}`, {
12469
+ stdio: ["pipe", "pipe", "pipe"],
12470
+ encoding: "utf-8"
12471
+ });
12472
+ const running = output.includes("RUNNING");
12473
+ const pidMatch = output.match(/PID\s*:\s*(\d+)/);
12474
+ return {
12475
+ running,
12476
+ pid: pidMatch ? parseInt(pidMatch[1], 10) : void 0
12477
+ };
12478
+ } catch {
12479
+ return { running: false };
12480
+ }
12481
+ }
12482
+ async isInstalled() {
12483
+ try {
12484
+ execSync3(`sc.exe query ${SERVICE_NAME2}`, { stdio: "pipe" });
12485
+ return true;
12486
+ } catch {
12487
+ return false;
12488
+ }
12489
+ }
12490
+ };
12491
+ }
12492
+ });
12493
+
12494
+ // tools/cli/src/commands/service/platform.ts
12495
+ function getPlatform() {
12496
+ switch (process.platform) {
12497
+ case "darwin":
12498
+ return new MacOSService();
12499
+ case "linux":
12500
+ return new LinuxService();
12501
+ case "win32":
12502
+ return new WindowsService();
12503
+ default:
12504
+ throw new Error(
12505
+ `Unsupported platform: ${process.platform}. Background service mode supports macOS, Linux, and Windows.`
12506
+ );
12455
12507
  }
12456
- console.log(` Summary: ${result.summary.newCount} new issues, ${result.summary.resolvedCount} resolved`);
12457
- console.log("");
12458
- v2.info("Run `truecourse list --diff` to see full details.");
12459
12508
  }
12460
- var DEFAULT_PORT, DEFAULT_CONFIG;
12461
- var init_helpers = __esm({
12462
- "tools/cli/src/commands/helpers.ts"() {
12509
+ var init_platform = __esm({
12510
+ "tools/cli/src/commands/service/platform.ts"() {
12463
12511
  "use strict";
12464
- init_dist2();
12465
- init_esm_debug3();
12466
- DEFAULT_PORT = 3001;
12467
- DEFAULT_CONFIG = { runMode: "console" };
12512
+ init_macos();
12513
+ init_linux();
12514
+ init_windows();
12468
12515
  }
12469
12516
  });
12470
12517
 
12471
- // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
12472
- var import_index = __toESM(require_commander(), 1);
12473
- var {
12474
- program,
12475
- createCommand,
12476
- createArgument,
12477
- createOption,
12478
- CommanderError,
12479
- InvalidArgumentError,
12480
- InvalidOptionArgumentError,
12481
- // deprecated old name
12482
- Command,
12483
- Argument,
12484
- Option,
12485
- Help
12486
- } = import_index.default;
12487
-
12488
- // tools/cli/src/index.ts
12489
- init_dist2();
12490
- import fs7 from "node:fs";
12491
- import path8 from "node:path";
12492
- import os6 from "node:os";
12493
-
12494
- // tools/cli/src/commands/setup.ts
12495
- init_dist2();
12496
- import fs2 from "node:fs";
12497
- import path2 from "node:path";
12498
- import os2 from "node:os";
12499
- var DEFAULT_MODELS = {
12500
- anthropic: "claude-sonnet-4-20250514",
12501
- openai: "gpt-5.3-codex"
12502
- };
12503
- function buildEnvContents(config) {
12504
- const lines = [
12505
- "# TrueCourse Environment Configuration",
12506
- `# Generated by truecourse setup on ${(/* @__PURE__ */ new Date()).toISOString()}`,
12507
- ""
12508
- ];
12509
- if (config.provider) {
12510
- lines.push(`LLM_PROVIDER=${config.provider}`);
12511
- }
12512
- if (config.model) {
12513
- lines.push(`LLM_MODEL=${config.model}`);
12514
- }
12515
- if (config.anthropicKey) {
12516
- lines.push(`ANTHROPIC_API_KEY=${config.anthropicKey}`);
12517
- }
12518
- if (config.openaiKey) {
12519
- lines.push(`OPENAI_API_KEY=${config.openaiKey}`);
12520
- }
12521
- if (config.langfusePublicKey && config.langfuseSecretKey) {
12522
- lines.push("");
12523
- lines.push("# Langfuse Tracing");
12524
- lines.push(`LANGFUSE_PUBLIC_KEY=${config.langfusePublicKey}`);
12525
- lines.push(`LANGFUSE_SECRET_KEY=${config.langfuseSecretKey}`);
12526
- }
12527
- lines.push("");
12528
- return lines.join("\n");
12518
+ // tools/cli/src/commands/service/logs.ts
12519
+ import fs4 from "node:fs";
12520
+ import path3 from "node:path";
12521
+ import os3 from "node:os";
12522
+ import { spawn } from "node:child_process";
12523
+ function getLogDir() {
12524
+ return path3.join(os3.homedir(), ".truecourse", "logs");
12529
12525
  }
12530
- async function runSetup() {
12531
- we("Welcome to TrueCourse");
12532
- const provider = await de({
12533
- message: "Which LLM provider would you like to use?",
12534
- options: [
12535
- { value: "anthropic", label: "Anthropic (Claude)" },
12536
- { value: "openai", label: "OpenAI (GPT)" },
12537
- { value: "skip", label: "Skip for now" }
12538
- ]
12539
- });
12540
- if (BD(provider)) {
12541
- ve("Setup cancelled.");
12542
- process.exit(0);
12543
- }
12544
- const config = {};
12545
- if (provider === "anthropic" || provider === "openai") {
12546
- config.provider = provider;
12547
- }
12548
- if (provider === "anthropic") {
12549
- const anthropicKey = await ue({
12550
- message: "Enter your Anthropic API key:",
12551
- placeholder: "sk-ant-...",
12552
- validate(value2) {
12553
- if (!value2 || value2.trim().length === 0) {
12554
- return "API key is required";
12555
- }
12556
- }
12557
- });
12558
- if (BD(anthropicKey)) {
12559
- ve("Setup cancelled.");
12560
- process.exit(0);
12561
- }
12562
- config.anthropicKey = anthropicKey;
12563
- }
12564
- if (provider === "openai") {
12565
- const openaiKey = await ue({
12566
- message: "Enter your OpenAI API key:",
12567
- placeholder: "sk-...",
12568
- validate(value2) {
12569
- if (!value2 || value2.trim().length === 0) {
12570
- return "API key is required";
12571
- }
12572
- }
12573
- });
12574
- if (BD(openaiKey)) {
12575
- ve("Setup cancelled.");
12576
- process.exit(0);
12526
+ function getLogPath() {
12527
+ return path3.join(getLogDir(), "truecourse.log");
12528
+ }
12529
+ function rotateLogs(logDir) {
12530
+ const logFile = path3.join(logDir, "truecourse.log");
12531
+ if (!fs4.existsSync(logFile)) return;
12532
+ const stats = fs4.statSync(logFile);
12533
+ if (stats.size < MAX_LOG_SIZE) return;
12534
+ for (let i = MAX_LOG_FILES; i >= 1; i--) {
12535
+ const older = path3.join(logDir, `truecourse.log.${i}`);
12536
+ if (i === MAX_LOG_FILES) {
12537
+ if (fs4.existsSync(older)) fs4.unlinkSync(older);
12538
+ } else {
12539
+ const newer = path3.join(logDir, `truecourse.log.${i + 1}`);
12540
+ if (fs4.existsSync(older)) fs4.renameSync(older, newer);
12577
12541
  }
12578
- config.openaiKey = openaiKey;
12579
12542
  }
12580
- if (provider === "anthropic" || provider === "openai") {
12581
- const defaultModel = DEFAULT_MODELS[provider];
12582
- const model = await ue({
12583
- message: `Enter the model to use (default: ${defaultModel}):`,
12584
- placeholder: defaultModel
12585
- });
12586
- if (BD(model)) {
12587
- ve("Setup cancelled.");
12588
- process.exit(0);
12543
+ fs4.renameSync(logFile, path3.join(logDir, "truecourse.log.1"));
12544
+ }
12545
+ function rotateErrorLogs(logDir) {
12546
+ const logFile = path3.join(logDir, "truecourse.error.log");
12547
+ if (!fs4.existsSync(logFile)) return;
12548
+ const stats = fs4.statSync(logFile);
12549
+ if (stats.size < MAX_LOG_SIZE) return;
12550
+ for (let i = MAX_LOG_FILES; i >= 1; i--) {
12551
+ const older = path3.join(logDir, `truecourse.error.log.${i}`);
12552
+ if (i === MAX_LOG_FILES) {
12553
+ if (fs4.existsSync(older)) fs4.unlinkSync(older);
12554
+ } else {
12555
+ const newer = path3.join(logDir, `truecourse.error.log.${i + 1}`);
12556
+ if (fs4.existsSync(older)) fs4.renameSync(older, newer);
12589
12557
  }
12590
- config.model = model?.trim() || defaultModel;
12591
12558
  }
12592
- const useLangfuse = await me({
12593
- message: "Would you like to enable Langfuse tracing?",
12594
- initialValue: false
12595
- });
12596
- if (BD(useLangfuse)) {
12597
- ve("Setup cancelled.");
12598
- process.exit(0);
12559
+ fs4.renameSync(logFile, path3.join(logDir, "truecourse.error.log.1"));
12560
+ }
12561
+ function tailLogs() {
12562
+ const logFile = getLogPath();
12563
+ if (!fs4.existsSync(logFile)) {
12564
+ console.log("No log file found. Is the service running?");
12565
+ console.log(`Expected at: ${logFile}`);
12566
+ return;
12599
12567
  }
12600
- if (useLangfuse) {
12601
- const langfusePublicKey = await ue({
12602
- message: "Enter your Langfuse public key:",
12603
- placeholder: "pk-lf-...",
12604
- validate(value2) {
12605
- if (!value2 || value2.trim().length === 0) {
12606
- return "Public key is required";
12607
- }
12608
- }
12609
- });
12610
- if (BD(langfusePublicKey)) {
12611
- ve("Setup cancelled.");
12612
- process.exit(0);
12568
+ if (process.platform === "win32") {
12569
+ const content = fs4.readFileSync(logFile, "utf-8");
12570
+ const lines = content.split("\n");
12571
+ const tail = lines.slice(-50);
12572
+ for (const line of tail) {
12573
+ process.stdout.write(line + "\n");
12613
12574
  }
12614
- const langfuseSecretKey = await ue({
12615
- message: "Enter your Langfuse secret key:",
12616
- placeholder: "sk-lf-...",
12617
- validate(value2) {
12618
- if (!value2 || value2.trim().length === 0) {
12619
- return "Secret key is required";
12620
- }
12575
+ let lastSize = fs4.statSync(logFile).size;
12576
+ fs4.watchFile(logFile, { interval: 500 }, () => {
12577
+ const newSize = fs4.statSync(logFile).size;
12578
+ if (newSize > lastSize) {
12579
+ const fd = fs4.openSync(logFile, "r");
12580
+ const buf = Buffer.alloc(newSize - lastSize);
12581
+ fs4.readSync(fd, buf, 0, buf.length, lastSize);
12582
+ fs4.closeSync(fd);
12583
+ process.stdout.write(buf.toString("utf-8"));
12584
+ lastSize = newSize;
12621
12585
  }
12622
12586
  });
12623
- if (BD(langfuseSecretKey)) {
12624
- ve("Setup cancelled.");
12587
+ } else {
12588
+ const tail = spawn("tail", ["-f", "-n", "50", logFile], {
12589
+ stdio: "inherit"
12590
+ });
12591
+ const cleanup = () => {
12592
+ tail.kill("SIGTERM");
12593
+ };
12594
+ process.on("SIGINT", cleanup);
12595
+ process.on("SIGTERM", cleanup);
12596
+ tail.on("close", () => {
12625
12597
  process.exit(0);
12626
- }
12627
- config.langfusePublicKey = langfusePublicKey;
12628
- config.langfuseSecretKey = langfuseSecretKey;
12598
+ });
12629
12599
  }
12630
- const configDir = path2.join(os2.homedir(), ".truecourse");
12631
- fs2.mkdirSync(configDir, { recursive: true });
12632
- const envPath = path2.join(configDir, ".env");
12633
- fs2.writeFileSync(envPath, buildEnvContents(config), "utf-8");
12634
- v2.success(`Configuration saved to ${envPath}`);
12635
- const runMode = await de({
12636
- message: "How would you like to run TrueCourse?",
12637
- options: [
12638
- { value: "console", label: "Console (keep terminal open)" },
12639
- { value: "service", label: "Background service (runs automatically, no terminal needed)" }
12640
- ]
12641
- });
12642
- if (BD(runMode)) {
12643
- ve("Setup cancelled.");
12644
- process.exit(0);
12600
+ }
12601
+ var MAX_LOG_SIZE, MAX_LOG_FILES;
12602
+ var init_logs = __esm({
12603
+ "tools/cli/src/commands/service/logs.ts"() {
12604
+ "use strict";
12605
+ MAX_LOG_SIZE = 10 * 1024 * 1024;
12606
+ MAX_LOG_FILES = 5;
12645
12607
  }
12646
- const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_helpers(), helpers_exports));
12647
- writeConfig2({ runMode });
12648
- if (runMode === "service") {
12649
- v2.info("Background service selected. Run `truecourse start` to install and start the service.");
12608
+ });
12609
+
12610
+ // tools/cli/src/commands/start.ts
12611
+ var start_exports = {};
12612
+ __export(start_exports, {
12613
+ runStart: () => runStart
12614
+ });
12615
+ import { spawn as spawn2 } from "node:child_process";
12616
+ import path4 from "node:path";
12617
+ import { fileURLToPath } from "node:url";
12618
+ function getServerPath() {
12619
+ return path4.join(__dirname, "server.mjs");
12620
+ }
12621
+ async function healthcheck() {
12622
+ const url2 = getServerUrl();
12623
+ for (let i = 0; i < 30; i++) {
12624
+ try {
12625
+ const res = await fetch(`${url2}/api/health`);
12626
+ if (res.ok) return true;
12627
+ } catch {
12628
+ }
12629
+ await new Promise((r2) => setTimeout(r2, 500));
12650
12630
  }
12651
- v2.info("Embedded PostgreSQL will start automatically when the server runs.");
12652
- v2.info("Database migrations are applied on server startup.");
12653
- fe("Setup complete!");
12631
+ return false;
12654
12632
  }
12655
-
12656
- // tools/cli/src/commands/start.ts
12657
- init_dist2();
12658
- init_helpers();
12659
- import { spawn as spawn2 } from "node:child_process";
12660
- import path6 from "node:path";
12661
- import { fileURLToPath } from "node:url";
12662
-
12663
- // tools/cli/src/commands/service/macos.ts
12664
- import fs4 from "node:fs";
12665
- import path3 from "node:path";
12666
- import os3 from "node:os";
12667
- import { execSync } from "node:child_process";
12668
-
12669
- // tools/cli/src/commands/service/env.ts
12670
- import fs3 from "node:fs";
12671
- function parseEnvFile(filePath) {
12672
- const vars = {};
12673
- if (!fs3.existsSync(filePath)) {
12674
- return vars;
12633
+ async function startServiceMode(openBrowser) {
12634
+ const platform = getPlatform();
12635
+ const serverPath = getServerPath();
12636
+ const logDir = getLogDir();
12637
+ const logPath = getLogPath();
12638
+ const url2 = getServerUrl();
12639
+ const { running } = await platform.status();
12640
+ if (running) {
12641
+ v2.info(`TrueCourse is already running at ${url2}`);
12642
+ return;
12675
12643
  }
12676
- const content = fs3.readFileSync(filePath, "utf-8");
12677
- for (const line of content.split("\n")) {
12678
- const trimmed = line.trim();
12679
- if (!trimmed || trimmed.startsWith("#")) continue;
12680
- const eqIndex = trimmed.indexOf("=");
12681
- if (eqIndex === -1) continue;
12682
- const key = trimmed.slice(0, eqIndex).trim();
12683
- let value2 = trimmed.slice(eqIndex + 1).trim();
12684
- if (value2.startsWith('"') && value2.endsWith('"') || value2.startsWith("'") && value2.endsWith("'")) {
12685
- value2 = value2.slice(1, -1);
12644
+ const installed = await platform.isInstalled();
12645
+ if (installed) {
12646
+ rotateLogs(logDir);
12647
+ rotateErrorLogs(logDir);
12648
+ v2.step("Starting background service...");
12649
+ await platform.start();
12650
+ } else {
12651
+ rotateLogs(logDir);
12652
+ rotateErrorLogs(logDir);
12653
+ v2.step("Installing and starting background service...");
12654
+ await platform.install(serverPath, logPath);
12655
+ }
12656
+ const healthy = await healthcheck();
12657
+ if (healthy) {
12658
+ v2.success(`TrueCourse is running at ${url2}`);
12659
+ if (openBrowser) openInBrowser(url2);
12660
+ } else {
12661
+ v2.warn("Service started but server hasn't responded yet.");
12662
+ v2.info("Check logs with: truecourse service logs");
12663
+ }
12664
+ }
12665
+ function startConsoleMode() {
12666
+ const serverPath = getServerPath();
12667
+ v2.step("Starting server (embedded PostgreSQL starts automatically)...");
12668
+ const serverProcess = spawn2(
12669
+ process.execPath,
12670
+ [serverPath],
12671
+ {
12672
+ stdio: "inherit",
12673
+ env: { ...process.env }
12686
12674
  }
12687
- if (key) {
12688
- vars[key] = value2;
12675
+ );
12676
+ serverProcess.on("error", (error) => {
12677
+ v2.error(`Failed to start server: ${error.message}`);
12678
+ process.exit(1);
12679
+ });
12680
+ serverProcess.on("close", (code) => {
12681
+ if (code !== null && code !== 0) {
12682
+ process.exit(code);
12683
+ }
12684
+ });
12685
+ const cleanup = () => {
12686
+ serverProcess.kill("SIGTERM");
12687
+ };
12688
+ process.on("SIGINT", cleanup);
12689
+ process.on("SIGTERM", cleanup);
12690
+ }
12691
+ async function runStart({ openBrowser = true } = {}) {
12692
+ we("Starting TrueCourse");
12693
+ const config = readConfig();
12694
+ if (config.runMode === "service") {
12695
+ try {
12696
+ await startServiceMode(openBrowser);
12697
+ } catch (error) {
12698
+ v2.error(`Service mode failed: ${error.message}`);
12699
+ v2.info("Falling back to console mode. Reconfigure with: truecourse setup");
12700
+ startConsoleMode();
12689
12701
  }
12702
+ } else {
12703
+ startConsoleMode();
12690
12704
  }
12691
- return vars;
12692
12705
  }
12706
+ var __dirname;
12707
+ var init_start = __esm({
12708
+ "tools/cli/src/commands/start.ts"() {
12709
+ "use strict";
12710
+ init_dist2();
12711
+ init_helpers();
12712
+ init_platform();
12713
+ init_logs();
12714
+ __dirname = path4.dirname(fileURLToPath(import.meta.url));
12715
+ }
12716
+ });
12693
12717
 
12694
- // tools/cli/src/commands/service/macos.ts
12695
- var SERVICE_LABEL = "com.truecourse.server";
12696
- var PLIST_DIR = path3.join(os3.homedir(), "Library", "LaunchAgents");
12697
- var PLIST_PATH = path3.join(PLIST_DIR, `${SERVICE_LABEL}.plist`);
12698
- function escapeXml(s) {
12699
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
12718
+ // tools/cli/src/commands/helpers.ts
12719
+ var helpers_exports = {};
12720
+ __export(helpers_exports, {
12721
+ connectSocket: () => connectSocket,
12722
+ ensureRepo: () => ensureRepo,
12723
+ ensureServer: () => ensureServer,
12724
+ getConfigPath: () => getConfigPath,
12725
+ getServerUrl: () => getServerUrl,
12726
+ openInBrowser: () => openInBrowser,
12727
+ readConfig: () => readConfig,
12728
+ renderDiffResults: () => renderDiffResults,
12729
+ renderDiffResultsSummary: () => renderDiffResultsSummary,
12730
+ renderViolations: () => renderViolations,
12731
+ renderViolationsSummary: () => renderViolationsSummary,
12732
+ severityColor: () => severityColor,
12733
+ severityIcon: () => severityIcon,
12734
+ writeConfig: () => writeConfig
12735
+ });
12736
+ import { exec } from "node:child_process";
12737
+ import fs5 from "node:fs";
12738
+ import path5 from "node:path";
12739
+ import os4 from "node:os";
12740
+ function getConfigPath() {
12741
+ return path5.join(os4.homedir(), ".truecourse", "config.json");
12700
12742
  }
12701
- function buildPlist(serverPath, logPath, envVars) {
12702
- const stdoutPath = path3.join(path3.dirname(logPath), "truecourse.log");
12703
- const stderrPath = path3.join(path3.dirname(logPath), "truecourse.error.log");
12704
- let envSection = "";
12705
- if (Object.keys(envVars).length > 0) {
12706
- const entries = Object.entries(envVars).map(([k3, v3]) => ` <key>${escapeXml(k3)}</key>
12707
- <string>${escapeXml(v3)}</string>`).join("\n");
12708
- envSection = `
12709
- <key>EnvironmentVariables</key>
12710
- <dict>
12711
- ${entries}
12712
- </dict>`;
12743
+ function readConfig() {
12744
+ const configPath = getConfigPath();
12745
+ try {
12746
+ const raw = fs5.readFileSync(configPath, "utf-8");
12747
+ const parsed = JSON.parse(raw);
12748
+ return { ...DEFAULT_CONFIG, ...parsed };
12749
+ } catch {
12750
+ return { ...DEFAULT_CONFIG };
12713
12751
  }
12714
- return `<?xml version="1.0" encoding="UTF-8"?>
12715
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12716
- <plist version="1.0">
12717
- <dict>
12718
- <key>Label</key>
12719
- <string>${SERVICE_LABEL}</string>
12720
- <key>ProgramArguments</key>
12721
- <array>
12722
- <string>${escapeXml(process.execPath)}</string>
12723
- <string>${escapeXml(serverPath)}</string>
12724
- </array>
12725
- <key>KeepAlive</key>
12726
- <true/>
12727
- <key>RunAtLoad</key>
12728
- <true/>
12729
- <key>StandardOutPath</key>
12730
- <string>${escapeXml(stdoutPath)}</string>
12731
- <key>StandardErrorPath</key>
12732
- <string>${escapeXml(stderrPath)}</string>${envSection}
12733
- </dict>
12734
- </plist>
12735
- `;
12736
12752
  }
12737
- var MacOSService = class {
12738
- async install(serverPath, logPath) {
12739
- const envFile = path3.join(os3.homedir(), ".truecourse", ".env");
12740
- const envVars = parseEnvFile(envFile);
12741
- fs4.mkdirSync(PLIST_DIR, { recursive: true });
12742
- fs4.mkdirSync(path3.dirname(logPath), { recursive: true });
12743
- const plist = buildPlist(serverPath, logPath, envVars);
12744
- fs4.writeFileSync(PLIST_PATH, plist, "utf-8");
12745
- execSync(`launchctl load -w "${PLIST_PATH}"`, { stdio: "pipe" });
12746
- }
12747
- async uninstall() {
12753
+ function writeConfig(partial) {
12754
+ const configPath = getConfigPath();
12755
+ const dir = path5.dirname(configPath);
12756
+ fs5.mkdirSync(dir, { recursive: true });
12757
+ const current = readConfig();
12758
+ const merged = { ...current, ...partial };
12759
+ fs5.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
12760
+ }
12761
+ function getServerUrl() {
12762
+ const port = process.env.PORT || DEFAULT_PORT;
12763
+ return `http://localhost:${port}`;
12764
+ }
12765
+ async function ensureServer() {
12766
+ const url2 = getServerUrl();
12767
+ try {
12768
+ const res = await fetch(`${url2}/api/health`);
12769
+ if (!res.ok) throw new Error(`Server returned ${res.status}`);
12770
+ return false;
12771
+ } catch {
12772
+ const { runStart: runStart2 } = await Promise.resolve().then(() => (init_start(), start_exports));
12773
+ await runStart2({ openBrowser: false });
12748
12774
  try {
12749
- execSync(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
12775
+ const res = await fetch(`${url2}/api/health`);
12776
+ if (!res.ok) throw new Error();
12750
12777
  } catch {
12778
+ v2.error("Server failed to start. Check logs with: truecourse service logs");
12779
+ process.exit(1);
12751
12780
  }
12752
- if (fs4.existsSync(PLIST_PATH)) {
12753
- fs4.unlinkSync(PLIST_PATH);
12754
- }
12755
- }
12756
- async start() {
12757
- execSync(`launchctl start ${SERVICE_LABEL}`, { stdio: "pipe" });
12758
- }
12759
- async stop() {
12760
- execSync(`launchctl stop ${SERVICE_LABEL}`, { stdio: "pipe" });
12781
+ return true;
12761
12782
  }
12762
- async status() {
12783
+ }
12784
+ async function ensureRepo() {
12785
+ const url2 = getServerUrl();
12786
+ const repoPath = process.cwd();
12787
+ const res = await fetch(`${url2}/api/repos`, {
12788
+ method: "POST",
12789
+ headers: { "Content-Type": "application/json" },
12790
+ body: JSON.stringify({ path: repoPath })
12791
+ });
12792
+ if (!res.ok) {
12793
+ const body = await res.text().catch(() => "");
12794
+ let message = `Server returned ${res.status}`;
12763
12795
  try {
12764
- const output = execSync(`launchctl list ${SERVICE_LABEL}`, {
12765
- stdio: ["pipe", "pipe", "pipe"],
12766
- encoding: "utf-8"
12767
- });
12768
- const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
12769
- if (pidMatch) {
12770
- return { running: true, pid: parseInt(pidMatch[1], 10) };
12771
- }
12772
- if (output.includes('"PID"')) {
12773
- return { running: true };
12774
- }
12775
- return { running: false };
12796
+ const json = JSON.parse(body);
12797
+ if (json.error) message = json.error;
12776
12798
  } catch {
12777
- return { running: false };
12799
+ if (body) message = body;
12800
+ }
12801
+ v2.error(message);
12802
+ process.exit(1);
12803
+ }
12804
+ return await res.json();
12805
+ }
12806
+ function connectSocket(repoId) {
12807
+ const url2 = getServerUrl();
12808
+ const socket = lookup(url2, {
12809
+ autoConnect: false,
12810
+ reconnection: true,
12811
+ reconnectionAttempts: 5,
12812
+ transports: ["websocket", "polling"]
12813
+ });
12814
+ socket.connect();
12815
+ socket.on("connect", () => {
12816
+ socket.emit("joinRepo", repoId);
12817
+ });
12818
+ if (socket.connected) {
12819
+ socket.emit("joinRepo", repoId);
12820
+ }
12821
+ return socket;
12822
+ }
12823
+ function severityIcon(severity) {
12824
+ const s = severity.toLowerCase();
12825
+ if (s === "critical" || s === "high") return "\u2716";
12826
+ if (s === "medium") return "\u26A0";
12827
+ return "\u2139";
12828
+ }
12829
+ function severityColor(severity) {
12830
+ const s = severity.toLowerCase();
12831
+ if (s === "critical") return (t) => `\x1B[91m${t}\x1B[0m`;
12832
+ if (s === "high") return (t) => `\x1B[31m${t}\x1B[0m`;
12833
+ if (s === "medium") return (t) => `\x1B[33m${t}\x1B[0m`;
12834
+ return (t) => `\x1B[36m${t}\x1B[0m`;
12835
+ }
12836
+ function buildTargetPath(v3) {
12837
+ const parts2 = [];
12838
+ if (v3.targetServiceName) parts2.push(v3.targetServiceName);
12839
+ if (v3.targetDatabaseName) parts2.push(v3.targetDatabaseName);
12840
+ if (v3.targetModuleName) parts2.push(v3.targetModuleName);
12841
+ if (v3.targetMethodName) parts2.push(v3.targetMethodName);
12842
+ if (v3.targetTable) parts2.push(`table: ${v3.targetTable}`);
12843
+ return parts2.join(" :: ");
12844
+ }
12845
+ function wrapText(text, indent, maxWidth) {
12846
+ const words = text.split(/\s+/);
12847
+ const lines = [];
12848
+ let line = "";
12849
+ for (const word of words) {
12850
+ if (line && line.length + 1 + word.length > maxWidth) {
12851
+ lines.push(line);
12852
+ line = word;
12853
+ } else {
12854
+ line = line ? `${line} ${word}` : word;
12778
12855
  }
12779
12856
  }
12780
- async isInstalled() {
12781
- return fs4.existsSync(PLIST_PATH);
12782
- }
12783
- };
12784
-
12785
- // tools/cli/src/commands/service/linux.ts
12786
- import fs5 from "node:fs";
12787
- import path4 from "node:path";
12788
- import os4 from "node:os";
12789
- import { execSync as execSync2 } from "node:child_process";
12790
- var SERVICE_NAME = "truecourse";
12791
- var UNIT_DIR = path4.join(os4.homedir(), ".config", "systemd", "user");
12792
- var UNIT_PATH = path4.join(UNIT_DIR, `${SERVICE_NAME}.service`);
12793
- function buildUnitFile(serverPath, logPath) {
12794
- const envFile = path4.join(os4.homedir(), ".truecourse", ".env");
12795
- const logDir = path4.dirname(logPath);
12796
- return `[Unit]
12797
- Description=TrueCourse Server
12798
- After=network.target
12799
-
12800
- [Service]
12801
- Type=simple
12802
- ExecStart=${process.execPath} ${serverPath}
12803
- Restart=on-failure
12804
- RestartSec=5
12805
- EnvironmentFile=${envFile}
12806
- StandardOutput=append:${path4.join(logDir, "truecourse.log")}
12807
- StandardError=append:${path4.join(logDir, "truecourse.error.log")}
12808
-
12809
- [Install]
12810
- WantedBy=default.target
12811
- `;
12857
+ if (line) lines.push(line);
12858
+ return lines.map((l2, i) => i === 0 ? l2 : `${indent}${l2}`).join("\n");
12812
12859
  }
12813
- var LinuxService = class {
12814
- async install(serverPath, logPath) {
12815
- fs5.mkdirSync(UNIT_DIR, { recursive: true });
12816
- fs5.mkdirSync(path4.dirname(logPath), { recursive: true });
12817
- const unit = buildUnitFile(serverPath, logPath);
12818
- fs5.writeFileSync(UNIT_PATH, unit, "utf-8");
12819
- execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
12820
- execSync2(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "pipe" });
12821
- }
12822
- async uninstall() {
12823
- try {
12824
- execSync2(`systemctl --user stop ${SERVICE_NAME}`, { stdio: "pipe" });
12825
- } catch {
12826
- }
12827
- try {
12828
- execSync2(`systemctl --user disable ${SERVICE_NAME}`, { stdio: "pipe" });
12829
- } catch {
12830
- }
12831
- if (fs5.existsSync(UNIT_PATH)) {
12832
- fs5.unlinkSync(UNIT_PATH);
12860
+ function renderViolations(violations) {
12861
+ if (violations.length === 0) {
12862
+ v2.info("No violations found. Run `truecourse analyze` first.");
12863
+ return;
12864
+ }
12865
+ console.log("");
12866
+ const counts = {};
12867
+ for (const v3 of violations) {
12868
+ const sev = v3.severity.toLowerCase();
12869
+ counts[sev] = (counts[sev] || 0) + 1;
12870
+ const icon = severityIcon(v3.severity);
12871
+ const color = severityColor(v3.severity);
12872
+ const label = v3.severity.toUpperCase();
12873
+ const target = buildTargetPath(v3);
12874
+ console.log(` ${color(`${icon} ${label}`)} ${v3.title}`);
12875
+ if (target) {
12876
+ console.log(` ${target}`);
12833
12877
  }
12834
- try {
12835
- execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
12836
- } catch {
12878
+ if (v3.fixPrompt) {
12879
+ const indent = " ";
12880
+ console.log("");
12881
+ console.log(` Fix: ${wrapText(v3.fixPrompt, indent + " ", 60)}`);
12837
12882
  }
12883
+ console.log("");
12838
12884
  }
12839
- async start() {
12840
- execSync2(`systemctl --user start ${SERVICE_NAME}`, { stdio: "pipe" });
12841
- }
12842
- async stop() {
12843
- execSync2(`systemctl --user stop ${SERVICE_NAME}`, { stdio: "pipe" });
12885
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12886
+ const parts2 = [];
12887
+ for (const sev of ["critical", "high", "medium", "low", "info"]) {
12888
+ if (counts[sev]) parts2.push(`${counts[sev]} ${sev}`);
12844
12889
  }
12845
- async status() {
12846
- try {
12847
- const output = execSync2(
12848
- `systemctl --user show ${SERVICE_NAME} --property=ActiveState,MainPID`,
12849
- { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" }
12850
- );
12851
- const activeMatch = output.match(/ActiveState=(\w+)/);
12852
- const pidMatch = output.match(/MainPID=(\d+)/);
12853
- const isActive = activeMatch?.[1] === "active";
12854
- const pid = pidMatch ? parseInt(pidMatch[1], 10) : void 0;
12855
- return { running: isActive, pid: isActive && pid ? pid : void 0 };
12856
- } catch {
12857
- return { running: false };
12858
- }
12890
+ console.log(` ${violations.length} violations (${parts2.join(", ")})`);
12891
+ console.log("");
12892
+ }
12893
+ function renderViolationsSummary(violations) {
12894
+ if (violations.length === 0) {
12895
+ v2.info("No violations found.");
12896
+ return;
12859
12897
  }
12860
- async isInstalled() {
12861
- return fs5.existsSync(UNIT_PATH);
12898
+ const counts = {};
12899
+ for (const v3 of violations) {
12900
+ const sev = v3.severity.toLowerCase();
12901
+ counts[sev] = (counts[sev] || 0) + 1;
12862
12902
  }
12863
- };
12864
-
12865
- // tools/cli/src/commands/service/windows.ts
12866
- import { execSync as execSync3 } from "node:child_process";
12867
- var SERVICE_NAME2 = "TrueCourse";
12868
- var WindowsService = class {
12869
- svc;
12870
- async getNodeWindows() {
12871
- try {
12872
- return __require("node-windows");
12873
- } catch {
12874
- throw new Error(
12875
- "node-windows is required for background service on Windows.\nInstall it with: npm install -g node-windows"
12876
- );
12903
+ const parts2 = [];
12904
+ for (const sev of ["critical", "high", "medium", "low", "info"]) {
12905
+ if (counts[sev]) {
12906
+ const color = severityColor(sev);
12907
+ parts2.push(color(`${counts[sev]} ${sev}`));
12877
12908
  }
12878
12909
  }
12879
- async install(serverPath, logPath) {
12880
- const nw = await this.getNodeWindows();
12881
- const { Service } = nw;
12882
- return new Promise((resolve2, reject) => {
12883
- const svc = new Service({
12884
- name: SERVICE_NAME2,
12885
- description: "TrueCourse Server",
12886
- script: serverPath,
12887
- nodeOptions: [],
12888
- env: [{
12889
- name: "TRUECOURSE_LOG_DIR",
12890
- value: logPath
12891
- }]
12892
- });
12893
- svc.on("install", () => {
12894
- svc.start();
12895
- resolve2();
12896
- });
12897
- svc.on("error", (err) => reject(err));
12898
- svc.install();
12899
- });
12900
- }
12901
- async uninstall() {
12902
- const nw = await this.getNodeWindows();
12903
- const { Service } = nw;
12904
- return new Promise((resolve2, reject) => {
12905
- const svc = new Service({
12906
- name: SERVICE_NAME2,
12907
- script: ""
12908
- // Not needed for uninstall
12909
- });
12910
- svc.on("uninstall", () => resolve2());
12911
- svc.on("error", (err) => reject(err));
12912
- svc.uninstall();
12913
- });
12914
- }
12915
- async start() {
12916
- execSync3(`sc.exe start ${SERVICE_NAME2}`, { stdio: "pipe" });
12917
- }
12918
- async stop() {
12919
- execSync3(`sc.exe stop ${SERVICE_NAME2}`, { stdio: "pipe" });
12910
+ console.log("");
12911
+ console.log(` ${violations.length} violations (${parts2.join(", ")})`);
12912
+ console.log("");
12913
+ v2.info("Run `truecourse list` to see full details.");
12914
+ }
12915
+ function renderDiffResults(result) {
12916
+ console.log("");
12917
+ const modified = result.changedFiles.filter((f2) => f2.status === "modified").length;
12918
+ const newFiles = result.changedFiles.filter((f2) => f2.status === "new").length;
12919
+ const deleted = result.changedFiles.filter((f2) => f2.status === "deleted").length;
12920
+ const fileParts = [];
12921
+ if (modified) fileParts.push(`${modified} modified`);
12922
+ if (newFiles) fileParts.push(`${newFiles} new`);
12923
+ if (deleted) fileParts.push(`${deleted} deleted`);
12924
+ console.log(` Changed files: ${result.changedFiles.length} (${fileParts.join(", ")})`);
12925
+ console.log("");
12926
+ if (result.isStale) {
12927
+ console.log(` \x1B[33m\u26A0 Results may be stale \u2014 baseline analysis has changed.\x1B[0m`);
12928
+ console.log("");
12920
12929
  }
12921
- async status() {
12922
- try {
12923
- const output = execSync3(`sc.exe query ${SERVICE_NAME2}`, {
12924
- stdio: ["pipe", "pipe", "pipe"],
12925
- encoding: "utf-8"
12926
- });
12927
- const running = output.includes("RUNNING");
12928
- const pidMatch = output.match(/PID\s*:\s*(\d+)/);
12929
- return {
12930
- running,
12931
- pid: pidMatch ? parseInt(pidMatch[1], 10) : void 0
12932
- };
12933
- } catch {
12934
- return { running: false };
12930
+ if (result.newViolations.length > 0) {
12931
+ console.log(` NEW ISSUES (${result.newViolations.length})`);
12932
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12933
+ for (const v3 of result.newViolations) {
12934
+ const icon = severityIcon(v3.severity);
12935
+ const color = severityColor(v3.severity);
12936
+ const label = v3.severity.toUpperCase();
12937
+ const target = buildTargetPath(v3);
12938
+ console.log(` ${color(`${icon} ${label}`)} ${v3.title}`);
12939
+ if (target) {
12940
+ console.log(` ${target}`);
12941
+ }
12942
+ if (v3.fixPrompt) {
12943
+ const indent = " ";
12944
+ console.log("");
12945
+ console.log(` Fix: ${wrapText(v3.fixPrompt, indent + " ", 60)}`);
12946
+ }
12947
+ console.log("");
12935
12948
  }
12949
+ } else {
12950
+ console.log(" NEW ISSUES (0)");
12951
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12952
+ console.log(" None");
12953
+ console.log("");
12936
12954
  }
12937
- async isInstalled() {
12938
- try {
12939
- execSync3(`sc.exe query ${SERVICE_NAME2}`, { stdio: "pipe" });
12940
- return true;
12941
- } catch {
12942
- return false;
12955
+ if (result.resolvedViolations.length > 0) {
12956
+ console.log(` RESOLVED (${result.resolvedViolations.length})`);
12957
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12958
+ for (const v3 of result.resolvedViolations) {
12959
+ const target = buildTargetPath(v3);
12960
+ const color = severityColor(v3.severity);
12961
+ const label = v3.severity.toUpperCase();
12962
+ console.log(` ${color(`\u2714 ${label}`)} ${v3.title}`);
12963
+ if (target) {
12964
+ console.log(` ${target}`);
12965
+ }
12966
+ console.log("");
12943
12967
  }
12968
+ } else {
12969
+ console.log(" RESOLVED (0)");
12970
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12971
+ console.log(" None");
12972
+ console.log("");
12944
12973
  }
12945
- };
12974
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12975
+ console.log(` Summary: ${result.summary.newCount} new issues, ${result.summary.resolvedCount} resolved`);
12976
+ console.log("");
12977
+ }
12978
+ function renderDiffResultsSummary(result) {
12979
+ console.log("");
12980
+ const modified = result.changedFiles.filter((f2) => f2.status === "modified").length;
12981
+ const newFiles = result.changedFiles.filter((f2) => f2.status === "new").length;
12982
+ const deleted = result.changedFiles.filter((f2) => f2.status === "deleted").length;
12983
+ const fileParts = [];
12984
+ if (modified) fileParts.push(`${modified} modified`);
12985
+ if (newFiles) fileParts.push(`${newFiles} new`);
12986
+ if (deleted) fileParts.push(`${deleted} deleted`);
12987
+ console.log(` Changed files: ${result.changedFiles.length} (${fileParts.join(", ")})`);
12988
+ console.log("");
12989
+ if (result.isStale) {
12990
+ console.log(` \x1B[33m\u26A0 Results may be stale \u2014 baseline analysis has changed.\x1B[0m`);
12991
+ console.log("");
12992
+ }
12993
+ console.log(` Summary: ${result.summary.newCount} new issues, ${result.summary.resolvedCount} resolved`);
12994
+ console.log("");
12995
+ v2.info("Run `truecourse list --diff` to see full details.");
12996
+ }
12997
+ function openInBrowser(url2) {
12998
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
12999
+ exec(`${cmd} ${url2}`);
13000
+ }
13001
+ var DEFAULT_PORT, DEFAULT_CONFIG;
13002
+ var init_helpers = __esm({
13003
+ "tools/cli/src/commands/helpers.ts"() {
13004
+ "use strict";
13005
+ init_dist2();
13006
+ init_esm_debug3();
13007
+ DEFAULT_PORT = 3001;
13008
+ DEFAULT_CONFIG = { runMode: "console" };
13009
+ }
13010
+ });
12946
13011
 
12947
- // tools/cli/src/commands/service/platform.ts
12948
- function getPlatform() {
12949
- switch (process.platform) {
12950
- case "darwin":
12951
- return new MacOSService();
12952
- case "linux":
12953
- return new LinuxService();
12954
- case "win32":
12955
- return new WindowsService();
12956
- default:
12957
- throw new Error(
12958
- `Unsupported platform: ${process.platform}. Background service mode supports macOS, Linux, and Windows.`
12959
- );
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
+ // tools/cli/src/commands/setup.ts
13036
+ init_dist2();
13037
+ import fs6 from "node:fs";
13038
+ import path6 from "node:path";
13039
+ import os5 from "node:os";
13040
+ var DEFAULT_MODELS = {
13041
+ anthropic: "claude-sonnet-4-20250514",
13042
+ openai: "gpt-5.3-codex"
13043
+ };
13044
+ function buildEnvContents(config) {
13045
+ const lines = [
13046
+ "# TrueCourse Environment Configuration",
13047
+ `# Generated by truecourse setup on ${(/* @__PURE__ */ new Date()).toISOString()}`,
13048
+ ""
13049
+ ];
13050
+ if (config.provider) {
13051
+ lines.push(`LLM_PROVIDER=${config.provider}`);
12960
13052
  }
12961
- }
12962
-
12963
- // tools/cli/src/commands/service/logs.ts
12964
- import fs6 from "node:fs";
12965
- import path5 from "node:path";
12966
- import os5 from "node:os";
12967
- import { spawn } from "node:child_process";
12968
- var MAX_LOG_SIZE = 10 * 1024 * 1024;
12969
- var MAX_LOG_FILES = 5;
12970
- function getLogDir() {
12971
- return path5.join(os5.homedir(), ".truecourse", "logs");
12972
- }
12973
- function getLogPath() {
12974
- return path5.join(getLogDir(), "truecourse.log");
12975
- }
12976
- function rotateLogs(logDir) {
12977
- const logFile = path5.join(logDir, "truecourse.log");
12978
- if (!fs6.existsSync(logFile)) return;
12979
- const stats = fs6.statSync(logFile);
12980
- if (stats.size < MAX_LOG_SIZE) return;
12981
- for (let i = MAX_LOG_FILES; i >= 1; i--) {
12982
- const older = path5.join(logDir, `truecourse.log.${i}`);
12983
- if (i === MAX_LOG_FILES) {
12984
- if (fs6.existsSync(older)) fs6.unlinkSync(older);
12985
- } else {
12986
- const newer = path5.join(logDir, `truecourse.log.${i + 1}`);
12987
- if (fs6.existsSync(older)) fs6.renameSync(older, newer);
12988
- }
13053
+ if (config.model) {
13054
+ lines.push(`LLM_MODEL=${config.model}`);
12989
13055
  }
12990
- fs6.renameSync(logFile, path5.join(logDir, "truecourse.log.1"));
12991
- }
12992
- function rotateErrorLogs(logDir) {
12993
- const logFile = path5.join(logDir, "truecourse.error.log");
12994
- if (!fs6.existsSync(logFile)) return;
12995
- const stats = fs6.statSync(logFile);
12996
- if (stats.size < MAX_LOG_SIZE) return;
12997
- for (let i = MAX_LOG_FILES; i >= 1; i--) {
12998
- const older = path5.join(logDir, `truecourse.error.log.${i}`);
12999
- if (i === MAX_LOG_FILES) {
13000
- if (fs6.existsSync(older)) fs6.unlinkSync(older);
13001
- } else {
13002
- const newer = path5.join(logDir, `truecourse.error.log.${i + 1}`);
13003
- if (fs6.existsSync(older)) fs6.renameSync(older, newer);
13004
- }
13056
+ if (config.anthropicKey) {
13057
+ lines.push(`ANTHROPIC_API_KEY=${config.anthropicKey}`);
13058
+ }
13059
+ if (config.openaiKey) {
13060
+ lines.push(`OPENAI_API_KEY=${config.openaiKey}`);
13061
+ }
13062
+ if (config.langfusePublicKey && config.langfuseSecretKey) {
13063
+ lines.push("");
13064
+ lines.push("# Langfuse Tracing");
13065
+ lines.push(`LANGFUSE_PUBLIC_KEY=${config.langfusePublicKey}`);
13066
+ lines.push(`LANGFUSE_SECRET_KEY=${config.langfuseSecretKey}`);
13005
13067
  }
13006
- fs6.renameSync(logFile, path5.join(logDir, "truecourse.error.log.1"));
13068
+ lines.push("");
13069
+ return lines.join("\n");
13007
13070
  }
13008
- function tailLogs() {
13009
- const logFile = getLogPath();
13010
- if (!fs6.existsSync(logFile)) {
13011
- console.log("No log file found. Is the service running?");
13012
- console.log(`Expected at: ${logFile}`);
13013
- return;
13071
+ async function runSetup() {
13072
+ we("Welcome to TrueCourse");
13073
+ const provider = await de({
13074
+ message: "Which LLM provider would you like to use?",
13075
+ options: [
13076
+ { value: "anthropic", label: "Anthropic (Claude)" },
13077
+ { value: "openai", label: "OpenAI (GPT)" },
13078
+ { value: "skip", label: "Skip for now" }
13079
+ ]
13080
+ });
13081
+ if (BD(provider)) {
13082
+ ve("Setup cancelled.");
13083
+ process.exit(0);
13014
13084
  }
13015
- if (process.platform === "win32") {
13016
- const content = fs6.readFileSync(logFile, "utf-8");
13017
- const lines = content.split("\n");
13018
- const tail = lines.slice(-50);
13019
- for (const line of tail) {
13020
- process.stdout.write(line + "\n");
13021
- }
13022
- let lastSize = fs6.statSync(logFile).size;
13023
- fs6.watchFile(logFile, { interval: 500 }, () => {
13024
- const newSize = fs6.statSync(logFile).size;
13025
- if (newSize > lastSize) {
13026
- const fd = fs6.openSync(logFile, "r");
13027
- const buf = Buffer.alloc(newSize - lastSize);
13028
- fs6.readSync(fd, buf, 0, buf.length, lastSize);
13029
- fs6.closeSync(fd);
13030
- process.stdout.write(buf.toString("utf-8"));
13031
- lastSize = newSize;
13085
+ const config = {};
13086
+ if (provider === "anthropic" || provider === "openai") {
13087
+ config.provider = provider;
13088
+ }
13089
+ if (provider === "anthropic") {
13090
+ const anthropicKey = await ue({
13091
+ message: "Enter your Anthropic API key:",
13092
+ placeholder: "sk-ant-...",
13093
+ validate(value2) {
13094
+ if (!value2 || value2.trim().length === 0) {
13095
+ return "API key is required";
13096
+ }
13032
13097
  }
13033
13098
  });
13034
- } else {
13035
- const tail = spawn("tail", ["-f", "-n", "50", logFile], {
13036
- stdio: "inherit"
13037
- });
13038
- const cleanup = () => {
13039
- tail.kill("SIGTERM");
13040
- };
13041
- process.on("SIGINT", cleanup);
13042
- process.on("SIGTERM", cleanup);
13043
- tail.on("close", () => {
13099
+ if (BD(anthropicKey)) {
13100
+ ve("Setup cancelled.");
13044
13101
  process.exit(0);
13045
- });
13046
- }
13047
- }
13048
-
13049
- // tools/cli/src/commands/start.ts
13050
- var __dirname = path6.dirname(fileURLToPath(import.meta.url));
13051
- function getServerPath() {
13052
- return path6.join(__dirname, "server.mjs");
13053
- }
13054
- async function healthcheck() {
13055
- const url2 = getServerUrl();
13056
- for (let i = 0; i < 30; i++) {
13057
- try {
13058
- const res = await fetch(`${url2}/api/health`);
13059
- if (res.ok) return true;
13060
- } catch {
13061
13102
  }
13062
- await new Promise((r2) => setTimeout(r2, 500));
13103
+ config.anthropicKey = anthropicKey;
13063
13104
  }
13064
- return false;
13065
- }
13066
- async function startServiceMode() {
13067
- const platform = getPlatform();
13068
- const serverPath = getServerPath();
13069
- const logDir = getLogDir();
13070
- const logPath = getLogPath();
13071
- const url2 = getServerUrl();
13072
- const { running } = await platform.status();
13073
- if (running) {
13074
- v2.info(`TrueCourse is already running at ${url2}`);
13075
- return;
13105
+ if (provider === "openai") {
13106
+ const openaiKey = await ue({
13107
+ message: "Enter your OpenAI API key:",
13108
+ placeholder: "sk-...",
13109
+ validate(value2) {
13110
+ if (!value2 || value2.trim().length === 0) {
13111
+ return "API key is required";
13112
+ }
13113
+ }
13114
+ });
13115
+ if (BD(openaiKey)) {
13116
+ ve("Setup cancelled.");
13117
+ process.exit(0);
13118
+ }
13119
+ config.openaiKey = openaiKey;
13076
13120
  }
13077
- const installed = await platform.isInstalled();
13078
- if (installed) {
13079
- rotateLogs(logDir);
13080
- rotateErrorLogs(logDir);
13081
- v2.step("Starting background service...");
13082
- await platform.start();
13083
- } else {
13084
- rotateLogs(logDir);
13085
- rotateErrorLogs(logDir);
13086
- v2.step("Installing and starting background service...");
13087
- await platform.install(serverPath, logPath);
13121
+ if (provider === "anthropic" || provider === "openai") {
13122
+ const defaultModel = DEFAULT_MODELS[provider];
13123
+ const model = await ue({
13124
+ message: `Enter the model to use (default: ${defaultModel}):`,
13125
+ placeholder: defaultModel
13126
+ });
13127
+ if (BD(model)) {
13128
+ ve("Setup cancelled.");
13129
+ process.exit(0);
13130
+ }
13131
+ config.model = model?.trim() || defaultModel;
13088
13132
  }
13089
- const healthy = await healthcheck();
13090
- if (healthy) {
13091
- v2.success(`TrueCourse is running at ${url2}`);
13092
- } else {
13093
- v2.warn("Service started but server hasn't responded yet.");
13094
- v2.info("Check logs with: truecourse service logs");
13133
+ const useLangfuse = await me({
13134
+ message: "Would you like to enable Langfuse tracing?",
13135
+ initialValue: false
13136
+ });
13137
+ if (BD(useLangfuse)) {
13138
+ ve("Setup cancelled.");
13139
+ process.exit(0);
13095
13140
  }
13096
- }
13097
- function startConsoleMode() {
13098
- const serverPath = getServerPath();
13099
- v2.step("Starting server (embedded PostgreSQL starts automatically)...");
13100
- const serverProcess = spawn2(
13101
- process.execPath,
13102
- [serverPath],
13103
- {
13104
- stdio: "inherit",
13105
- env: { ...process.env }
13141
+ if (useLangfuse) {
13142
+ const langfusePublicKey = await ue({
13143
+ message: "Enter your Langfuse public key:",
13144
+ placeholder: "pk-lf-...",
13145
+ validate(value2) {
13146
+ if (!value2 || value2.trim().length === 0) {
13147
+ return "Public key is required";
13148
+ }
13149
+ }
13150
+ });
13151
+ if (BD(langfusePublicKey)) {
13152
+ ve("Setup cancelled.");
13153
+ process.exit(0);
13106
13154
  }
13107
- );
13108
- serverProcess.on("error", (error) => {
13109
- v2.error(`Failed to start server: ${error.message}`);
13110
- process.exit(1);
13111
- });
13112
- serverProcess.on("close", (code) => {
13113
- if (code !== null && code !== 0) {
13114
- process.exit(code);
13155
+ const langfuseSecretKey = await ue({
13156
+ message: "Enter your Langfuse secret key:",
13157
+ placeholder: "sk-lf-...",
13158
+ validate(value2) {
13159
+ if (!value2 || value2.trim().length === 0) {
13160
+ return "Secret key is required";
13161
+ }
13162
+ }
13163
+ });
13164
+ if (BD(langfuseSecretKey)) {
13165
+ ve("Setup cancelled.");
13166
+ process.exit(0);
13115
13167
  }
13168
+ config.langfusePublicKey = langfusePublicKey;
13169
+ config.langfuseSecretKey = langfuseSecretKey;
13170
+ }
13171
+ const configDir = path6.join(os5.homedir(), ".truecourse");
13172
+ fs6.mkdirSync(configDir, { recursive: true });
13173
+ const envPath = path6.join(configDir, ".env");
13174
+ fs6.writeFileSync(envPath, buildEnvContents(config), "utf-8");
13175
+ v2.success(`Configuration saved to ${envPath}`);
13176
+ const runMode = await de({
13177
+ message: "How would you like to run TrueCourse?",
13178
+ options: [
13179
+ { value: "console", label: "Console (keep terminal open)" },
13180
+ { value: "service", label: "Background service (runs automatically, no terminal needed)" }
13181
+ ]
13116
13182
  });
13117
- const cleanup = () => {
13118
- serverProcess.kill("SIGTERM");
13119
- };
13120
- process.on("SIGINT", cleanup);
13121
- process.on("SIGTERM", cleanup);
13122
- }
13123
- async function runStart() {
13124
- we("Starting TrueCourse");
13125
- const config = readConfig();
13126
- if (config.runMode === "service") {
13127
- try {
13128
- await startServiceMode();
13129
- } catch (error) {
13130
- v2.error(`Service mode failed: ${error.message}`);
13131
- v2.info("Falling back to console mode. Reconfigure with: truecourse setup");
13132
- startConsoleMode();
13133
- }
13134
- } else {
13135
- startConsoleMode();
13183
+ if (BD(runMode)) {
13184
+ ve("Setup cancelled.");
13185
+ process.exit(0);
13186
+ }
13187
+ const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_helpers(), helpers_exports));
13188
+ writeConfig2({ runMode });
13189
+ if (runMode === "service") {
13190
+ v2.info("Background service selected. Run `truecourse start` to install and start the service.");
13136
13191
  }
13192
+ v2.info("Embedded PostgreSQL will start automatically when the server runs.");
13193
+ v2.info("Database migrations are applied on server startup.");
13194
+ fe("Setup complete!");
13137
13195
  }
13138
13196
 
13197
+ // tools/cli/src/index.ts
13198
+ init_start();
13199
+
13139
13200
  // tools/cli/src/commands/add.ts
13140
13201
  init_dist2();
13141
13202
  init_helpers();
@@ -13172,7 +13233,8 @@ async function runAdd() {
13172
13233
  message: "Would you like to install Claude Code skills?"
13173
13234
  });
13174
13235
  if (BD(installSkills)) {
13175
- fe(`Open ${repoUrl}`);
13236
+ openInBrowser(repoUrl);
13237
+ fe("Opened in browser");
13176
13238
  return;
13177
13239
  }
13178
13240
  if (installSkills) {
@@ -13189,7 +13251,8 @@ async function runAdd() {
13189
13251
  v2.message(" - truecourse-fix (apply fixes)");
13190
13252
  }
13191
13253
  }
13192
- fe(`Open ${repoUrl}`);
13254
+ openInBrowser(repoUrl);
13255
+ fe("Opened in browser");
13193
13256
  } catch (err) {
13194
13257
  const message = err instanceof Error ? err.message : String(err);
13195
13258
  if (message.includes("ECONNREFUSED") || message.includes("fetch failed")) {
@@ -13209,7 +13272,7 @@ init_helpers();
13209
13272
  var TIMEOUT_MS = 15 * 60 * 1e3;
13210
13273
  async function runAnalyze() {
13211
13274
  we("Analyzing repository");
13212
- await ensureServer();
13275
+ const firstRun = await ensureServer();
13213
13276
  const repo = await ensureRepo();
13214
13277
  v2.step(`Repository: ${repo.name}`);
13215
13278
  const serverUrl = getServerUrl();
@@ -13273,9 +13336,11 @@ async function runAnalyze() {
13273
13336
  }
13274
13337
  const violations = await res.json();
13275
13338
  renderViolationsSummary(violations);
13276
- const repoUrl = `${serverUrl}/repos/${repo.id}`;
13277
- const link = `\x1B[4m\x1B[38;5;75m${repoUrl}\x1B[0m`;
13278
- fe(`Open ${link} to see violations and architecture diagrams in the UI`);
13339
+ if (firstRun) {
13340
+ const repoUrl = `${serverUrl}/repos/${repo.id}`;
13341
+ openInBrowser(repoUrl);
13342
+ }
13343
+ fe("Analysis complete");
13279
13344
  } catch (err) {
13280
13345
  spinner.stop("Analysis failed");
13281
13346
  const message = err instanceof Error ? err.message : String(err);
@@ -13367,9 +13432,11 @@ async function runListDiff() {
13367
13432
 
13368
13433
  // tools/cli/src/commands/service/index.ts
13369
13434
  init_dist2();
13435
+ init_platform();
13436
+ init_logs();
13437
+ init_helpers();
13370
13438
  import path7 from "node:path";
13371
13439
  import { fileURLToPath as fileURLToPath3 } from "node:url";
13372
- init_helpers();
13373
13440
  var __dirname2 = path7.dirname(fileURLToPath3(import.meta.url));
13374
13441
  function getServerPath2() {
13375
13442
  return path7.join(__dirname2, "..", "server.mjs");
@@ -13525,6 +13592,7 @@ function registerServiceCommand(program3) {
13525
13592
 
13526
13593
  // tools/cli/src/index.ts
13527
13594
  init_helpers();
13595
+ init_platform();
13528
13596
  var program2 = new Command();
13529
13597
  program2.name("truecourse").version("0.1.0").description("TrueCourse CLI - Setup and manage your TrueCourse instance");
13530
13598
  program2.command("setup").description("Run the setup wizard to configure TrueCourse").action(async () => {