truecourse 0.1.11 → 0.1.13

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
@@ -12185,957 +12185,1036 @@ 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
12209
  }
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
- }
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}`);
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");
12525
+ }
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);
12541
+ }
12526
12542
  }
12527
- lines.push("");
12528
- return lines.join("\n");
12543
+ fs4.renameSync(logFile, path3.join(logDir, "truecourse.log.1"));
12529
12544
  }
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);
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);
12557
+ }
12543
12558
  }
12544
- const config = {};
12545
- if (provider === "anthropic" || provider === "openai") {
12546
- config.provider = provider;
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;
12547
12567
  }
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);
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");
12561
12574
  }
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
- }
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;
12572
12585
  }
12573
12586
  });
12574
- if (BD(openaiKey)) {
12575
- ve("Setup cancelled.");
12576
- process.exit(0);
12577
- }
12578
- config.openaiKey = openaiKey;
12579
- }
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
12587
+ } else {
12588
+ const tail = spawn("tail", ["-f", "-n", "50", logFile], {
12589
+ stdio: "inherit"
12585
12590
  });
12586
- if (BD(model)) {
12587
- ve("Setup cancelled.");
12591
+ const cleanup = () => {
12592
+ tail.kill("SIGTERM");
12593
+ };
12594
+ process.on("SIGINT", cleanup);
12595
+ process.on("SIGTERM", cleanup);
12596
+ tail.on("close", () => {
12588
12597
  process.exit(0);
12598
+ });
12599
+ }
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;
12607
+ }
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 {
12589
12628
  }
12590
- config.model = model?.trim() || defaultModel;
12629
+ await new Promise((r2) => setTimeout(r2, 500));
12591
12630
  }
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);
12631
+ return false;
12632
+ }
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;
12599
12643
  }
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);
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(openBrowser) {
12666
+ const serverPath = getServerPath();
12667
+ const url2 = getServerUrl();
12668
+ v2.step("Starting server (embedded PostgreSQL starts automatically)...");
12669
+ const serverProcess = spawn2(
12670
+ process.execPath,
12671
+ [serverPath],
12672
+ {
12673
+ stdio: "inherit",
12674
+ env: { ...process.env }
12613
12675
  }
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
- }
12621
- }
12622
- });
12623
- if (BD(langfuseSecretKey)) {
12624
- ve("Setup cancelled.");
12625
- process.exit(0);
12676
+ );
12677
+ serverProcess.on("error", (error) => {
12678
+ v2.error(`Failed to start server: ${error.message}`);
12679
+ process.exit(1);
12680
+ });
12681
+ serverProcess.on("close", (code) => {
12682
+ if (code !== null && code !== 0) {
12683
+ process.exit(code);
12626
12684
  }
12627
- config.langfusePublicKey = langfusePublicKey;
12628
- config.langfuseSecretKey = langfuseSecretKey;
12629
- }
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
12685
  });
12642
- if (BD(runMode)) {
12643
- ve("Setup cancelled.");
12644
- process.exit(0);
12686
+ const cleanup = () => {
12687
+ serverProcess.kill("SIGTERM");
12688
+ };
12689
+ process.on("SIGINT", cleanup);
12690
+ process.on("SIGTERM", cleanup);
12691
+ if (openBrowser) {
12692
+ healthcheck().then((healthy) => {
12693
+ if (healthy) openInBrowser(url2);
12694
+ });
12645
12695
  }
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.");
12696
+ }
12697
+ async function runStart({ openBrowser = true } = {}) {
12698
+ we("Starting TrueCourse");
12699
+ const config = readConfig();
12700
+ if (config.runMode === "service") {
12701
+ try {
12702
+ await startServiceMode(openBrowser);
12703
+ } catch (error) {
12704
+ v2.error(`Service mode failed: ${error.message}`);
12705
+ v2.info("Falling back to console mode. Reconfigure with: truecourse setup");
12706
+ startConsoleMode(openBrowser);
12707
+ }
12708
+ } else {
12709
+ startConsoleMode(openBrowser);
12650
12710
  }
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!");
12654
12711
  }
12712
+ var __dirname;
12713
+ var init_start = __esm({
12714
+ "tools/cli/src/commands/start.ts"() {
12715
+ "use strict";
12716
+ init_dist2();
12717
+ init_helpers();
12718
+ init_platform();
12719
+ init_logs();
12720
+ __dirname = path4.dirname(fileURLToPath(import.meta.url));
12721
+ }
12722
+ });
12655
12723
 
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;
12724
+ // tools/cli/src/commands/helpers.ts
12725
+ var helpers_exports = {};
12726
+ __export(helpers_exports, {
12727
+ connectSocket: () => connectSocket,
12728
+ ensureRepo: () => ensureRepo,
12729
+ ensureServer: () => ensureServer,
12730
+ getConfigPath: () => getConfigPath,
12731
+ getServerUrl: () => getServerUrl,
12732
+ openInBrowser: () => openInBrowser,
12733
+ readConfig: () => readConfig,
12734
+ renderDiffResults: () => renderDiffResults,
12735
+ renderDiffResultsSummary: () => renderDiffResultsSummary,
12736
+ renderViolations: () => renderViolations,
12737
+ renderViolationsSummary: () => renderViolationsSummary,
12738
+ severityColor: () => severityColor,
12739
+ severityIcon: () => severityIcon,
12740
+ writeConfig: () => writeConfig
12741
+ });
12742
+ import { exec } from "node:child_process";
12743
+ import fs5 from "node:fs";
12744
+ import path5 from "node:path";
12745
+ import os4 from "node:os";
12746
+ function getConfigPath() {
12747
+ return path5.join(os4.homedir(), ".truecourse", "config.json");
12748
+ }
12749
+ function readConfig() {
12750
+ const configPath = getConfigPath();
12751
+ try {
12752
+ const raw = fs5.readFileSync(configPath, "utf-8");
12753
+ const parsed = JSON.parse(raw);
12754
+ return { ...DEFAULT_CONFIG, ...parsed };
12755
+ } catch {
12756
+ return { ...DEFAULT_CONFIG };
12675
12757
  }
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);
12686
- }
12687
- if (key) {
12688
- vars[key] = value2;
12758
+ }
12759
+ function writeConfig(partial) {
12760
+ const configPath = getConfigPath();
12761
+ const dir = path5.dirname(configPath);
12762
+ fs5.mkdirSync(dir, { recursive: true });
12763
+ const current = readConfig();
12764
+ const merged = { ...current, ...partial };
12765
+ fs5.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
12766
+ }
12767
+ function getServerUrl() {
12768
+ const port = process.env.PORT || DEFAULT_PORT;
12769
+ return `http://localhost:${port}`;
12770
+ }
12771
+ async function ensureServer() {
12772
+ const url2 = getServerUrl();
12773
+ try {
12774
+ const res = await fetch(`${url2}/api/health`);
12775
+ if (!res.ok) throw new Error(`Server returned ${res.status}`);
12776
+ return false;
12777
+ } catch {
12778
+ const { runStart: runStart2 } = await Promise.resolve().then(() => (init_start(), start_exports));
12779
+ await runStart2({ openBrowser: false });
12780
+ try {
12781
+ const res = await fetch(`${url2}/api/health`);
12782
+ if (!res.ok) throw new Error();
12783
+ } catch {
12784
+ v2.error("Server failed to start. Check logs with: truecourse service logs");
12785
+ process.exit(1);
12689
12786
  }
12787
+ return true;
12690
12788
  }
12691
- return vars;
12692
12789
  }
12693
-
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;");
12790
+ async function ensureRepo() {
12791
+ const url2 = getServerUrl();
12792
+ const repoPath = process.cwd();
12793
+ const res = await fetch(`${url2}/api/repos`, {
12794
+ method: "POST",
12795
+ headers: { "Content-Type": "application/json" },
12796
+ body: JSON.stringify({ path: repoPath })
12797
+ });
12798
+ if (!res.ok) {
12799
+ const body = await res.text().catch(() => "");
12800
+ let message = `Server returned ${res.status}`;
12801
+ try {
12802
+ const json = JSON.parse(body);
12803
+ if (json.error) message = json.error;
12804
+ } catch {
12805
+ if (body) message = body;
12806
+ }
12807
+ v2.error(message);
12808
+ process.exit(1);
12809
+ }
12810
+ return await res.json();
12700
12811
  }
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>`;
12812
+ function connectSocket(repoId) {
12813
+ const url2 = getServerUrl();
12814
+ const socket = lookup(url2, {
12815
+ autoConnect: false,
12816
+ reconnection: true,
12817
+ reconnectionAttempts: 5,
12818
+ transports: ["websocket", "polling"]
12819
+ });
12820
+ socket.connect();
12821
+ socket.on("connect", () => {
12822
+ socket.emit("joinRepo", repoId);
12823
+ });
12824
+ if (socket.connected) {
12825
+ socket.emit("joinRepo", repoId);
12713
12826
  }
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
- `;
12827
+ return socket;
12828
+ }
12829
+ function severityIcon(severity) {
12830
+ const s = severity.toLowerCase();
12831
+ if (s === "critical" || s === "high") return "\u2716";
12832
+ if (s === "medium") return "\u26A0";
12833
+ return "\u2139";
12834
+ }
12835
+ function severityColor(severity) {
12836
+ const s = severity.toLowerCase();
12837
+ if (s === "critical") return (t) => `\x1B[91m${t}\x1B[0m`;
12838
+ if (s === "high") return (t) => `\x1B[31m${t}\x1B[0m`;
12839
+ if (s === "medium") return (t) => `\x1B[33m${t}\x1B[0m`;
12840
+ return (t) => `\x1B[36m${t}\x1B[0m`;
12841
+ }
12842
+ function buildTargetPath(v3) {
12843
+ const parts2 = [];
12844
+ if (v3.targetServiceName) parts2.push(v3.targetServiceName);
12845
+ if (v3.targetDatabaseName) parts2.push(v3.targetDatabaseName);
12846
+ if (v3.targetModuleName) parts2.push(v3.targetModuleName);
12847
+ if (v3.targetMethodName) parts2.push(v3.targetMethodName);
12848
+ if (v3.targetTable) parts2.push(`table: ${v3.targetTable}`);
12849
+ return parts2.join(" :: ");
12850
+ }
12851
+ function wrapText(text, indent, maxWidth) {
12852
+ const words = text.split(/\s+/);
12853
+ const lines = [];
12854
+ let line = "";
12855
+ for (const word of words) {
12856
+ if (line && line.length + 1 + word.length > maxWidth) {
12857
+ lines.push(line);
12858
+ line = word;
12859
+ } else {
12860
+ line = line ? `${line} ${word}` : word;
12861
+ }
12862
+ }
12863
+ if (line) lines.push(line);
12864
+ return lines.map((l2, i) => i === 0 ? l2 : `${indent}${l2}`).join("\n");
12736
12865
  }
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() {
12748
- try {
12749
- execSync(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
12750
- } catch {
12866
+ function renderViolations(violations) {
12867
+ if (violations.length === 0) {
12868
+ v2.info("No violations found. Run `truecourse analyze` first.");
12869
+ return;
12870
+ }
12871
+ console.log("");
12872
+ const counts = {};
12873
+ for (const v3 of violations) {
12874
+ const sev = v3.severity.toLowerCase();
12875
+ counts[sev] = (counts[sev] || 0) + 1;
12876
+ const icon = severityIcon(v3.severity);
12877
+ const color = severityColor(v3.severity);
12878
+ const label = v3.severity.toUpperCase();
12879
+ const target = buildTargetPath(v3);
12880
+ console.log(` ${color(`${icon} ${label}`)} ${v3.title}`);
12881
+ if (target) {
12882
+ console.log(` ${target}`);
12751
12883
  }
12752
- if (fs4.existsSync(PLIST_PATH)) {
12753
- fs4.unlinkSync(PLIST_PATH);
12884
+ if (v3.fixPrompt) {
12885
+ const indent = " ";
12886
+ console.log("");
12887
+ console.log(` Fix: ${wrapText(v3.fixPrompt, indent + " ", 60)}`);
12754
12888
  }
12889
+ console.log("");
12755
12890
  }
12756
- async start() {
12757
- execSync(`launchctl start ${SERVICE_LABEL}`, { stdio: "pipe" });
12891
+ 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");
12892
+ const parts2 = [];
12893
+ for (const sev of ["critical", "high", "medium", "low", "info"]) {
12894
+ if (counts[sev]) parts2.push(`${counts[sev]} ${sev}`);
12758
12895
  }
12759
- async stop() {
12760
- execSync(`launchctl stop ${SERVICE_LABEL}`, { stdio: "pipe" });
12896
+ console.log(` ${violations.length} violations (${parts2.join(", ")})`);
12897
+ console.log("");
12898
+ }
12899
+ function renderViolationsSummary(violations) {
12900
+ if (violations.length === 0) {
12901
+ v2.info("No violations found.");
12902
+ return;
12761
12903
  }
12762
- async status() {
12763
- 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 };
12776
- } catch {
12777
- return { running: false };
12778
- }
12904
+ const counts = {};
12905
+ for (const v3 of violations) {
12906
+ const sev = v3.severity.toLowerCase();
12907
+ counts[sev] = (counts[sev] || 0) + 1;
12779
12908
  }
12780
- async isInstalled() {
12781
- return fs4.existsSync(PLIST_PATH);
12909
+ const parts2 = [];
12910
+ for (const sev of ["critical", "high", "medium", "low", "info"]) {
12911
+ if (counts[sev]) {
12912
+ const color = severityColor(sev);
12913
+ parts2.push(color(`${counts[sev]} ${sev}`));
12914
+ }
12782
12915
  }
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
- `;
12916
+ console.log("");
12917
+ console.log(` ${violations.length} violations (${parts2.join(", ")})`);
12918
+ console.log("");
12919
+ v2.info("Run `truecourse list` to see full details.");
12812
12920
  }
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);
12921
+ function renderDiffResults(result) {
12922
+ console.log("");
12923
+ const modified = result.changedFiles.filter((f2) => f2.status === "modified").length;
12924
+ const newFiles = result.changedFiles.filter((f2) => f2.status === "new").length;
12925
+ const deleted = result.changedFiles.filter((f2) => f2.status === "deleted").length;
12926
+ const fileParts = [];
12927
+ if (modified) fileParts.push(`${modified} modified`);
12928
+ if (newFiles) fileParts.push(`${newFiles} new`);
12929
+ if (deleted) fileParts.push(`${deleted} deleted`);
12930
+ console.log(` Changed files: ${result.changedFiles.length} (${fileParts.join(", ")})`);
12931
+ console.log("");
12932
+ if (result.isStale) {
12933
+ console.log(` \x1B[33m\u26A0 Results may be stale \u2014 baseline analysis has changed.\x1B[0m`);
12934
+ console.log("");
12935
+ }
12936
+ if (result.newViolations.length > 0) {
12937
+ console.log(` NEW ISSUES (${result.newViolations.length})`);
12938
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12939
+ for (const v3 of result.newViolations) {
12940
+ const icon = severityIcon(v3.severity);
12941
+ const color = severityColor(v3.severity);
12942
+ const label = v3.severity.toUpperCase();
12943
+ const target = buildTargetPath(v3);
12944
+ console.log(` ${color(`${icon} ${label}`)} ${v3.title}`);
12945
+ if (target) {
12946
+ console.log(` ${target}`);
12947
+ }
12948
+ if (v3.fixPrompt) {
12949
+ const indent = " ";
12950
+ console.log("");
12951
+ console.log(` Fix: ${wrapText(v3.fixPrompt, indent + " ", 60)}`);
12952
+ }
12953
+ console.log("");
12833
12954
  }
12834
- try {
12835
- execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
12836
- } catch {
12955
+ } else {
12956
+ console.log(" NEW ISSUES (0)");
12957
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12958
+ console.log(" None");
12959
+ console.log("");
12960
+ }
12961
+ if (result.resolvedViolations.length > 0) {
12962
+ console.log(` RESOLVED (${result.resolvedViolations.length})`);
12963
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12964
+ for (const v3 of result.resolvedViolations) {
12965
+ const target = buildTargetPath(v3);
12966
+ const color = severityColor(v3.severity);
12967
+ const label = v3.severity.toUpperCase();
12968
+ console.log(` ${color(`\u2714 ${label}`)} ${v3.title}`);
12969
+ if (target) {
12970
+ console.log(` ${target}`);
12971
+ }
12972
+ console.log("");
12837
12973
  }
12974
+ } else {
12975
+ console.log(" RESOLVED (0)");
12976
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
12977
+ console.log(" None");
12978
+ console.log("");
12838
12979
  }
12839
- async start() {
12840
- execSync2(`systemctl --user start ${SERVICE_NAME}`, { stdio: "pipe" });
12980
+ 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");
12981
+ console.log(` Summary: ${result.summary.newCount} new issues, ${result.summary.resolvedCount} resolved`);
12982
+ console.log("");
12983
+ }
12984
+ function renderDiffResultsSummary(result) {
12985
+ console.log("");
12986
+ const modified = result.changedFiles.filter((f2) => f2.status === "modified").length;
12987
+ const newFiles = result.changedFiles.filter((f2) => f2.status === "new").length;
12988
+ const deleted = result.changedFiles.filter((f2) => f2.status === "deleted").length;
12989
+ const fileParts = [];
12990
+ if (modified) fileParts.push(`${modified} modified`);
12991
+ if (newFiles) fileParts.push(`${newFiles} new`);
12992
+ if (deleted) fileParts.push(`${deleted} deleted`);
12993
+ console.log(` Changed files: ${result.changedFiles.length} (${fileParts.join(", ")})`);
12994
+ console.log("");
12995
+ if (result.isStale) {
12996
+ console.log(` \x1B[33m\u26A0 Results may be stale \u2014 baseline analysis has changed.\x1B[0m`);
12997
+ console.log("");
12841
12998
  }
12842
- async stop() {
12843
- execSync2(`systemctl --user stop ${SERVICE_NAME}`, { stdio: "pipe" });
12999
+ console.log(` Summary: ${result.summary.newCount} new issues, ${result.summary.resolvedCount} resolved`);
13000
+ console.log("");
13001
+ v2.info("Run `truecourse list --diff` to see full details.");
13002
+ }
13003
+ function openInBrowser(url2) {
13004
+ console.log(` Opening ${url2}`);
13005
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
13006
+ exec(`${cmd} ${url2}`);
13007
+ }
13008
+ var DEFAULT_PORT, DEFAULT_CONFIG;
13009
+ var init_helpers = __esm({
13010
+ "tools/cli/src/commands/helpers.ts"() {
13011
+ "use strict";
13012
+ init_dist2();
13013
+ init_esm_debug3();
13014
+ DEFAULT_PORT = 3001;
13015
+ DEFAULT_CONFIG = { runMode: "console" };
12844
13016
  }
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
- }
13017
+ });
13018
+
13019
+ // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
13020
+ var import_index = __toESM(require_commander(), 1);
13021
+ var {
13022
+ program,
13023
+ createCommand,
13024
+ createArgument,
13025
+ createOption,
13026
+ CommanderError,
13027
+ InvalidArgumentError,
13028
+ InvalidOptionArgumentError,
13029
+ // deprecated old name
13030
+ Command,
13031
+ Argument,
13032
+ Option,
13033
+ Help
13034
+ } = import_index.default;
13035
+
13036
+ // tools/cli/src/index.ts
13037
+ init_dist2();
13038
+ import fs7 from "node:fs";
13039
+ import path8 from "node:path";
13040
+ import os6 from "node:os";
13041
+
13042
+ // tools/cli/src/commands/setup.ts
13043
+ init_dist2();
13044
+ import { execSync as execSync4 } from "node:child_process";
13045
+ import fs6 from "node:fs";
13046
+ import path6 from "node:path";
13047
+ import os5 from "node:os";
13048
+ var DEFAULT_MODELS = {
13049
+ anthropic: "claude-sonnet-4-20250514",
13050
+ openai: "gpt-5.3-codex"
13051
+ };
13052
+ function buildEnvContents(config) {
13053
+ const lines = [
13054
+ "# TrueCourse Environment Configuration",
13055
+ `# Generated by truecourse setup on ${(/* @__PURE__ */ new Date()).toISOString()}`,
13056
+ ""
13057
+ ];
13058
+ if (config.provider) {
13059
+ lines.push(`LLM_PROVIDER=${config.provider}`);
12859
13060
  }
12860
- async isInstalled() {
12861
- return fs5.existsSync(UNIT_PATH);
13061
+ if (config.model) {
13062
+ lines.push(`LLM_MODEL=${config.model}`);
12862
13063
  }
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
- );
12877
- }
13064
+ if (config.anthropicKey) {
13065
+ lines.push(`ANTHROPIC_API_KEY=${config.anthropicKey}`);
12878
13066
  }
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
- });
13067
+ if (config.openaiKey) {
13068
+ lines.push(`OPENAI_API_KEY=${config.openaiKey}`);
12900
13069
  }
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
- });
13070
+ if (config.langfusePublicKey && config.langfuseSecretKey) {
13071
+ lines.push("");
13072
+ lines.push("# Langfuse Tracing");
13073
+ lines.push(`LANGFUSE_PUBLIC_KEY=${config.langfusePublicKey}`);
13074
+ lines.push(`LANGFUSE_SECRET_KEY=${config.langfuseSecretKey}`);
12914
13075
  }
12915
- async start() {
12916
- execSync3(`sc.exe start ${SERVICE_NAME2}`, { stdio: "pipe" });
13076
+ lines.push("");
13077
+ return lines.join("\n");
13078
+ }
13079
+ async function runSetup() {
13080
+ we("Welcome to TrueCourse");
13081
+ const provider = await de({
13082
+ message: "Which LLM provider would you like to use?",
13083
+ options: [
13084
+ { value: "claude-code", label: "Claude Code CLI \u2014 no API key needed (Recommended)" },
13085
+ { value: "anthropic", label: "Anthropic (Claude API)" },
13086
+ { value: "openai", label: "OpenAI (GPT API)" },
13087
+ { value: "skip", label: "Skip for now" }
13088
+ ]
13089
+ });
13090
+ if (BD(provider)) {
13091
+ ve("Setup cancelled.");
13092
+ process.exit(0);
12917
13093
  }
12918
- async stop() {
12919
- execSync3(`sc.exe stop ${SERVICE_NAME2}`, { stdio: "pipe" });
13094
+ const config = {};
13095
+ if (provider === "anthropic" || provider === "openai" || provider === "claude-code") {
13096
+ config.provider = provider;
12920
13097
  }
12921
- async status() {
13098
+ if (provider === "claude-code") {
12922
13099
  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
- };
13100
+ execSync4("which claude", { stdio: "ignore" });
12933
13101
  } catch {
12934
- return { running: false };
13102
+ v2.error("Claude Code CLI not found on PATH. Install it first: https://docs.anthropic.com/en/docs/claude-code");
13103
+ process.exit(1);
12935
13104
  }
13105
+ v2.success("Claude Code CLI detected.");
12936
13106
  }
12937
- async isInstalled() {
12938
- try {
12939
- execSync3(`sc.exe query ${SERVICE_NAME2}`, { stdio: "pipe" });
12940
- return true;
12941
- } catch {
12942
- return false;
13107
+ if (provider === "anthropic") {
13108
+ const anthropicKey = await ue({
13109
+ message: "Enter your Anthropic API key:",
13110
+ placeholder: "sk-ant-...",
13111
+ validate(value2) {
13112
+ if (!value2 || value2.trim().length === 0) {
13113
+ return "API key is required";
13114
+ }
13115
+ }
13116
+ });
13117
+ if (BD(anthropicKey)) {
13118
+ ve("Setup cancelled.");
13119
+ process.exit(0);
12943
13120
  }
13121
+ config.anthropicKey = anthropicKey;
12944
13122
  }
12945
- };
12946
-
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
- );
12960
- }
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);
13123
+ if (provider === "openai") {
13124
+ const openaiKey = await ue({
13125
+ message: "Enter your OpenAI API key:",
13126
+ placeholder: "sk-...",
13127
+ validate(value2) {
13128
+ if (!value2 || value2.trim().length === 0) {
13129
+ return "API key is required";
13130
+ }
13131
+ }
13132
+ });
13133
+ if (BD(openaiKey)) {
13134
+ ve("Setup cancelled.");
13135
+ process.exit(0);
12988
13136
  }
13137
+ config.openaiKey = openaiKey;
12989
13138
  }
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);
13139
+ if (provider === "anthropic" || provider === "openai") {
13140
+ const defaultModel = DEFAULT_MODELS[provider];
13141
+ const model = await ue({
13142
+ message: `Enter the model to use (default: ${defaultModel}):`,
13143
+ placeholder: defaultModel
13144
+ });
13145
+ if (BD(model)) {
13146
+ ve("Setup cancelled.");
13147
+ process.exit(0);
13004
13148
  }
13149
+ config.model = model?.trim() || defaultModel;
13005
13150
  }
13006
- fs6.renameSync(logFile, path5.join(logDir, "truecourse.error.log.1"));
13007
- }
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;
13151
+ const useLangfuse = provider !== "claude-code" && await me({
13152
+ message: "Would you like to enable Langfuse tracing?",
13153
+ initialValue: false
13154
+ });
13155
+ if (BD(useLangfuse)) {
13156
+ ve("Setup cancelled.");
13157
+ process.exit(0);
13014
13158
  }
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;
13159
+ if (useLangfuse) {
13160
+ const langfusePublicKey = await ue({
13161
+ message: "Enter your Langfuse public key:",
13162
+ placeholder: "pk-lf-...",
13163
+ validate(value2) {
13164
+ if (!value2 || value2.trim().length === 0) {
13165
+ return "Public key is required";
13166
+ }
13032
13167
  }
13033
13168
  });
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", () => {
13169
+ if (BD(langfusePublicKey)) {
13170
+ ve("Setup cancelled.");
13044
13171
  process.exit(0);
13172
+ }
13173
+ const langfuseSecretKey = await ue({
13174
+ message: "Enter your Langfuse secret key:",
13175
+ placeholder: "sk-lf-...",
13176
+ validate(value2) {
13177
+ if (!value2 || value2.trim().length === 0) {
13178
+ return "Secret key is required";
13179
+ }
13180
+ }
13045
13181
  });
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 {
13182
+ if (BD(langfuseSecretKey)) {
13183
+ ve("Setup cancelled.");
13184
+ process.exit(0);
13061
13185
  }
13062
- await new Promise((r2) => setTimeout(r2, 500));
13063
- }
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;
13076
- }
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);
13088
- }
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");
13186
+ config.langfusePublicKey = langfusePublicKey;
13187
+ config.langfuseSecretKey = langfuseSecretKey;
13095
13188
  }
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 }
13106
- }
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);
13115
- }
13189
+ const configDir = path6.join(os5.homedir(), ".truecourse");
13190
+ fs6.mkdirSync(configDir, { recursive: true });
13191
+ const envPath = path6.join(configDir, ".env");
13192
+ fs6.writeFileSync(envPath, buildEnvContents(config), "utf-8");
13193
+ v2.success(`Configuration saved to ${envPath}`);
13194
+ const runMode = await de({
13195
+ message: "How would you like to run TrueCourse?",
13196
+ options: [
13197
+ { value: "console", label: "Console (keep terminal open)" },
13198
+ { value: "service", label: "Background service (runs automatically, no terminal needed)" }
13199
+ ]
13116
13200
  });
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();
13201
+ if (BD(runMode)) {
13202
+ ve("Setup cancelled.");
13203
+ process.exit(0);
13204
+ }
13205
+ const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_helpers(), helpers_exports));
13206
+ writeConfig2({ runMode });
13207
+ if (runMode === "service") {
13208
+ v2.info("Background service selected. Run `truecourse start` to install and start the service.");
13136
13209
  }
13210
+ v2.info("Embedded PostgreSQL will start automatically when the server runs.");
13211
+ v2.info("Database migrations are applied on server startup.");
13212
+ fe("Setup complete!");
13137
13213
  }
13138
13214
 
13215
+ // tools/cli/src/index.ts
13216
+ init_start();
13217
+
13139
13218
  // tools/cli/src/commands/add.ts
13140
13219
  init_dist2();
13141
13220
  init_helpers();
@@ -13209,7 +13288,7 @@ init_helpers();
13209
13288
  var TIMEOUT_MS = 15 * 60 * 1e3;
13210
13289
  async function runAnalyze() {
13211
13290
  we("Analyzing repository");
13212
- await ensureServer();
13291
+ const firstRun = await ensureServer();
13213
13292
  const repo = await ensureRepo();
13214
13293
  v2.step(`Repository: ${repo.name}`);
13215
13294
  const serverUrl = getServerUrl();
@@ -13274,8 +13353,12 @@ async function runAnalyze() {
13274
13353
  const violations = await res.json();
13275
13354
  renderViolationsSummary(violations);
13276
13355
  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`);
13356
+ if (firstRun) {
13357
+ openInBrowser(repoUrl);
13358
+ fe("Analysis complete \u2014 opened in browser");
13359
+ } else {
13360
+ fe(`Analysis complete \u2014 open ${repoUrl}`);
13361
+ }
13279
13362
  } catch (err) {
13280
13363
  spinner.stop("Analysis failed");
13281
13364
  const message = err instanceof Error ? err.message : String(err);
@@ -13367,9 +13450,11 @@ async function runListDiff() {
13367
13450
 
13368
13451
  // tools/cli/src/commands/service/index.ts
13369
13452
  init_dist2();
13453
+ init_platform();
13454
+ init_logs();
13455
+ init_helpers();
13370
13456
  import path7 from "node:path";
13371
13457
  import { fileURLToPath as fileURLToPath3 } from "node:url";
13372
- init_helpers();
13373
13458
  var __dirname2 = path7.dirname(fileURLToPath3(import.meta.url));
13374
13459
  function getServerPath2() {
13375
13460
  return path7.join(__dirname2, "..", "server.mjs");
@@ -13525,6 +13610,7 @@ function registerServiceCommand(program3) {
13525
13610
 
13526
13611
  // tools/cli/src/index.ts
13527
13612
  init_helpers();
13613
+ init_platform();
13528
13614
  var program2 = new Command();
13529
13615
  program2.name("truecourse").version("0.1.0").description("TrueCourse CLI - Setup and manage your TrueCourse instance");
13530
13616
  program2.command("setup").description("Run the setup wizard to configure TrueCourse").action(async () => {