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 +952 -866
- package/db/migrations/0009_sticky_blizzard.sql +16 -0
- package/db/migrations/meta/0009_snapshot.json +2062 -0
- package/db/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/public/assets/{index-DYIYjwei.js → index-CVwZeppe.js} +163 -153
- package/public/assets/index-JxEVjRt1.css +1 -0
- package/public/index.html +2 -2
- package/server.mjs +148561 -144650
- package/public/assets/index-CFKYSTp1.css +0 -1
package/cli.mjs
CHANGED
|
@@ -12185,957 +12185,1036 @@ var init_esm_debug3 = __esm({
|
|
|
12185
12185
|
}
|
|
12186
12186
|
});
|
|
12187
12187
|
|
|
12188
|
-
// tools/cli/src/commands/
|
|
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
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
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
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
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
|
-
|
|
12241
|
-
|
|
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
|
|
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
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
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
|
-
|
|
12321
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
12322
12225
|
}
|
|
12323
|
-
function
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
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
|
-
|
|
12354
|
-
|
|
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
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
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
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
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
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
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
|
-
|
|
12406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
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
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
}
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
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
|
|
12461
|
-
|
|
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
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
DEFAULT_CONFIG = { runMode: "console" };
|
|
12512
|
+
init_macos();
|
|
12513
|
+
init_linux();
|
|
12514
|
+
init_windows();
|
|
12468
12515
|
}
|
|
12469
12516
|
});
|
|
12470
12517
|
|
|
12471
|
-
//
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
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
|
-
|
|
12528
|
-
return lines.join("\n");
|
|
12543
|
+
fs4.renameSync(logFile, path3.join(logDir, "truecourse.log.1"));
|
|
12529
12544
|
}
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
12536
|
-
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
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
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
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 (
|
|
12549
|
-
const
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
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
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
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
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
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
|
-
|
|
12587
|
-
|
|
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
|
-
|
|
12629
|
+
await new Promise((r2) => setTimeout(r2, 500));
|
|
12591
12630
|
}
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
12598
|
-
|
|
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
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
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
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
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
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
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
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
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/
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
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
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
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
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
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
|
|
12702
|
-
const
|
|
12703
|
-
const
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
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
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
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
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
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 (
|
|
12753
|
-
|
|
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
|
-
|
|
12757
|
-
|
|
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
|
-
|
|
12760
|
-
|
|
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
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
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
|
-
|
|
12781
|
-
|
|
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
|
-
|
|
12786
|
-
|
|
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
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
}
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
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
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
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
|
-
|
|
12840
|
-
|
|
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
|
-
|
|
12843
|
-
|
|
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
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
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
|
-
|
|
12861
|
-
|
|
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
|
-
|
|
12880
|
-
|
|
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
|
-
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
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
|
-
|
|
12916
|
-
|
|
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
|
-
|
|
12919
|
-
|
|
13094
|
+
const config = {};
|
|
13095
|
+
if (provider === "anthropic" || provider === "openai" || provider === "claude-code") {
|
|
13096
|
+
config.provider = provider;
|
|
12920
13097
|
}
|
|
12921
|
-
|
|
13098
|
+
if (provider === "claude-code") {
|
|
12922
13099
|
try {
|
|
12923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
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
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
12953
|
-
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
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
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
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
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
if (
|
|
13011
|
-
|
|
13012
|
-
|
|
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 (
|
|
13016
|
-
const
|
|
13017
|
-
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
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
|
-
|
|
13035
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13098
|
-
const
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
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
|
-
|
|
13118
|
-
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
}
|
|
13123
|
-
|
|
13124
|
-
|
|
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
|
-
|
|
13278
|
-
|
|
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 () => {
|