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