truecourse 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -972,8 +972,8 @@ var require_command = __commonJS({
972
972
  "node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports) {
973
973
  var EventEmitter2 = __require("node:events").EventEmitter;
974
974
  var childProcess = __require("node:child_process");
975
- var path21 = __require("node:path");
976
- var fs16 = __require("node:fs");
975
+ var path22 = __require("node:path");
976
+ var fs17 = __require("node:fs");
977
977
  var process2 = __require("node:process");
978
978
  var { Argument: Argument2, humanReadableArgName } = require_argument();
979
979
  var { CommanderError: CommanderError2 } = require_error();
@@ -1905,11 +1905,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1905
1905
  let launchWithNode = false;
1906
1906
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1907
1907
  function findFile(baseDir, baseName) {
1908
- const localBin = path21.resolve(baseDir, baseName);
1909
- if (fs16.existsSync(localBin)) return localBin;
1910
- if (sourceExt.includes(path21.extname(baseName))) return void 0;
1908
+ const localBin = path22.resolve(baseDir, baseName);
1909
+ if (fs17.existsSync(localBin)) return localBin;
1910
+ if (sourceExt.includes(path22.extname(baseName))) return void 0;
1911
1911
  const foundExt = sourceExt.find(
1912
- (ext2) => fs16.existsSync(`${localBin}${ext2}`)
1912
+ (ext2) => fs17.existsSync(`${localBin}${ext2}`)
1913
1913
  );
1914
1914
  if (foundExt) return `${localBin}${foundExt}`;
1915
1915
  return void 0;
@@ -1921,21 +1921,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1921
1921
  if (this._scriptPath) {
1922
1922
  let resolvedScriptPath;
1923
1923
  try {
1924
- resolvedScriptPath = fs16.realpathSync(this._scriptPath);
1924
+ resolvedScriptPath = fs17.realpathSync(this._scriptPath);
1925
1925
  } catch (err) {
1926
1926
  resolvedScriptPath = this._scriptPath;
1927
1927
  }
1928
- executableDir = path21.resolve(
1929
- path21.dirname(resolvedScriptPath),
1928
+ executableDir = path22.resolve(
1929
+ path22.dirname(resolvedScriptPath),
1930
1930
  executableDir
1931
1931
  );
1932
1932
  }
1933
1933
  if (executableDir) {
1934
1934
  let localFile = findFile(executableDir, executableFile);
1935
1935
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1936
- const legacyName = path21.basename(
1936
+ const legacyName = path22.basename(
1937
1937
  this._scriptPath,
1938
- path21.extname(this._scriptPath)
1938
+ path22.extname(this._scriptPath)
1939
1939
  );
1940
1940
  if (legacyName !== this._name) {
1941
1941
  localFile = findFile(
@@ -1946,7 +1946,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1946
1946
  }
1947
1947
  executableFile = localFile || executableFile;
1948
1948
  }
1949
- launchWithNode = sourceExt.includes(path21.extname(executableFile));
1949
+ launchWithNode = sourceExt.includes(path22.extname(executableFile));
1950
1950
  let proc;
1951
1951
  if (process2.platform !== "win32") {
1952
1952
  if (launchWithNode) {
@@ -2786,7 +2786,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2786
2786
  * @return {Command}
2787
2787
  */
2788
2788
  nameFromFilename(filename) {
2789
- this._name = path21.basename(filename, path21.extname(filename));
2789
+ this._name = path22.basename(filename, path22.extname(filename));
2790
2790
  return this;
2791
2791
  }
2792
2792
  /**
@@ -2800,9 +2800,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2800
2800
  * @param {string} [path]
2801
2801
  * @return {(string|null|Command)}
2802
2802
  */
2803
- executableDir(path22) {
2804
- if (path22 === void 0) return this._executableDir;
2805
- this._executableDir = path22;
2803
+ executableDir(path23) {
2804
+ if (path23 === void 0) return this._executableDir;
2805
+ this._executableDir = path23;
2806
2806
  return this;
2807
2807
  }
2808
2808
  /**
@@ -4062,8 +4062,11 @@ var init_registry = __esm({
4062
4062
  // tools/cli/src/commands/helpers.ts
4063
4063
  var helpers_exports = {};
4064
4064
  __export(helpers_exports, {
4065
+ exitMissingNonInteractiveFlag: () => exitMissingNonInteractiveFlag,
4065
4066
  getConfigPath: () => getConfigPath,
4066
4067
  getServerUrl: () => getServerUrl,
4068
+ hasInstalledSkills: () => hasInstalledSkills,
4069
+ isInteractive: () => isInteractive,
4067
4070
  openInBrowser: () => openInBrowser,
4068
4071
  promptInstallSkills: () => promptInstallSkills,
4069
4072
  readConfig: () => readConfig,
@@ -4316,11 +4319,24 @@ function openInBrowser(url) {
4316
4319
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
4317
4320
  exec2(`${cmd} ${url}`);
4318
4321
  }
4319
- async function promptInstallSkills(repoPath) {
4320
- const installSkills = await ot2({
4321
- message: "Would you like to install Claude Code skills?"
4322
- });
4323
- if (q(installSkills) || !installSkills) return;
4322
+ function skillDestPath(repoPath) {
4323
+ return resolve(repoPath, ".claude", "skills", "truecourse");
4324
+ }
4325
+ function hasInstalledSkills(repoPath) {
4326
+ return existsSync(skillDestPath(repoPath));
4327
+ }
4328
+ function isInteractive() {
4329
+ return !!process.stdin.isTTY;
4330
+ }
4331
+ function exitMissingNonInteractiveFlag(context, flagGuidance) {
4332
+ O2.error(
4333
+ `${context}
4334
+
4335
+ Running non-interactively with no answer. ${flagGuidance}`
4336
+ );
4337
+ process.exit(1);
4338
+ }
4339
+ function copySkillsInto(repoPath) {
4324
4340
  const __dirname4 = dirname(fileURLToPath(import.meta.url));
4325
4341
  const srcPath = resolve(__dirname4, "..", "..", "skills", "truecourse");
4326
4342
  const distPath = resolve(__dirname4, "skills", "truecourse");
@@ -4336,6 +4352,20 @@ async function promptInstallSkills(repoPath) {
4336
4352
  O2.message(" - truecourse-list (list violations)");
4337
4353
  O2.message(" - truecourse-fix (apply fixes)");
4338
4354
  }
4355
+ async function promptInstallSkills(repoPath, { install } = {}) {
4356
+ if (hasInstalledSkills(repoPath)) return;
4357
+ if (install === true) {
4358
+ copySkillsInto(repoPath);
4359
+ return;
4360
+ }
4361
+ if (install === false) return;
4362
+ if (!isInteractive()) return;
4363
+ const answer = await ot2({
4364
+ message: "Would you like to install Claude Code skills?"
4365
+ });
4366
+ if (q(answer) || !answer) return;
4367
+ copySkillsInto(repoPath);
4368
+ }
4339
4369
  var DEFAULT_PORT, DEFAULT_CONFIG;
4340
4370
  var init_helpers = __esm({
4341
4371
  "tools/cli/src/commands/helpers.ts"() {
@@ -4458,6 +4488,26 @@ function writeAnalysis(repoPath, snapshot) {
4458
4488
  atomicWriteJson(analysisFilePath(repoPath, filename), snapshot);
4459
4489
  return { filename, snapshot };
4460
4490
  }
4491
+ function readAnalysis(repoPath, filename) {
4492
+ const file = analysisFilePath(repoPath, filename);
4493
+ if (!fs5.existsSync(file))
4494
+ return null;
4495
+ return JSON.parse(fs5.readFileSync(file, "utf-8"));
4496
+ }
4497
+ function listAnalyses(repoPath) {
4498
+ const dir = analysesDir(repoPath);
4499
+ if (!fs5.existsSync(dir))
4500
+ return [];
4501
+ return fs5.readdirSync(dir).filter((name) => name.endsWith(".json")).sort();
4502
+ }
4503
+ function findAnalysisFilename(repoPath, analysisId) {
4504
+ for (const name of listAnalyses(repoPath).reverse()) {
4505
+ const snap = readAnalysis(repoPath, name);
4506
+ if (snap?.id === analysisId)
4507
+ return name;
4508
+ }
4509
+ return null;
4510
+ }
4461
4511
  function readHistory(repoPath) {
4462
4512
  const file = historyPath(repoPath);
4463
4513
  if (!fs5.existsSync(file))
@@ -5249,10 +5299,10 @@ var require_src3 = __commonJS({
5249
5299
  var fs_1 = __require("fs");
5250
5300
  var debug_1 = __importDefault(require_src2());
5251
5301
  var log2 = debug_1.default("@kwsites/file-exists");
5252
- function check(path21, isFile, isDirectory) {
5253
- log2(`checking %s`, path21);
5302
+ function check(path22, isFile, isDirectory) {
5303
+ log2(`checking %s`, path22);
5254
5304
  try {
5255
- const stat = fs_1.statSync(path21);
5305
+ const stat = fs_1.statSync(path22);
5256
5306
  if (stat.isFile() && isFile) {
5257
5307
  log2(`[OK] path represents a file`);
5258
5308
  return true;
@@ -5272,8 +5322,8 @@ var require_src3 = __commonJS({
5272
5322
  throw e;
5273
5323
  }
5274
5324
  }
5275
- function exists2(path21, type2 = exports.READABLE) {
5276
- return check(path21, (type2 & exports.FILE) > 0, (type2 & exports.FOLDER) > 0);
5325
+ function exists2(path22, type2 = exports.READABLE) {
5326
+ return check(path22, (type2 & exports.FILE) > 0, (type2 & exports.FOLDER) > 0);
5277
5327
  }
5278
5328
  exports.exists = exists2;
5279
5329
  exports.FILE = 1;
@@ -5346,8 +5396,8 @@ function pathspec(...paths) {
5346
5396
  cache.set(key, paths);
5347
5397
  return key;
5348
5398
  }
5349
- function isPathSpec(path21) {
5350
- return path21 instanceof String && cache.has(path21);
5399
+ function isPathSpec(path22) {
5400
+ return path22 instanceof String && cache.has(path22);
5351
5401
  }
5352
5402
  function toPaths(pathSpec) {
5353
5403
  return cache.get(pathSpec) || [];
@@ -5391,8 +5441,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
5391
5441
  function forEachLineWithContent(input, callback) {
5392
5442
  return toLinesWithContent(input, true).map((line) => callback(line));
5393
5443
  }
5394
- function folderExists(path21) {
5395
- return (0, import_file_exists.exists)(path21, import_file_exists.FOLDER);
5444
+ function folderExists(path22) {
5445
+ return (0, import_file_exists.exists)(path22, import_file_exists.FOLDER);
5396
5446
  }
5397
5447
  function append(target, item) {
5398
5448
  if (Array.isArray(target)) {
@@ -5583,8 +5633,8 @@ function checkIsRepoRootTask() {
5583
5633
  commands,
5584
5634
  format: "utf-8",
5585
5635
  onError,
5586
- parser(path21) {
5587
- return /^\.(git)?$/.test(path21.trim());
5636
+ parser(path22) {
5637
+ return /^\.(git)?$/.test(path22.trim());
5588
5638
  }
5589
5639
  };
5590
5640
  }
@@ -5840,11 +5890,11 @@ function parseGrep(grep) {
5840
5890
  const paths = /* @__PURE__ */ new Set();
5841
5891
  const results = {};
5842
5892
  forEachLineWithContent(grep, (input) => {
5843
- const [path21, line, preview] = input.split(NULL);
5844
- paths.add(path21);
5845
- (results[path21] = results[path21] || []).push({
5893
+ const [path22, line, preview] = input.split(NULL);
5894
+ paths.add(path22);
5895
+ (results[path22] = results[path22] || []).push({
5846
5896
  line: asNumber(line),
5847
- path: path21,
5897
+ path: path22,
5848
5898
  preview
5849
5899
  });
5850
5900
  });
@@ -6141,14 +6191,14 @@ function hashObjectTask(filePath, write) {
6141
6191
  }
6142
6192
  return straightThroughStringTask(commands, true);
6143
6193
  }
6144
- function parseInit(bare, path21, text) {
6194
+ function parseInit(bare, path22, text) {
6145
6195
  const response = String(text).trim();
6146
6196
  let result;
6147
6197
  if (result = initResponseRegex.exec(response)) {
6148
- return new InitSummary(bare, path21, false, result[1]);
6198
+ return new InitSummary(bare, path22, false, result[1]);
6149
6199
  }
6150
6200
  if (result = reInitResponseRegex.exec(response)) {
6151
- return new InitSummary(bare, path21, true, result[1]);
6201
+ return new InitSummary(bare, path22, true, result[1]);
6152
6202
  }
6153
6203
  let gitDir = "";
6154
6204
  const tokens = response.split(" ");
@@ -6159,12 +6209,12 @@ function parseInit(bare, path21, text) {
6159
6209
  break;
6160
6210
  }
6161
6211
  }
6162
- return new InitSummary(bare, path21, /^re/i.test(response), gitDir);
6212
+ return new InitSummary(bare, path22, /^re/i.test(response), gitDir);
6163
6213
  }
6164
6214
  function hasBareCommand(command) {
6165
6215
  return command.includes(bareCommand);
6166
6216
  }
6167
- function initTask(bare = false, path21, customArgs) {
6217
+ function initTask(bare = false, path22, customArgs) {
6168
6218
  const commands = ["init", ...customArgs];
6169
6219
  if (bare && !hasBareCommand(commands)) {
6170
6220
  commands.splice(1, 0, bareCommand);
@@ -6173,7 +6223,7 @@ function initTask(bare = false, path21, customArgs) {
6173
6223
  commands,
6174
6224
  format: "utf-8",
6175
6225
  parser(text) {
6176
- return parseInit(commands.includes("--bare"), path21, text);
6226
+ return parseInit(commands.includes("--bare"), path22, text);
6177
6227
  }
6178
6228
  };
6179
6229
  }
@@ -6452,14 +6502,14 @@ function splitLine(result, lineStr) {
6452
6502
  default:
6453
6503
  return;
6454
6504
  }
6455
- function data(index, workingDir, path21) {
6505
+ function data(index, workingDir, path22) {
6456
6506
  const raw = `${index}${workingDir}`;
6457
6507
  const handler = parsers6.get(raw);
6458
6508
  if (handler) {
6459
- handler(result, path21);
6509
+ handler(result, path22);
6460
6510
  }
6461
6511
  if (raw !== "##" && raw !== "!!") {
6462
- result.files.push(new FileStatusSummary(path21, index, workingDir));
6512
+ result.files.push(new FileStatusSummary(path22, index, workingDir));
6463
6513
  }
6464
6514
  }
6465
6515
  }
@@ -6646,8 +6696,8 @@ function deleteBranchTask(branch, forceDelete = false) {
6646
6696
  return task;
6647
6697
  }
6648
6698
  function toPath(input) {
6649
- const path21 = input.trim().replace(/^["']|["']$/g, "");
6650
- return path21 && normalize(path21);
6699
+ const path22 = input.trim().replace(/^["']|["']$/g, "");
6700
+ return path22 && normalize(path22);
6651
6701
  }
6652
6702
  function checkIgnoreTask(paths) {
6653
6703
  return {
@@ -6786,8 +6836,8 @@ function stashListTask(opt = {}, customArgs) {
6786
6836
  parser: parser4
6787
6837
  };
6788
6838
  }
6789
- function addSubModuleTask(repo, path21) {
6790
- return subModuleTask(["add", repo, path21]);
6839
+ function addSubModuleTask(repo, path22) {
6840
+ return subModuleTask(["add", repo, path22]);
6791
6841
  }
6792
6842
  function initSubModuleTask(customArgs) {
6793
6843
  return subModuleTask(["init", ...customArgs]);
@@ -8099,9 +8149,9 @@ var init_esm = __esm({
8099
8149
  "src/lib/responses/InitSummary.ts"() {
8100
8150
  "use strict";
8101
8151
  InitSummary = class {
8102
- constructor(bare, path21, existing, gitDir) {
8152
+ constructor(bare, path22, existing, gitDir) {
8103
8153
  this.bare = bare;
8104
- this.path = path21;
8154
+ this.path = path22;
8105
8155
  this.existing = existing;
8106
8156
  this.gitDir = gitDir;
8107
8157
  }
@@ -8635,12 +8685,12 @@ var init_esm = __esm({
8635
8685
  "use strict";
8636
8686
  fromPathRegex = /^(.+)\0(.+)$/;
8637
8687
  FileStatusSummary = class {
8638
- constructor(path21, index, working_dir) {
8639
- this.path = path21;
8688
+ constructor(path22, index, working_dir) {
8689
+ this.path = path22;
8640
8690
  this.index = index;
8641
8691
  this.working_dir = working_dir;
8642
8692
  if (index === "R" || working_dir === "R") {
8643
- const detail = fromPathRegex.exec(path21) || [null, path21, path21];
8693
+ const detail = fromPathRegex.exec(path22) || [null, path22, path22];
8644
8694
  this.from = detail[2] || "";
8645
8695
  this.path = detail[1] || "";
8646
8696
  }
@@ -8903,9 +8953,9 @@ var init_esm = __esm({
8903
8953
  next
8904
8954
  );
8905
8955
  }
8906
- hashObject(path21, write) {
8956
+ hashObject(path22, write) {
8907
8957
  return this._runTask(
8908
- hashObjectTask(path21, write === true),
8958
+ hashObjectTask(path22, write === true),
8909
8959
  trailingFunctionArgument(arguments)
8910
8960
  );
8911
8961
  }
@@ -9542,8 +9592,8 @@ var init_esm = __esm({
9542
9592
  }
9543
9593
  return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
9544
9594
  };
9545
- Git2.prototype.submoduleAdd = function(repo, path21, then) {
9546
- return this._runTask(addSubModuleTask2(repo, path21), trailingFunctionArgument2(arguments));
9595
+ Git2.prototype.submoduleAdd = function(repo, path22, then) {
9596
+ return this._runTask(addSubModuleTask2(repo, path22), trailingFunctionArgument2(arguments));
9547
9597
  };
9548
9598
  Git2.prototype.submoduleUpdate = function(args, then) {
9549
9599
  return this._runTask(
@@ -10164,7 +10214,7 @@ var require_ignore = __commonJS({
10164
10214
  // path matching.
10165
10215
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
10166
10216
  // @returns {TestResult} true if a file is ignored
10167
- test(path21, checkUnignored, mode) {
10217
+ test(path22, checkUnignored, mode) {
10168
10218
  let ignored = false;
10169
10219
  let unignored = false;
10170
10220
  let matchedRule;
@@ -10173,7 +10223,7 @@ var require_ignore = __commonJS({
10173
10223
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
10174
10224
  return;
10175
10225
  }
10176
- const matched = rule[mode].test(path21);
10226
+ const matched = rule[mode].test(path22);
10177
10227
  if (!matched) {
10178
10228
  return;
10179
10229
  }
@@ -10194,17 +10244,17 @@ var require_ignore = __commonJS({
10194
10244
  var throwError2 = (message, Ctor) => {
10195
10245
  throw new Ctor(message);
10196
10246
  };
10197
- var checkPath = (path21, originalPath, doThrow) => {
10198
- if (!isString(path21)) {
10247
+ var checkPath = (path22, originalPath, doThrow) => {
10248
+ if (!isString(path22)) {
10199
10249
  return doThrow(
10200
10250
  `path must be a string, but got \`${originalPath}\``,
10201
10251
  TypeError
10202
10252
  );
10203
10253
  }
10204
- if (!path21) {
10254
+ if (!path22) {
10205
10255
  return doThrow(`path must not be empty`, TypeError);
10206
10256
  }
10207
- if (checkPath.isNotRelative(path21)) {
10257
+ if (checkPath.isNotRelative(path22)) {
10208
10258
  const r = "`path.relative()`d";
10209
10259
  return doThrow(
10210
10260
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -10213,7 +10263,7 @@ var require_ignore = __commonJS({
10213
10263
  }
10214
10264
  return true;
10215
10265
  };
10216
- var isNotRelative = (path21) => REGEX_TEST_INVALID_PATH.test(path21);
10266
+ var isNotRelative = (path22) => REGEX_TEST_INVALID_PATH.test(path22);
10217
10267
  checkPath.isNotRelative = isNotRelative;
10218
10268
  checkPath.convert = (p2) => p2;
10219
10269
  var Ignore = class {
@@ -10243,19 +10293,19 @@ var require_ignore = __commonJS({
10243
10293
  }
10244
10294
  // @returns {TestResult}
10245
10295
  _test(originalPath, cache2, checkUnignored, slices) {
10246
- const path21 = originalPath && checkPath.convert(originalPath);
10296
+ const path22 = originalPath && checkPath.convert(originalPath);
10247
10297
  checkPath(
10248
- path21,
10298
+ path22,
10249
10299
  originalPath,
10250
10300
  this._strictPathCheck ? throwError2 : RETURN_FALSE
10251
10301
  );
10252
- return this._t(path21, cache2, checkUnignored, slices);
10302
+ return this._t(path22, cache2, checkUnignored, slices);
10253
10303
  }
10254
- checkIgnore(path21) {
10255
- if (!REGEX_TEST_TRAILING_SLASH.test(path21)) {
10256
- return this.test(path21);
10304
+ checkIgnore(path22) {
10305
+ if (!REGEX_TEST_TRAILING_SLASH.test(path22)) {
10306
+ return this.test(path22);
10257
10307
  }
10258
- const slices = path21.split(SLASH).filter(Boolean);
10308
+ const slices = path22.split(SLASH).filter(Boolean);
10259
10309
  slices.pop();
10260
10310
  if (slices.length) {
10261
10311
  const parent = this._t(
@@ -10268,18 +10318,18 @@ var require_ignore = __commonJS({
10268
10318
  return parent;
10269
10319
  }
10270
10320
  }
10271
- return this._rules.test(path21, false, MODE_CHECK_IGNORE);
10321
+ return this._rules.test(path22, false, MODE_CHECK_IGNORE);
10272
10322
  }
10273
- _t(path21, cache2, checkUnignored, slices) {
10274
- if (path21 in cache2) {
10275
- return cache2[path21];
10323
+ _t(path22, cache2, checkUnignored, slices) {
10324
+ if (path22 in cache2) {
10325
+ return cache2[path22];
10276
10326
  }
10277
10327
  if (!slices) {
10278
- slices = path21.split(SLASH).filter(Boolean);
10328
+ slices = path22.split(SLASH).filter(Boolean);
10279
10329
  }
10280
10330
  slices.pop();
10281
10331
  if (!slices.length) {
10282
- return cache2[path21] = this._rules.test(path21, checkUnignored, MODE_IGNORE);
10332
+ return cache2[path22] = this._rules.test(path22, checkUnignored, MODE_IGNORE);
10283
10333
  }
10284
10334
  const parent = this._t(
10285
10335
  slices.join(SLASH) + SLASH,
@@ -10287,29 +10337,29 @@ var require_ignore = __commonJS({
10287
10337
  checkUnignored,
10288
10338
  slices
10289
10339
  );
10290
- return cache2[path21] = parent.ignored ? parent : this._rules.test(path21, checkUnignored, MODE_IGNORE);
10340
+ return cache2[path22] = parent.ignored ? parent : this._rules.test(path22, checkUnignored, MODE_IGNORE);
10291
10341
  }
10292
- ignores(path21) {
10293
- return this._test(path21, this._ignoreCache, false).ignored;
10342
+ ignores(path22) {
10343
+ return this._test(path22, this._ignoreCache, false).ignored;
10294
10344
  }
10295
10345
  createFilter() {
10296
- return (path21) => !this.ignores(path21);
10346
+ return (path22) => !this.ignores(path22);
10297
10347
  }
10298
10348
  filter(paths) {
10299
10349
  return makeArray(paths).filter(this.createFilter());
10300
10350
  }
10301
10351
  // @returns {TestResult}
10302
- test(path21) {
10303
- return this._test(path21, this._testCache, true);
10352
+ test(path22) {
10353
+ return this._test(path22, this._testCache, true);
10304
10354
  }
10305
10355
  };
10306
10356
  var factory = (options) => new Ignore(options);
10307
- var isPathValid = (path21) => checkPath(path21 && checkPath.convert(path21), path21, RETURN_FALSE);
10357
+ var isPathValid = (path22) => checkPath(path22 && checkPath.convert(path22), path22, RETURN_FALSE);
10308
10358
  var setupWindows = () => {
10309
10359
  const makePosix = (str2) => /^\\\\\?\\/.test(str2) || /["<>|\u0000-\u001F]+/u.test(str2) ? str2 : str2.replace(/\\/g, "/");
10310
10360
  checkPath.convert = makePosix;
10311
10361
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
10312
- checkPath.isNotRelative = (path21) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path21) || isNotRelative(path21);
10362
+ checkPath.isNotRelative = (path22) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path22) || isNotRelative(path22);
10313
10363
  };
10314
10364
  if (
10315
10365
  // Detect `process` so that it can run in browsers.
@@ -10589,30 +10639,52 @@ var init_file_discovery = __esm({
10589
10639
  });
10590
10640
 
10591
10641
  // packages/analyzer/dist/parser.js
10592
- import Parser from "tree-sitter";
10593
- import TypeScriptParser from "tree-sitter-typescript";
10594
- import JavaScriptParser from "tree-sitter-javascript";
10595
- import PythonParser from "tree-sitter-python";
10596
- function getTreeSitterLanguage(language) {
10597
- switch (language) {
10598
- case "typescript":
10599
- return TypeScriptParser.typescript;
10600
- case "tsx":
10601
- return TypeScriptParser.tsx;
10602
- case "javascript":
10603
- return JavaScriptParser;
10604
- case "python":
10605
- return PythonParser;
10606
- default:
10607
- throw new Error(`Unsupported language: ${language}`);
10642
+ import { Parser, Language } from "web-tree-sitter";
10643
+ import { createRequire as _createRequire } from "node:module";
10644
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
10645
+ import path7 from "node:path";
10646
+ import fs8 from "node:fs";
10647
+ function resolveWasmPath(subpath) {
10648
+ const bundled = path7.join(BUNDLED_WASM_DIR, path7.basename(subpath));
10649
+ if (fs8.existsSync(bundled))
10650
+ return bundled;
10651
+ return _require.resolve(subpath);
10652
+ }
10653
+ function initParsers() {
10654
+ if (!initPromise) {
10655
+ initPromise = doInit().then(() => {
10656
+ initialized = true;
10657
+ });
10658
+ }
10659
+ return initPromise;
10660
+ }
10661
+ async function doInit() {
10662
+ await Parser.init({
10663
+ locateFile(file) {
10664
+ return resolveWasmPath(`web-tree-sitter/${file}`);
10665
+ }
10666
+ });
10667
+ await Promise.all(Object.keys(GRAMMAR_WASM).map(async (lang) => {
10668
+ const wasmPath = resolveWasmPath(GRAMMAR_WASM[lang]);
10669
+ const language = await Language.load(wasmPath);
10670
+ languageCache.set(lang, language);
10671
+ }));
10672
+ }
10673
+ function assertInitialized() {
10674
+ if (!initialized) {
10675
+ throw new Error("tree-sitter parsers are not loaded. Call `await initParsers()` from @truecourse/analyzer before using parseCode / parseFile / getParser. This is required once per process \u2014 the call is idempotent.");
10608
10676
  }
10609
10677
  }
10610
10678
  function getParser(language) {
10679
+ if (!(language in GRAMMAR_WASM)) {
10680
+ throw new Error(`Unsupported language: ${language}`);
10681
+ }
10682
+ assertInitialized();
10611
10683
  let parser4 = parserCache.get(language);
10612
10684
  if (!parser4) {
10685
+ const lang = languageCache.get(language);
10613
10686
  parser4 = new Parser();
10614
- const tsLanguage = getTreeSitterLanguage(language);
10615
- parser4.setLanguage(tsLanguage);
10687
+ parser4.setLanguage(lang);
10616
10688
  parserCache.set(language, parser4);
10617
10689
  }
10618
10690
  return parser4;
@@ -10632,11 +10704,22 @@ function parseFile(filePath, code, language) {
10632
10704
  throw new Error(`Failed to parse file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
10633
10705
  }
10634
10706
  }
10635
- var parserCache;
10707
+ var _require, BUNDLED_WASM_DIR, GRAMMAR_WASM, languageCache, parserCache, initPromise, initialized;
10636
10708
  var init_parser = __esm({
10637
10709
  "packages/analyzer/dist/parser.js"() {
10638
10710
  "use strict";
10711
+ _require = _createRequire(import.meta.url);
10712
+ BUNDLED_WASM_DIR = path7.join(path7.dirname(fileURLToPath2(import.meta.url)), "wasm");
10713
+ GRAMMAR_WASM = {
10714
+ typescript: "tree-sitter-typescript/tree-sitter-typescript.wasm",
10715
+ tsx: "tree-sitter-typescript/tree-sitter-tsx.wasm",
10716
+ javascript: "tree-sitter-javascript/tree-sitter-javascript.wasm",
10717
+ python: "tree-sitter-python/tree-sitter-python.wasm"
10718
+ };
10719
+ languageCache = /* @__PURE__ */ new Map();
10639
10720
  parserCache = /* @__PURE__ */ new Map();
10721
+ initPromise = null;
10722
+ initialized = false;
10640
10723
  }
10641
10724
  });
10642
10725
 
@@ -11575,18 +11658,18 @@ function extractRouterMount(callNode, filePath) {
11575
11658
  const routerName = args[0]?.type === "identifier" ? args[0].text : null;
11576
11659
  if (!routerName)
11577
11660
  return null;
11578
- let path21 = "/";
11661
+ let path22 = "/";
11579
11662
  for (const arg of args) {
11580
11663
  if (arg.type === "keyword_argument") {
11581
11664
  const key = arg.childForFieldName("name")?.text;
11582
11665
  const value = arg.childForFieldName("value");
11583
11666
  if ((key === "url_prefix" || key === "prefix") && value) {
11584
- path21 = value.text.replace(/^["']|["']$/g, "");
11667
+ path22 = value.text.replace(/^["']|["']$/g, "");
11585
11668
  }
11586
11669
  }
11587
11670
  }
11588
11671
  return {
11589
- path: path21,
11672
+ path: path22,
11590
11673
  routerName,
11591
11674
  location: {
11592
11675
  filePath,
@@ -11610,11 +11693,11 @@ function extractChainedRoute(outerCall, filePath) {
11610
11693
  const innerArgs = innerCall.childForFieldName("arguments");
11611
11694
  if (!innerArgs)
11612
11695
  return null;
11613
- let path21 = null;
11696
+ let path22 = null;
11614
11697
  let httpMethod = methodName === "route" ? "GET" : methodName.toUpperCase();
11615
11698
  for (const arg of innerArgs.namedChildren) {
11616
- if (!path21 && arg.type === "string") {
11617
- path21 = arg.text.replace(/^["']|["']$/g, "");
11699
+ if (!path22 && arg.type === "string") {
11700
+ path22 = arg.text.replace(/^["']|["']$/g, "");
11618
11701
  }
11619
11702
  if (arg.type === "keyword_argument") {
11620
11703
  const key = arg.childForFieldName("name")?.text;
@@ -11626,14 +11709,14 @@ function extractChainedRoute(outerCall, filePath) {
11626
11709
  }
11627
11710
  }
11628
11711
  }
11629
- if (!path21)
11712
+ if (!path22)
11630
11713
  return null;
11631
11714
  const outerArgs = outerCall.childForFieldName("arguments");
11632
11715
  const handlerArg = outerArgs?.namedChildren[0];
11633
11716
  const handlerName = handlerArg?.type === "identifier" ? handlerArg.text : "anonymous";
11634
11717
  return {
11635
11718
  httpMethod,
11636
- path: path21,
11719
+ path: path22,
11637
11720
  handlerName,
11638
11721
  location: {
11639
11722
  filePath,
@@ -11709,8 +11792,8 @@ function extractRoute(methodName, argsNode, filePath, callNode) {
11709
11792
  const firstArg = argsNode.namedChild(0);
11710
11793
  if (!firstArg)
11711
11794
  return null;
11712
- const path21 = extractStringLiteral(firstArg);
11713
- if (!path21)
11795
+ const path22 = extractStringLiteral(firstArg);
11796
+ if (!path22)
11714
11797
  return null;
11715
11798
  const argCount = argsNode.namedChildCount;
11716
11799
  if (argCount < 2)
@@ -11723,7 +11806,7 @@ function extractRoute(methodName, argsNode, filePath, callNode) {
11723
11806
  return null;
11724
11807
  return {
11725
11808
  httpMethod: methodName.toUpperCase(),
11726
- path: path21,
11809
+ path: path22,
11727
11810
  handlerName,
11728
11811
  location: {
11729
11812
  filePath,
@@ -11740,8 +11823,8 @@ function extractMount(argsNode, filePath, callNode) {
11740
11823
  const firstArg = argsNode.namedChild(0);
11741
11824
  if (!firstArg)
11742
11825
  return null;
11743
- const path21 = extractStringLiteral(firstArg);
11744
- if (!path21)
11826
+ const path22 = extractStringLiteral(firstArg);
11827
+ if (!path22)
11745
11828
  return null;
11746
11829
  const secondArg = argsNode.namedChild(1);
11747
11830
  if (!secondArg)
@@ -11750,7 +11833,7 @@ function extractMount(argsNode, filePath, callNode) {
11750
11833
  if (!routerName)
11751
11834
  return null;
11752
11835
  return {
11753
- path: path21,
11836
+ path: path22,
11754
11837
  routerName,
11755
11838
  location: {
11756
11839
  filePath,
@@ -11871,7 +11954,7 @@ var init_common = __esm({
11871
11954
  });
11872
11955
 
11873
11956
  // packages/analyzer/dist/extractors/languages/typescript.js
11874
- import Parser2 from "tree-sitter";
11957
+ import { Query as Query2 } from "web-tree-sitter";
11875
11958
  function isNestedInFunction(node) {
11876
11959
  let current = node.parent;
11877
11960
  while (current) {
@@ -12167,8 +12250,10 @@ function extractTypeScriptFunctions(tree, filePath, language = "typescript") {
12167
12250
  const seenLocations = /* @__PURE__ */ new Set();
12168
12251
  const queryString = config2.functionQuery || config2.functionNodeTypes.map((type2) => `(${type2}) @function`).join("\n");
12169
12252
  const parser4 = getParser(language);
12170
- const tsLanguage = parser4.getLanguage();
12171
- const query = new Parser2.Query(tsLanguage, queryString);
12253
+ const tsLanguage = parser4.language;
12254
+ if (!tsLanguage)
12255
+ throw new Error(`parser for '${language}' has no language set`);
12256
+ const query = new Query2(tsLanguage, queryString);
12172
12257
  const captures = query.captures(tree.rootNode);
12173
12258
  for (const capture of captures) {
12174
12259
  const node = capture.node;
@@ -12206,8 +12291,10 @@ function extractTypeScriptClasses(tree, filePath, language = "typescript") {
12206
12291
  const classes = [];
12207
12292
  const queryString = config2.classQuery || config2.classNodeTypes.map((type2) => `(${type2}) @class`).join("\n");
12208
12293
  const parser4 = getParser(language);
12209
- const tsLanguage = parser4.getLanguage();
12210
- const query = new Parser2.Query(tsLanguage, queryString);
12294
+ const tsLanguage = parser4.language;
12295
+ if (!tsLanguage)
12296
+ throw new Error(`parser for '${language}' has no language set`);
12297
+ const query = new Query2(tsLanguage, queryString);
12211
12298
  const captures = query.captures(tree.rootNode);
12212
12299
  for (const capture of captures) {
12213
12300
  const node = capture.node;
@@ -12246,8 +12333,10 @@ function extractTypeScriptImports(tree, filePath, language = "typescript") {
12246
12333
  const imports = [];
12247
12334
  const queryString = config2.importQuery || config2.importNodeTypes.map((type2) => `(${type2}) @import`).join("\n");
12248
12335
  const parser4 = getParser(language);
12249
- const tsLanguage = parser4.getLanguage();
12250
- const query = new Parser2.Query(tsLanguage, queryString);
12336
+ const tsLanguage = parser4.language;
12337
+ if (!tsLanguage)
12338
+ throw new Error(`parser for '${language}' has no language set`);
12339
+ const query = new Query2(tsLanguage, queryString);
12251
12340
  const captures = query.captures(tree.rootNode);
12252
12341
  for (const capture of captures) {
12253
12342
  const node = capture.node;
@@ -12272,8 +12361,10 @@ function extractTypeScriptExports(tree, _filePath, language = "typescript") {
12272
12361
  const exports = [];
12273
12362
  const queryString = config2.exportQuery || config2.exportNodeTypes.map((type2) => `(${type2}) @export`).join("\n");
12274
12363
  const parser4 = getParser(language);
12275
- const tsLanguage = parser4.getLanguage();
12276
- const query = new Parser2.Query(tsLanguage, queryString);
12364
+ const tsLanguage = parser4.language;
12365
+ if (!tsLanguage)
12366
+ throw new Error(`parser for '${language}' has no language set`);
12367
+ const query = new Query2(tsLanguage, queryString);
12277
12368
  const captures = query.captures(tree.rootNode);
12278
12369
  for (const capture of captures) {
12279
12370
  const node = capture.node;
@@ -12312,7 +12403,7 @@ var init_typescript = __esm({
12312
12403
  });
12313
12404
 
12314
12405
  // packages/analyzer/dist/extractors/languages/javascript.js
12315
- import Parser3 from "tree-sitter";
12406
+ import { Query as Query3 } from "web-tree-sitter";
12316
12407
  function extractFunctionName2(node) {
12317
12408
  const nameNode = node.childForFieldName("name");
12318
12409
  if (nameNode?.text)
@@ -12394,8 +12485,10 @@ function extractJavaScriptFunctions(tree, filePath) {
12394
12485
  const functions = [];
12395
12486
  const queryString = config2.functionQuery || config2.functionNodeTypes.map((type2) => `(${type2}) @function`).join("\n");
12396
12487
  const parser4 = getParser("javascript");
12397
- const jsLanguage = parser4.getLanguage();
12398
- const query = new Parser3.Query(jsLanguage, queryString);
12488
+ const jsLanguage = parser4.language;
12489
+ if (!jsLanguage)
12490
+ throw new Error("parser has no language set");
12491
+ const query = new Query3(jsLanguage, queryString);
12399
12492
  const captures = query.captures(tree.rootNode);
12400
12493
  for (const capture of captures) {
12401
12494
  const node = capture.node;
@@ -12530,8 +12623,10 @@ function extractJavaScriptClasses(tree, filePath) {
12530
12623
  const classes = [];
12531
12624
  const queryString = config2.classQuery || config2.classNodeTypes.map((type2) => `(${type2}) @class`).join("\n");
12532
12625
  const parser4 = getParser("javascript");
12533
- const jsLanguage = parser4.getLanguage();
12534
- const query = new Parser3.Query(jsLanguage, queryString);
12626
+ const jsLanguage = parser4.language;
12627
+ if (!jsLanguage)
12628
+ throw new Error("parser has no language set");
12629
+ const query = new Query3(jsLanguage, queryString);
12535
12630
  const captures = query.captures(tree.rootNode);
12536
12631
  for (const capture of captures) {
12537
12632
  const node = capture.node;
@@ -12615,8 +12710,10 @@ function extractJavaScriptImports(tree, _filePath) {
12615
12710
  const imports = [];
12616
12711
  const queryString = config2.importQuery || config2.importNodeTypes.map((type2) => `(${type2}) @import`).join("\n");
12617
12712
  const parser4 = getParser("javascript");
12618
- const jsLanguage = parser4.getLanguage();
12619
- const query = new Parser3.Query(jsLanguage, queryString);
12713
+ const jsLanguage = parser4.language;
12714
+ if (!jsLanguage)
12715
+ throw new Error("parser has no language set");
12716
+ const query = new Query3(jsLanguage, queryString);
12620
12717
  const captures = query.captures(tree.rootNode);
12621
12718
  for (const capture of captures) {
12622
12719
  const node = capture.node;
@@ -12672,8 +12769,10 @@ function extractJavaScriptExports(tree, _filePath) {
12672
12769
  const exports = [];
12673
12770
  const queryString = config2.exportQuery || config2.exportNodeTypes.map((type2) => `(${type2}) @export`).join("\n");
12674
12771
  const parser4 = getParser("javascript");
12675
- const jsLanguage = parser4.getLanguage();
12676
- const query = new Parser3.Query(jsLanguage, queryString);
12772
+ const jsLanguage = parser4.language;
12773
+ if (!jsLanguage)
12774
+ throw new Error("parser has no language set");
12775
+ const query = new Query3(jsLanguage, queryString);
12677
12776
  const captures = query.captures(tree.rootNode);
12678
12777
  for (const capture of captures) {
12679
12778
  const node = capture.node;
@@ -15026,7 +15125,7 @@ var init_escape = __esm({
15026
15125
  });
15027
15126
 
15028
15127
  // node_modules/.pnpm/minimatch@10.2.4/node_modules/minimatch/dist/esm/index.js
15029
- var minimatch, starDotExtRE, starDotExtTest, starDotExtTestDot, starDotExtTestNocase, starDotExtTestNocaseDot, starDotStarRE, starDotStarTest, starDotStarTestDot, dotStarRE, dotStarTest, starRE, starTest, starTestDot, qmarksRE, qmarksTestNocase, qmarksTestNocaseDot, qmarksTestDot, qmarksTest, qmarksTestNoExt, qmarksTestNoExtDot, defaultPlatform, path7, sep, GLOBSTAR, qmark2, star2, twoStarDot, twoStarNoDot, filter, ext, defaults, braceExpand, makeRe, match, globMagic, regExpEscape2, Minimatch;
15128
+ var minimatch, starDotExtRE, starDotExtTest, starDotExtTestDot, starDotExtTestNocase, starDotExtTestNocaseDot, starDotStarRE, starDotStarTest, starDotStarTestDot, dotStarRE, dotStarTest, starRE, starTest, starTestDot, qmarksRE, qmarksTestNocase, qmarksTestNocaseDot, qmarksTestDot, qmarksTest, qmarksTestNoExt, qmarksTestNoExtDot, defaultPlatform, path8, sep, GLOBSTAR, qmark2, star2, twoStarDot, twoStarNoDot, filter, ext, defaults, braceExpand, makeRe, match, globMagic, regExpEscape2, Minimatch;
15030
15129
  var init_esm4 = __esm({
15031
15130
  "node_modules/.pnpm/minimatch@10.2.4/node_modules/minimatch/dist/esm/index.js"() {
15032
15131
  init_esm3();
@@ -15095,11 +15194,11 @@ var init_esm4 = __esm({
15095
15194
  return (f) => f.length === len && f !== "." && f !== "..";
15096
15195
  };
15097
15196
  defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
15098
- path7 = {
15197
+ path8 = {
15099
15198
  win32: { sep: "\\" },
15100
15199
  posix: { sep: "/" }
15101
15200
  };
15102
- sep = defaultPlatform === "win32" ? path7.win32.sep : path7.posix.sep;
15201
+ sep = defaultPlatform === "win32" ? path8.win32.sep : path8.posix.sep;
15103
15202
  minimatch.sep = sep;
15104
15203
  GLOBSTAR = Symbol("globstar **");
15105
15204
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -16274,9 +16373,9 @@ function detectDockerComposeServices(rootPath, allFiles) {
16274
16373
  ];
16275
16374
  let composePath2 = null;
16276
16375
  for (const file of composeFiles) {
16277
- const path21 = join6(rootPath, file);
16278
- if (existsSync7(path21)) {
16279
- composePath2 = path21;
16376
+ const path22 = join6(rootPath, file);
16377
+ if (existsSync7(path22)) {
16378
+ composePath2 = path22;
16280
16379
  break;
16281
16380
  }
16282
16381
  }
@@ -17145,7 +17244,7 @@ var init_database_detector = __esm({
17145
17244
  });
17146
17245
 
17147
17246
  // packages/analyzer/dist/module-extractor.js
17148
- import path8 from "path";
17247
+ import path9 from "path";
17149
17248
  function extractModulesAndMethods(analyses, layerDetails, fileDependencies) {
17150
17249
  const modules = [];
17151
17250
  const methods = [];
@@ -17292,7 +17391,7 @@ function deriveModuleName(analysis) {
17292
17391
  return localExports[0].name;
17293
17392
  }
17294
17393
  const baseName = fileBaseName(analysis.filePath);
17295
- const strippedName = stripExtension(path8.basename(analysis.filePath));
17394
+ const strippedName = stripExtension(path9.basename(analysis.filePath));
17296
17395
  if (INDEX_NAMES.has(strippedName)) {
17297
17396
  return baseName;
17298
17397
  }
@@ -17309,10 +17408,10 @@ function deriveModuleName(analysis) {
17309
17408
  return baseName;
17310
17409
  }
17311
17410
  function deriveNextjsRouteName(filePath) {
17312
- const base = path8.basename(filePath).replace(/\.(ts|tsx|js|jsx)$/, "");
17411
+ const base = path9.basename(filePath).replace(/\.(ts|tsx|js|jsx)$/, "");
17313
17412
  if (base !== "route" && base !== "page")
17314
17413
  return null;
17315
- const parts = filePath.split(path8.sep);
17414
+ const parts = filePath.split(path9.sep);
17316
17415
  const appIdx = parts.lastIndexOf("app");
17317
17416
  if (appIdx === -1)
17318
17417
  return null;
@@ -17330,9 +17429,9 @@ function stripExtension(filename) {
17330
17429
  return filename;
17331
17430
  }
17332
17431
  function fileBaseName(filePath) {
17333
- const name = stripExtension(path8.basename(filePath));
17432
+ const name = stripExtension(path9.basename(filePath));
17334
17433
  if (INDEX_NAMES.has(name)) {
17335
- return path8.basename(path8.dirname(filePath));
17434
+ return path9.basename(path9.dirname(filePath));
17336
17435
  }
17337
17436
  return name;
17338
17437
  }
@@ -18029,7 +18128,11 @@ var init_entities = __esm({
18029
18128
  init_patterns();
18030
18129
  init_parser();
18031
18130
  TypeScriptEntityDetector = class {
18032
- parser = getParser("typescript");
18131
+ // Lazy: do not call getParser() at construction time — callers may
18132
+ // instantiate this before initParsers() has completed.
18133
+ get parser() {
18134
+ return getParser("typescript");
18135
+ }
18033
18136
  shouldScanFile(filePath) {
18034
18137
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath)) {
18035
18138
  return false;
@@ -18046,6 +18149,8 @@ var init_entities = __esm({
18046
18149
  detectEntities(sourceCode, filePath, service) {
18047
18150
  const tree = this.parser.parse(sourceCode);
18048
18151
  const entities = [];
18152
+ if (!tree)
18153
+ return entities;
18049
18154
  const classNodes = this.findClassDeclarations(tree.rootNode);
18050
18155
  for (const classNode of classNodes) {
18051
18156
  const entity = this.detectEntityFromClass(classNode, sourceCode, filePath, service);
@@ -18333,7 +18438,7 @@ var init_entities = __esm({
18333
18438
  import { spawn as spawn2 } from "child_process";
18334
18439
  import { readFileSync as readFileSync7 } from "fs";
18335
18440
  import { resolve as resolve6 } from "path";
18336
- import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
18441
+ import { pathToFileURL, fileURLToPath as fileURLToPath3 } from "url";
18337
18442
  function encodeMessage(msg) {
18338
18443
  const body = JSON.stringify(msg);
18339
18444
  const header = `Content-Length: ${Buffer.byteLength(body)}\r
@@ -18478,7 +18583,7 @@ var init_lsp_client = __esm({
18478
18583
  if (!uri)
18479
18584
  return null;
18480
18585
  try {
18481
- return fileURLToPath2(uri);
18586
+ return fileURLToPath3(uri);
18482
18587
  } catch {
18483
18588
  return null;
18484
18589
  }
@@ -18646,7 +18751,7 @@ var init_lsp_client = __esm({
18646
18751
  // packages/analyzer/dist/lsp-servers/pyright.js
18647
18752
  import { resolve as resolve7, dirname as dirname6 } from "path";
18648
18753
  import { existsSync as existsSync9 } from "fs";
18649
- import { fileURLToPath as fileURLToPath3 } from "url";
18754
+ import { fileURLToPath as fileURLToPath4 } from "url";
18650
18755
  function findPyrightBinary() {
18651
18756
  let dir = resolve7(__dirname2, "..");
18652
18757
  for (let i = 0; i < 10; i++) {
@@ -18678,7 +18783,7 @@ var __filename, __dirname2;
18678
18783
  var init_pyright = __esm({
18679
18784
  "packages/analyzer/dist/lsp-servers/pyright.js"() {
18680
18785
  "use strict";
18681
- __filename = fileURLToPath3(import.meta.url);
18786
+ __filename = fileURLToPath4(import.meta.url);
18682
18787
  __dirname2 = dirname6(__filename);
18683
18788
  }
18684
18789
  });
@@ -19305,9 +19410,9 @@ function findSimpleCycles(component, componentSet, adjacency) {
19305
19410
  for (const next of successors) {
19306
19411
  if (!componentSet.has(next))
19307
19412
  continue;
19308
- if (next === start && path21.length >= 2) {
19309
- const chain = [...path21, start];
19310
- const minNode = path21.reduce((a, b) => a < b ? a : b);
19413
+ if (next === start && path22.length >= 2) {
19414
+ const chain = [...path22, start];
19415
+ const minNode = path22.reduce((a, b) => a < b ? a : b);
19311
19416
  if (minNode === start) {
19312
19417
  cycles.push(chain);
19313
19418
  }
@@ -19315,15 +19420,15 @@ function findSimpleCycles(component, componentSet, adjacency) {
19315
19420
  }
19316
19421
  if (!visited.has(next) && !seen.has(next)) {
19317
19422
  visited.add(next);
19318
- path21.push(next);
19423
+ path22.push(next);
19319
19424
  dfs2(next);
19320
- path21.pop();
19425
+ path22.pop();
19321
19426
  visited.delete(next);
19322
19427
  }
19323
19428
  }
19324
19429
  };
19325
19430
  var dfs = dfs2;
19326
- const path21 = [start];
19431
+ const path22 = [start];
19327
19432
  const visited = /* @__PURE__ */ new Set([start]);
19328
19433
  dfs2(start);
19329
19434
  seen.add(start);
@@ -19345,7 +19450,7 @@ function findShortestCycle(start, componentSet, adjacency) {
19345
19450
  visited.add(next);
19346
19451
  }
19347
19452
  while (queue.length > 0) {
19348
- const { node, path: path21 } = queue.shift();
19453
+ const { node, path: path22 } = queue.shift();
19349
19454
  const nexts = adjacency.get(node);
19350
19455
  if (!nexts)
19351
19456
  continue;
@@ -19353,11 +19458,11 @@ function findShortestCycle(start, componentSet, adjacency) {
19353
19458
  if (!componentSet.has(next))
19354
19459
  continue;
19355
19460
  if (next === start) {
19356
- return [...path21, start];
19461
+ return [...path22, start];
19357
19462
  }
19358
19463
  if (!visited.has(next)) {
19359
19464
  visited.add(next);
19360
- queue.push({ node: next, path: [...path21, next] });
19465
+ queue.push({ node: next, path: [...path22, next] });
19361
19466
  }
19362
19467
  }
19363
19468
  }
@@ -38153,7 +38258,8 @@ var init_python_helpers = __esm({
38153
38258
  // packages/analyzer/dist/rules/_shared/python-framework-detection.js
38154
38259
  function getPythonImportSources(node) {
38155
38260
  const program3 = getPythonModuleNode(node);
38156
- const cached = importSourceCache.get(program3);
38261
+ const tree = program3.tree;
38262
+ const cached = importSourceCache.get(tree);
38157
38263
  if (cached)
38158
38264
  return cached;
38159
38265
  const sources = /* @__PURE__ */ new Set();
@@ -38192,7 +38298,7 @@ function getPythonImportSources(node) {
38192
38298
  walk(child);
38193
38299
  }
38194
38300
  walk(program3);
38195
- importSourceCache.set(program3, sources);
38301
+ importSourceCache.set(tree, sources);
38196
38302
  return sources;
38197
38303
  }
38198
38304
  function detectPythonOrm(node) {
@@ -44095,10 +44201,10 @@ var init_secret_rules = __esm({
44095
44201
  });
44096
44202
 
44097
44203
  // packages/analyzer/dist/rules/security/secret-scanner.js
44098
- import path9 from "node:path";
44204
+ import path10 from "node:path";
44099
44205
  function isSensitiveFile(filePath) {
44100
- const basename2 = path9.basename(filePath);
44101
- const ext2 = path9.extname(filePath);
44206
+ const basename2 = path10.basename(filePath);
44207
+ const ext2 = path10.extname(filePath);
44102
44208
  if (basename2.startsWith(".env")) {
44103
44209
  const envVariant = basename2;
44104
44210
  if (SENSITIVE_FILE_EXTENSIONS.has(envVariant)) {
@@ -46266,7 +46372,8 @@ var init_fallthrough_case = __esm({
46266
46372
  const cases = body.namedChildren.filter((c2) => c2.type === "switch_case");
46267
46373
  for (let i = 0; i < cases.length - 1; i++) {
46268
46374
  const caseNode = cases[i];
46269
- const statements = caseNode.namedChildren.filter((c2) => c2.type !== "comment" && c2 !== caseNode.childForFieldName("value"));
46375
+ const valueNode = caseNode.childForFieldName("value");
46376
+ const statements = caseNode.namedChildren.filter((c2) => c2.type !== "comment" && (!valueNode || c2.id !== valueNode.id));
46270
46377
  if (statements.length === 0)
46271
46378
  continue;
46272
46379
  const last2 = statements[statements.length - 1];
@@ -67769,7 +67876,7 @@ var init_case_without_break = __esm({
67769
67876
  nodeTypes: ["switch_case"],
67770
67877
  visit(node, filePath, sourceCode) {
67771
67878
  const valueNode = node.childForFieldName("value");
67772
- const stmts = node.namedChildren.filter((c2) => c2 !== valueNode);
67879
+ const stmts = node.namedChildren.filter((c2) => !valueNode || c2.id !== valueNode.id);
67773
67880
  if (stmts.length === 0)
67774
67881
  return null;
67775
67882
  let last2 = stmts[stmts.length - 1];
@@ -90697,8 +90804,8 @@ var init_missing_error_status_code = __esm({
90697
90804
  });
90698
90805
 
90699
90806
  // packages/analyzer/dist/rules/architecture/visitors/javascript/route-without-auth-middleware.js
90700
- function isPublicPath(path21) {
90701
- return PUBLIC_PATH_PATTERNS.some((re2) => re2.test(path21));
90807
+ function isPublicPath(path22) {
90808
+ return PUBLIC_PATH_PATTERNS.some((re2) => re2.test(path22));
90702
90809
  }
90703
90810
  function extractMiddlewareNames(callNode) {
90704
90811
  const args = callNode.childForFieldName("arguments");
@@ -90824,8 +90931,8 @@ var init_route_without_auth_middleware = __esm({
90824
90931
  return null;
90825
90932
  const firstArg = args.namedChildren[0];
90826
90933
  if (firstArg?.type === "string") {
90827
- const path21 = firstArg.text.replace(/^['"`]|['"`]$/g, "");
90828
- if (isPublicPath(path21))
90934
+ const path22 = firstArg.text.replace(/^['"`]|['"`]$/g, "");
90935
+ if (isPublicPath(path22))
90829
90936
  return null;
90830
90937
  }
90831
90938
  const middleware = extractMiddlewareNames(node);
@@ -91222,7 +91329,7 @@ var init_declarations_in_global_scope2 = __esm({
91222
91329
 
91223
91330
  // packages/analyzer/dist/rules/architecture/visitors/python/unused-import.js
91224
91331
  function hasIdentifierOutside(root, name, excludeNode) {
91225
- if (root === excludeNode)
91332
+ if (root.id === excludeNode.id)
91226
91333
  return false;
91227
91334
  if (root.type === "identifier" && root.text === name)
91228
91335
  return true;
@@ -93598,6 +93705,7 @@ __export(dist_exports2, {
93598
93705
  hasLspServer: () => hasLspServer,
93599
93706
  hasSchemaAwareVisitors: () => hasSchemaAwareVisitors,
93600
93707
  hasTypeAwareVisitors: () => hasTypeAwareVisitors,
93708
+ initParsers: () => initParsers,
93601
93709
  isBootstrapEntry: () => isBootstrapEntry,
93602
93710
  makeViolation: () => makeViolation,
93603
93711
  matchesPattern: () => matchesPattern,
@@ -93617,6 +93725,7 @@ __export(dist_exports2, {
93617
93725
  walkAstWithVisitors: () => walkAstWithVisitors
93618
93726
  });
93619
93727
  async function analyzeRepository(rootPath) {
93728
+ await initParsers();
93620
93729
  const files = discoverFiles(rootPath);
93621
93730
  if (files.length === 0) {
93622
93731
  return {
@@ -93660,6 +93769,7 @@ var init_dist6 = __esm({
93660
93769
  init_file_analyzer();
93661
93770
  init_dependency_graph();
93662
93771
  init_split_analyzer();
93772
+ init_parser();
93663
93773
  init_file_analyzer();
93664
93774
  init_dependency_graph();
93665
93775
  init_service_detector();
@@ -93699,7 +93809,7 @@ var init_dist6 = __esm({
93699
93809
  });
93700
93810
 
93701
93811
  // apps/server/dist/services/analyzer.service.js
93702
- import path10 from "node:path";
93812
+ import path11 from "node:path";
93703
93813
  function runDeterministicModuleChecks(result, enabledDeterministic) {
93704
93814
  if (!result.modules || !result.methods)
93705
93815
  return [];
@@ -93742,7 +93852,7 @@ async function runAnalysis(repoPath, _branch, onProgress, options) {
93742
93852
  const statusResult = await git.status();
93743
93853
  hasChanges = !statusResult.isClean();
93744
93854
  const gitRoot = (await git.revparse(["--show-toplevel"])).trim();
93745
- isSubdirectory = path10.resolve(repoPath) !== path10.resolve(gitRoot);
93855
+ isSubdirectory = path11.resolve(repoPath) !== path11.resolve(gitRoot);
93746
93856
  }
93747
93857
  if (hasChanges && !options?.skipStash && !isSubdirectory && git) {
93748
93858
  onProgress({ step: "stash", percent: 2, detail: "Stashing pending changes to analyze committed state..." });
@@ -93756,6 +93866,7 @@ async function runAnalysis(repoPath, _branch, onProgress, options) {
93756
93866
  try {
93757
93867
  onProgress({ step: "discover", percent: 10, detail: "Discovering files..." });
93758
93868
  const analyzer = await Promise.resolve().then(() => (init_dist6(), dist_exports2));
93869
+ await analyzer.initParsers();
93759
93870
  const files = await analyzer.discoverFiles(repoPath);
93760
93871
  onProgress({
93761
93872
  step: "discover",
@@ -103367,11 +103478,11 @@ var require_mime_types = __commonJS({
103367
103478
  }
103368
103479
  return exts[0];
103369
103480
  }
103370
- function lookup(path21) {
103371
- if (!path21 || typeof path21 !== "string") {
103481
+ function lookup(path22) {
103482
+ if (!path22 || typeof path22 !== "string") {
103372
103483
  return false;
103373
103484
  }
103374
- var extension2 = extname("x." + path21).toLowerCase().substr(1);
103485
+ var extension2 = extname("x." + path22).toLowerCase().substr(1);
103375
103486
  if (!extension2) {
103376
103487
  return false;
103377
103488
  }
@@ -109586,11 +109697,11 @@ var require_server = __commonJS({
109586
109697
  * @protected
109587
109698
  */
109588
109699
  _computePath(options) {
109589
- let path21 = (options.path || "/engine.io").replace(/\/$/, "");
109700
+ let path22 = (options.path || "/engine.io").replace(/\/$/, "");
109590
109701
  if (options.addTrailingSlash !== false) {
109591
- path21 += "/";
109702
+ path22 += "/";
109592
109703
  }
109593
- return path21;
109704
+ return path22;
109594
109705
  }
109595
109706
  /**
109596
109707
  * Returns a list of available transports for upgrade given a certain transport.
@@ -110089,10 +110200,10 @@ var require_server = __commonJS({
110089
110200
  * @param {Object} options
110090
110201
  */
110091
110202
  attach(server, options = {}) {
110092
- const path21 = this._computePath(options);
110203
+ const path22 = this._computePath(options);
110093
110204
  const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1e3;
110094
110205
  function check(req) {
110095
- return path21 === req.url.slice(0, path21.length);
110206
+ return path22 === req.url.slice(0, path22.length);
110096
110207
  }
110097
110208
  const listeners = server.listeners("request").slice(0);
110098
110209
  server.removeAllListeners("request");
@@ -110100,7 +110211,7 @@ var require_server = __commonJS({
110100
110211
  server.on("listening", this.init.bind(this));
110101
110212
  server.on("request", (req, res) => {
110102
110213
  if (check(req)) {
110103
- debug2('intercepting request for path "%s"', path21);
110214
+ debug2('intercepting request for path "%s"', path22);
110104
110215
  this.handleRequest(req, res);
110105
110216
  } else {
110106
110217
  let i = 0;
@@ -110939,8 +111050,8 @@ var require_userver = __commonJS({
110939
111050
  * @param options
110940
111051
  */
110941
111052
  attach(app, options = {}) {
110942
- const path21 = this._computePath(options);
110943
- app.any(path21, this.handleRequest.bind(this)).ws(path21, {
111053
+ const path22 = this._computePath(options);
111054
+ app.any(path22, this.handleRequest.bind(this)).ws(path22, {
110944
111055
  compression: options.compression,
110945
111056
  idleTimeout: options.idleTimeout,
110946
111057
  maxBackpressure: options.maxBackpressure,
@@ -115365,7 +115476,7 @@ var require_dist4 = __commonJS({
115365
115476
  var zlib_1 = __require("zlib");
115366
115477
  var accepts = require_accepts();
115367
115478
  var stream_1 = __require("stream");
115368
- var path21 = __require("path");
115479
+ var path22 = __require("path");
115369
115480
  var engine_io_1 = require_engine_io();
115370
115481
  var client_1 = require_client();
115371
115482
  var events_1 = __require("events");
@@ -115560,7 +115671,7 @@ var require_dist4 = __commonJS({
115560
115671
  res.writeHeader("cache-control", "public, max-age=0");
115561
115672
  res.writeHeader("content-type", "application/" + (isMap ? "json" : "javascript") + "; charset=utf-8");
115562
115673
  res.writeHeader("etag", expectedEtag);
115563
- const filepath = path21.join(__dirname, "../client-dist/", filename);
115674
+ const filepath = path22.join(__dirname, "../client-dist/", filename);
115564
115675
  (0, uws_1.serveFile)(res, filepath);
115565
115676
  });
115566
115677
  }
@@ -115642,7 +115753,7 @@ var require_dist4 = __commonJS({
115642
115753
  * @private
115643
115754
  */
115644
115755
  static sendFile(filename, req, res) {
115645
- const readStream = (0, fs_1.createReadStream)(path21.join(__dirname, "../client-dist/", filename));
115756
+ const readStream = (0, fs_1.createReadStream)(path22.join(__dirname, "../client-dist/", filename));
115646
115757
  const encoding = accepts(req).encodings(["br", "gzip", "deflate"]);
115647
115758
  const onError2 = (err) => {
115648
115759
  if (err) {
@@ -116506,8 +116617,8 @@ var init_parseUtil = __esm({
116506
116617
  init_errors();
116507
116618
  init_en();
116508
116619
  makeIssue = (params) => {
116509
- const { data, path: path21, errorMaps, issueData } = params;
116510
- const fullPath = [...path21, ...issueData.path || []];
116620
+ const { data, path: path22, errorMaps, issueData } = params;
116621
+ const fullPath = [...path22, ...issueData.path || []];
116511
116622
  const fullIssue = {
116512
116623
  ...issueData,
116513
116624
  path: fullPath
@@ -116815,11 +116926,11 @@ var init_types2 = __esm({
116815
116926
  init_parseUtil();
116816
116927
  init_util3();
116817
116928
  ParseInputLazyPath = class {
116818
- constructor(parent, value, path21, key) {
116929
+ constructor(parent, value, path22, key) {
116819
116930
  this._cachedPath = [];
116820
116931
  this.parent = parent;
116821
116932
  this.data = value;
116822
- this._path = path21;
116933
+ this._path = path22;
116823
116934
  this._key = key;
116824
116935
  }
116825
116936
  get path() {
@@ -122572,8 +122683,8 @@ var require_package2 = __commonJS({
122572
122683
  // node_modules/.pnpm/dotenv@16.6.1/node_modules/dotenv/lib/main.js
122573
122684
  var require_main = __commonJS({
122574
122685
  "node_modules/.pnpm/dotenv@16.6.1/node_modules/dotenv/lib/main.js"(exports, module) {
122575
- var fs16 = __require("fs");
122576
- var path21 = __require("path");
122686
+ var fs17 = __require("fs");
122687
+ var path22 = __require("path");
122577
122688
  var os8 = __require("os");
122578
122689
  var crypto2 = __require("crypto");
122579
122690
  var packageJson = require_package2();
@@ -122681,7 +122792,7 @@ var require_main = __commonJS({
122681
122792
  if (options && options.path && options.path.length > 0) {
122682
122793
  if (Array.isArray(options.path)) {
122683
122794
  for (const filepath of options.path) {
122684
- if (fs16.existsSync(filepath)) {
122795
+ if (fs17.existsSync(filepath)) {
122685
122796
  possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
122686
122797
  }
122687
122798
  }
@@ -122689,15 +122800,15 @@ var require_main = __commonJS({
122689
122800
  possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
122690
122801
  }
122691
122802
  } else {
122692
- possibleVaultPath = path21.resolve(process.cwd(), ".env.vault");
122803
+ possibleVaultPath = path22.resolve(process.cwd(), ".env.vault");
122693
122804
  }
122694
- if (fs16.existsSync(possibleVaultPath)) {
122805
+ if (fs17.existsSync(possibleVaultPath)) {
122695
122806
  return possibleVaultPath;
122696
122807
  }
122697
122808
  return null;
122698
122809
  }
122699
122810
  function _resolveHome(envPath) {
122700
- return envPath[0] === "~" ? path21.join(os8.homedir(), envPath.slice(1)) : envPath;
122811
+ return envPath[0] === "~" ? path22.join(os8.homedir(), envPath.slice(1)) : envPath;
122701
122812
  }
122702
122813
  function _configVault(options) {
122703
122814
  const debug2 = Boolean(options && options.debug);
@@ -122714,7 +122825,7 @@ var require_main = __commonJS({
122714
122825
  return { parsed };
122715
122826
  }
122716
122827
  function configDotenv(options) {
122717
- const dotenvPath = path21.resolve(process.cwd(), ".env");
122828
+ const dotenvPath = path22.resolve(process.cwd(), ".env");
122718
122829
  let encoding = "utf8";
122719
122830
  const debug2 = Boolean(options && options.debug);
122720
122831
  const quiet = options && "quiet" in options ? options.quiet : true;
@@ -122738,13 +122849,13 @@ var require_main = __commonJS({
122738
122849
  }
122739
122850
  let lastError;
122740
122851
  const parsedAll = {};
122741
- for (const path22 of optionPaths) {
122852
+ for (const path23 of optionPaths) {
122742
122853
  try {
122743
- const parsed = DotenvModule.parse(fs16.readFileSync(path22, { encoding }));
122854
+ const parsed = DotenvModule.parse(fs17.readFileSync(path23, { encoding }));
122744
122855
  DotenvModule.populate(parsedAll, parsed, options);
122745
122856
  } catch (e) {
122746
122857
  if (debug2) {
122747
- _debug(`Failed to load ${path22} ${e.message}`);
122858
+ _debug(`Failed to load ${path23} ${e.message}`);
122748
122859
  }
122749
122860
  lastError = e;
122750
122861
  }
@@ -122759,7 +122870,7 @@ var require_main = __commonJS({
122759
122870
  const shortPaths = [];
122760
122871
  for (const filePath of optionPaths) {
122761
122872
  try {
122762
- const relative2 = path21.relative(process.cwd(), filePath);
122873
+ const relative2 = path22.relative(process.cwd(), filePath);
122763
122874
  shortPaths.push(relative2);
122764
122875
  } catch (e) {
122765
122876
  if (debug2) {
@@ -122860,14 +122971,14 @@ var require_main = __commonJS({
122860
122971
  });
122861
122972
 
122862
122973
  // apps/server/dist/config/env.js
122863
- import path11 from "node:path";
122974
+ import path12 from "node:path";
122864
122975
  import os3 from "node:os";
122865
122976
  var import_dotenv;
122866
122977
  var init_env = __esm({
122867
122978
  "apps/server/dist/config/env.js"() {
122868
122979
  "use strict";
122869
122980
  import_dotenv = __toESM(require_main(), 1);
122870
- import_dotenv.default.config({ path: path11.join(os3.homedir(), ".truecourse", ".env") });
122981
+ import_dotenv.default.config({ path: path12.join(os3.homedir(), ".truecourse", ".env") });
122871
122982
  import_dotenv.default.config({ path: "../../.env" });
122872
122983
  }
122873
122984
  });
@@ -123582,8 +123693,8 @@ var init_cli_provider = __esm({
123582
123693
  this._repoId = repoId;
123583
123694
  }
123584
123695
  /** Set target repo path — used as cwd when spawning CLI so Read tool accesses the right files. */
123585
- setRepoPath(path21) {
123586
- this._repoPath = path21;
123696
+ setRepoPath(path22) {
123697
+ this._repoPath = path22;
123587
123698
  }
123588
123699
  flushUsage() {
123589
123700
  if (this._usageRecords.length === 0)
@@ -125005,8 +125116,8 @@ var init_violation_lifecycle_service = __esm({
125005
125116
 
125006
125117
  // apps/server/dist/services/violation-pipeline.service.js
125007
125118
  import { randomUUID as randomUUID5 } from "node:crypto";
125008
- import fs8 from "node:fs";
125009
- import path12 from "node:path";
125119
+ import fs9 from "node:fs";
125120
+ import path13 from "node:path";
125010
125121
  function throwIfAborted(signal) {
125011
125122
  if (signal?.aborted)
125012
125123
  throw new DOMException("Analysis cancelled", "AbortError");
@@ -125038,6 +125149,7 @@ function compareDeterministicViolations(current, previous) {
125038
125149
  return { newDetections, unchangedDetections, resolvedDetections };
125039
125150
  }
125040
125151
  async function runViolationPipeline(input) {
125152
+ await initParsers();
125041
125153
  const { repoPath, analysisId, now, result, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap, previousActiveViolations, changedFileSet, onProgress, tracker, provider: externalProvider, enabledCategories, enableLlmRules, signal } = input;
125042
125154
  const added = [];
125043
125155
  const unchanged = [];
@@ -125066,29 +125178,35 @@ async function runViolationPipeline(input) {
125066
125178
  const enabledLlmCodeRules = enableLlmRules !== false ? allRules.filter((r) => (r.domain ? codeDomains.has(r.domain) : r.category === "code") && r.type === "llm" && r.prompt) : [];
125067
125179
  const archLlmRules = allRules.filter((r) => r.type === "llm" && r.prompt && r.domain === "architecture").map((r) => ({ key: r.key, name: r.name, severity: r.severity, prompt: r.prompt, category: r.category }));
125068
125180
  const dbSchemaLlmRules = allRules.filter((r) => r.type === "llm" && r.prompt && r.domain === "database" && r.category === "database").map((r) => ({ key: r.key, name: r.name, severity: r.severity, prompt: r.prompt, category: r.category }));
125069
- const filesToScan = changedFileSet ? [...changedFileSet].map((relPath) => ({ filePath: relPath, resolve: true })) : (result.fileAnalyses || []).map((fa) => ({ filePath: fa.filePath, resolve: !path12.isAbsolute(fa.filePath) }));
125181
+ const filesToScan = changedFileSet ? [...changedFileSet].map((relPath) => ({ filePath: relPath, resolve: true })) : (result.fileAnalyses || []).map((fa) => ({ filePath: fa.filePath, resolve: !path13.isAbsolute(fa.filePath) }));
125070
125182
  const hasLlm = enabledLlm.length > 0;
125071
125183
  if (hasLlm)
125072
125184
  tracker?.start("scan", "Reading files...");
125073
125185
  const fileContents = /* @__PURE__ */ new Map();
125186
+ const totalToScan = filesToScan.length;
125187
+ let scanned = 0;
125074
125188
  for (const { filePath, resolve: resolve8 } of filesToScan) {
125075
125189
  try {
125076
125190
  const lang = detectLanguage(filePath);
125077
125191
  if (!lang)
125078
125192
  continue;
125079
- const absPath = resolve8 ? path12.resolve(repoPath, filePath) : path12.isAbsolute(filePath) ? filePath : path12.join(repoPath, filePath);
125080
- if (!fs8.existsSync(absPath))
125193
+ const absPath = resolve8 ? path13.resolve(repoPath, filePath) : path13.isAbsolute(filePath) ? filePath : path13.join(repoPath, filePath);
125194
+ if (!fs9.existsSync(absPath))
125081
125195
  continue;
125082
- const content = fs8.readFileSync(absPath, "utf-8");
125196
+ const content = fs9.readFileSync(absPath, "utf-8");
125083
125197
  const lineCount = content.split("\n").length;
125084
125198
  fileContents.set(changedFileSet ? absPath : filePath, { content, lineCount });
125085
125199
  } catch {
125086
125200
  }
125201
+ scanned++;
125202
+ if (hasLlm && (scanned % 20 === 0 || scanned === totalToScan)) {
125203
+ tracker?.detail("scan", `Reading ${scanned}/${totalToScan} files...`);
125204
+ }
125087
125205
  }
125088
125206
  let typeQuery;
125089
125207
  const enabledCodeKeys = new Set(enabledCodeRules.filter((r) => r.type === "deterministic" && r.enabled).map((r) => r.key));
125090
125208
  if (hasTypeAwareVisitors(enabledCodeKeys)) {
125091
- const tsFiles = filesToScan.filter(({ filePath: fp }) => /\.(ts|tsx|js|jsx)$/.test(fp)).map(({ filePath: fp, resolve: res }) => res ? path12.resolve(repoPath, fp) : path12.isAbsolute(fp) ? fp : path12.join(repoPath, fp));
125209
+ const tsFiles = filesToScan.filter(({ filePath: fp }) => /\.(ts|tsx|js|jsx)$/.test(fp)).map(({ filePath: fp, resolve: res }) => res ? path13.resolve(repoPath, fp) : path13.isAbsolute(fp) ? fp : path13.join(repoPath, fp));
125092
125210
  if (tsFiles.length > 0) {
125093
125211
  const scoped = buildScopedCompilerOptions(repoPath);
125094
125212
  typeQuery = createTypeQueryService(tsFiles, scoped);
@@ -125167,7 +125285,7 @@ async function runViolationPipeline(input) {
125167
125285
  const lang = detectLanguage(filePath);
125168
125286
  if (!lang)
125169
125287
  continue;
125170
- const absPath = resolve8 ? path12.resolve(repoPath, filePath) : path12.isAbsolute(filePath) ? filePath : path12.join(repoPath, filePath);
125288
+ const absPath = resolve8 ? path13.resolve(repoPath, filePath) : path13.isAbsolute(filePath) ? filePath : path13.join(repoPath, filePath);
125171
125289
  const key = changedFileSet ? absPath : filePath;
125172
125290
  const fc = fileContents.get(key);
125173
125291
  if (!fc)
@@ -125181,11 +125299,11 @@ async function runViolationPipeline(input) {
125181
125299
  }
125182
125300
  log.info(`[Pipeline] Code scan: ${allCodeViolations.length} violations from ${filesToScan.length} files (${enabledCodeRules.length} det rules, ${enabledLlmCodeRules.length} LLM rules)`);
125183
125301
  if (enabledCodeRules.some((r) => r.key === "bugs/deterministic/invalid-pyproject-toml")) {
125184
- const pyprojectPath = path12.join(repoPath, "pyproject.toml");
125185
- if (fs8.existsSync(pyprojectPath)) {
125302
+ const pyprojectPath = path13.join(repoPath, "pyproject.toml");
125303
+ if (fs9.existsSync(pyprojectPath)) {
125186
125304
  try {
125187
125305
  const { checkPyprojectToml: checkPyprojectToml2 } = await Promise.resolve().then(() => (init_dist6(), dist_exports2));
125188
- const content = fs8.readFileSync(pyprojectPath, "utf-8");
125306
+ const content = fs9.readFileSync(pyprojectPath, "utf-8");
125189
125307
  const tomlViolations = checkPyprojectToml2(pyprojectPath, content);
125190
125308
  allCodeViolations.push(...tomlViolations);
125191
125309
  } catch {
@@ -125967,7 +126085,7 @@ function processLlmCodeViolations(codeResult, validFilePaths, fileContents, allC
125967
126085
  for (const v of codeResult.violations) {
125968
126086
  let filePath = v.filePath;
125969
126087
  if (!validFilePaths.has(filePath)) {
125970
- const resolved = path12.resolve(repoPath, filePath);
126088
+ const resolved = path13.resolve(repoPath, filePath);
125971
126089
  if (validFilePaths.has(resolved)) {
125972
126090
  filePath = resolved;
125973
126091
  } else {
@@ -126223,7 +126341,7 @@ var init_analyze_core = __esm({
126223
126341
  });
126224
126342
 
126225
126343
  // apps/server/dist/commands/analyze-persist.js
126226
- import path13 from "node:path";
126344
+ import path14 from "node:path";
126227
126345
  function persistFullAnalysis(project, core2, startedAt) {
126228
126346
  const filename = buildAnalysisFilename(core2.analysisId, core2.now);
126229
126347
  const snapshot = {
@@ -126351,8 +126469,8 @@ function buildDiffSnapshot(repoPath, core2, baseline) {
126351
126469
  const newViolations = pipelineResult.added.map(denormalize);
126352
126470
  const latestById = new Map(baseline.violations.map((v) => [v.id, v]));
126353
126471
  const resolvedViolations = pipelineResult.resolvedRefs.map((r) => latestById.get(r.id)).filter((v) => !!v);
126354
- const changedAbs = new Set(changedFiles.map((c2) => path13.resolve(repoPath, c2.path)));
126355
- const matchesChanged = (p2) => !!p2 && (changedAbs.has(p2) || changedAbs.has(path13.resolve(repoPath, p2)));
126472
+ const changedAbs = new Set(changedFiles.map((c2) => path14.resolve(repoPath, c2.path)));
126473
+ const matchesChanged = (p2) => !!p2 && (changedAbs.has(p2) || changedAbs.has(path14.resolve(repoPath, p2)));
126356
126474
  const affectedModules = graph.modules.filter((m) => matchesChanged(m.filePath));
126357
126475
  const affectedModuleIdSet = new Set(affectedModules.map((m) => m.id));
126358
126476
  const serviceNameById = new Map(graph.services.map((s) => [s.id, s.name]));
@@ -126469,7 +126587,7 @@ init_dist4();
126469
126587
  init_paths();
126470
126588
  init_registry();
126471
126589
  init_helpers();
126472
- async function runAdd() {
126590
+ async function runAdd(options = {}) {
126473
126591
  const repoPath = resolveRepoDir(process.cwd()) ?? process.cwd();
126474
126592
  mt("Adding repository to TrueCourse");
126475
126593
  O2.step(repoPath);
@@ -126481,14 +126599,14 @@ async function runAdd() {
126481
126599
  } else {
126482
126600
  O2.success(`Repository "${entry.name}" added.`);
126483
126601
  }
126484
- await promptInstallSkills(repoPath);
126602
+ await promptInstallSkills(repoPath, { install: options.installSkills });
126485
126603
  gt("Run `truecourse analyze` to generate analysis data.");
126486
126604
  }
126487
126605
 
126488
126606
  // tools/cli/src/commands/analyze.ts
126489
126607
  init_dist4();
126490
126608
  import { execSync } from "node:child_process";
126491
- import path15 from "node:path";
126609
+ import path16 from "node:path";
126492
126610
 
126493
126611
  // apps/server/dist/commands/analyze-in-process.js
126494
126612
  init_analysis_store();
@@ -126510,12 +126628,20 @@ init_helpers();
126510
126628
 
126511
126629
  // tools/cli/src/commands/llm-prompt.ts
126512
126630
  init_dist4();
126513
- async function promptLlmEstimate(estimate) {
126631
+ init_helpers();
126632
+ async function promptLlmEstimate(estimate, { autoApprove } = {}) {
126514
126633
  const totalRules = estimate.uniqueRuleCount ?? estimate.tiers.reduce((s, t2) => s + t2.ruleCount, 0);
126515
126634
  const totalFiles = estimate.uniqueFileCount ?? estimate.tiers.reduce((s, t2) => s + t2.fileCount, 0);
126516
126635
  const tokens = estimate.totalEstimatedTokens;
126517
126636
  const tokenStr = tokens >= 1e6 ? `~${(tokens / 1e6).toFixed(1)}M tokens` : `~${Math.round(tokens / 1e3)}k tokens`;
126518
126637
  O2.step(`LLM will analyze ${totalFiles} files with ${totalRules} rules (${tokenStr})`);
126638
+ if (autoApprove) return true;
126639
+ if (!isInteractive()) {
126640
+ O2.error(
126641
+ "Cannot prompt for LLM-rule confirmation non-interactively. Pass --llm to approve the estimate or --no-llm to skip LLM rules."
126642
+ );
126643
+ return false;
126644
+ }
126519
126645
  const proceed = await ot2({ message: "Run LLM-powered rules?", initialValue: true });
126520
126646
  if (q(proceed)) return false;
126521
126647
  if (!proceed) O2.info("Skipping LLM rules.");
@@ -126524,8 +126650,8 @@ async function promptLlmEstimate(estimate) {
126524
126650
 
126525
126651
  // tools/cli/src/telemetry.ts
126526
126652
  init_dist4();
126527
- import fs9 from "node:fs";
126528
- import path14 from "node:path";
126653
+ import fs10 from "node:fs";
126654
+ import path15 from "node:path";
126529
126655
  import os4 from "node:os";
126530
126656
  import crypto from "node:crypto";
126531
126657
  var DEFAULT_CONFIG2 = {
@@ -126534,12 +126660,12 @@ var DEFAULT_CONFIG2 = {
126534
126660
  noticeShown: false
126535
126661
  };
126536
126662
  function getTelemetryConfigPath() {
126537
- return path14.join(os4.homedir(), ".truecourse", "telemetry.json");
126663
+ return path15.join(os4.homedir(), ".truecourse", "telemetry.json");
126538
126664
  }
126539
126665
  function readTelemetryConfig() {
126540
126666
  const configPath = getTelemetryConfigPath();
126541
126667
  try {
126542
- const raw = fs9.readFileSync(configPath, "utf-8");
126668
+ const raw = fs10.readFileSync(configPath, "utf-8");
126543
126669
  const parsed = JSON.parse(raw);
126544
126670
  const config2 = { ...DEFAULT_CONFIG2, ...parsed };
126545
126671
  if (!config2.anonymousId) {
@@ -126559,17 +126685,17 @@ function readTelemetryConfig() {
126559
126685
  }
126560
126686
  function writeTelemetryConfig(partial) {
126561
126687
  const configPath = getTelemetryConfigPath();
126562
- const dir = path14.dirname(configPath);
126563
- fs9.mkdirSync(dir, { recursive: true });
126688
+ const dir = path15.dirname(configPath);
126689
+ fs10.mkdirSync(dir, { recursive: true });
126564
126690
  let current;
126565
126691
  try {
126566
- const raw = fs9.readFileSync(configPath, "utf-8");
126692
+ const raw = fs10.readFileSync(configPath, "utf-8");
126567
126693
  current = { ...DEFAULT_CONFIG2, ...JSON.parse(raw) };
126568
126694
  } catch {
126569
126695
  current = { ...DEFAULT_CONFIG2 };
126570
126696
  }
126571
126697
  const merged = { ...current, ...partial };
126572
- fs9.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
126698
+ fs10.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
126573
126699
  }
126574
126700
  function showFirstRunNotice() {
126575
126701
  try {
@@ -126663,18 +126789,31 @@ function stopSpinner() {
126663
126789
  spinnerInterval = null;
126664
126790
  }
126665
126791
  }
126666
- async function runAnalyze(_options = {}) {
126792
+ function resolveLlmDecision(options, configDefault) {
126793
+ if (options.llm === true) return { enabled: true, autoApproveEstimate: true };
126794
+ if (options.llm === false) return { enabled: false, autoApproveEstimate: false };
126795
+ if (!isInteractive()) {
126796
+ exitMissingNonInteractiveFlag(
126797
+ "analyze needs a decision on LLM rules before running non-interactively.",
126798
+ "Pass --llm to run with LLM rules (cost) or --no-llm to skip them."
126799
+ );
126800
+ }
126801
+ return { enabled: configDefault, autoApproveEstimate: false };
126802
+ }
126803
+ async function runAnalyze(options = {}) {
126667
126804
  mt("Analyzing repository");
126668
126805
  ensureClaudeCli();
126669
126806
  showFirstRunNotice();
126670
126807
  const project = resolveOrInitProject();
126671
126808
  O2.step(`Repository: ${project.name}`);
126809
+ await promptInstallSkills(project.path, { install: options.installSkills });
126672
126810
  configureLogger({
126673
- filePath: path15.join(project.path, ".truecourse/logs/analyze.log")
126811
+ filePath: path16.join(project.path, ".truecourse/logs/analyze.log")
126674
126812
  });
126675
126813
  const config2 = readProjectConfig(project.path);
126676
126814
  const enabledCategories = config2.enabledCategories ?? void 0;
126677
- const enableLlmRules = config2.enableLlmRules ?? true;
126815
+ const llmDecision = resolveLlmDecision(options, config2.enableLlmRules ?? true);
126816
+ const enableLlmRules = llmDecision.enabled;
126678
126817
  renderPhase = enableLlmRules ? "pre-llm" : "all";
126679
126818
  if (wipeLegacyPostgresData()) {
126680
126819
  O2.info("Legacy Postgres data wiped. Re-analyze to repopulate.");
@@ -126694,7 +126833,9 @@ async function runAnalyze(_options = {}) {
126694
126833
  enableLlmRulesOverride: enableLlmRules,
126695
126834
  onLlmEstimate: async (estimate) => {
126696
126835
  stopSpinner();
126697
- const proceed = await promptLlmEstimate(estimate);
126836
+ const proceed = await promptLlmEstimate(estimate, {
126837
+ autoApprove: llmDecision.autoApproveEstimate
126838
+ });
126698
126839
  renderPhase = "post-llm";
126699
126840
  return proceed;
126700
126841
  }
@@ -126716,7 +126857,7 @@ async function runAnalyze(_options = {}) {
126716
126857
  await closeLogger();
126717
126858
  }
126718
126859
  }
126719
- async function runAnalyzeDiff(_options = {}) {
126860
+ async function runAnalyzeDiff(options = {}) {
126720
126861
  const { diffInProcess: diffInProcess2 } = await Promise.resolve().then(() => (init_diff_in_process(), diff_in_process_exports));
126721
126862
  const { renderDiffResultsSummary: renderDiffResultsSummary2 } = await Promise.resolve().then(() => (init_helpers(), helpers_exports));
126722
126863
  mt("Running diff check");
@@ -126724,12 +126865,14 @@ async function runAnalyzeDiff(_options = {}) {
126724
126865
  showFirstRunNotice();
126725
126866
  const project = resolveOrInitProject();
126726
126867
  O2.step(`Repository: ${project.name}`);
126868
+ await promptInstallSkills(project.path, { install: options.installSkills });
126727
126869
  configureLogger({
126728
- filePath: path15.join(project.path, ".truecourse/logs/analyze.log")
126870
+ filePath: path16.join(project.path, ".truecourse/logs/analyze.log")
126729
126871
  });
126730
126872
  const config2 = readProjectConfig(project.path);
126731
126873
  const enabledCategories = config2.enabledCategories ?? void 0;
126732
- const enableLlmRules = config2.enableLlmRules ?? true;
126874
+ const llmDecision = resolveLlmDecision(options, config2.enableLlmRules ?? true);
126875
+ const enableLlmRules = llmDecision.enabled;
126733
126876
  renderPhase = enableLlmRules ? "pre-llm" : "all";
126734
126877
  const stepDefs = buildAnalysisSteps(enabledCategories, enableLlmRules);
126735
126878
  const tracker = new StepTracker((payload) => {
@@ -126746,7 +126889,9 @@ async function runAnalyzeDiff(_options = {}) {
126746
126889
  enableLlmRulesOverride: enableLlmRules,
126747
126890
  onLlmEstimate: async (estimate) => {
126748
126891
  stopSpinner();
126749
- const proceed = await promptLlmEstimate(estimate);
126892
+ const proceed = await promptLlmEstimate(estimate, {
126893
+ autoApprove: llmDecision.autoApproveEstimate
126894
+ });
126750
126895
  renderPhase = "post-llm";
126751
126896
  return proceed;
126752
126897
  }
@@ -126781,24 +126926,24 @@ init_paths();
126781
126926
  init_registry();
126782
126927
  init_helpers();
126783
126928
  import { spawn as spawn5 } from "node:child_process";
126784
- import fs14 from "node:fs";
126785
- import path19 from "node:path";
126786
- import { fileURLToPath as fileURLToPath4 } from "node:url";
126929
+ import fs15 from "node:fs";
126930
+ import path20 from "node:path";
126931
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
126787
126932
 
126788
126933
  // tools/cli/src/commands/service/macos.ts
126789
- import fs11 from "node:fs";
126790
- import path16 from "node:path";
126934
+ import fs12 from "node:fs";
126935
+ import path17 from "node:path";
126791
126936
  import os5 from "node:os";
126792
126937
  import { execSync as execSync2 } from "node:child_process";
126793
126938
 
126794
126939
  // tools/cli/src/commands/service/env.ts
126795
- import fs10 from "node:fs";
126940
+ import fs11 from "node:fs";
126796
126941
  function parseEnvFile(filePath) {
126797
126942
  const vars = {};
126798
- if (!fs10.existsSync(filePath)) {
126943
+ if (!fs11.existsSync(filePath)) {
126799
126944
  return vars;
126800
126945
  }
126801
- const content = fs10.readFileSync(filePath, "utf-8");
126946
+ const content = fs11.readFileSync(filePath, "utf-8");
126802
126947
  for (const line of content.split("\n")) {
126803
126948
  const trimmed2 = line.trim();
126804
126949
  if (!trimmed2 || trimmed2.startsWith("#")) continue;
@@ -126818,14 +126963,14 @@ function parseEnvFile(filePath) {
126818
126963
 
126819
126964
  // tools/cli/src/commands/service/macos.ts
126820
126965
  var SERVICE_LABEL = "com.truecourse.server";
126821
- var PLIST_DIR = path16.join(os5.homedir(), "Library", "LaunchAgents");
126822
- var PLIST_PATH = path16.join(PLIST_DIR, `${SERVICE_LABEL}.plist`);
126966
+ var PLIST_DIR = path17.join(os5.homedir(), "Library", "LaunchAgents");
126967
+ var PLIST_PATH = path17.join(PLIST_DIR, `${SERVICE_LABEL}.plist`);
126823
126968
  function escapeXml(s) {
126824
126969
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
126825
126970
  }
126826
126971
  function buildPlist(serverPath, logPath, envVars) {
126827
- const stdoutPath = path16.join(path16.dirname(logPath), "truecourse.log");
126828
- const stderrPath = path16.join(path16.dirname(logPath), "truecourse.error.log");
126972
+ const stdoutPath = path17.join(path17.dirname(logPath), "truecourse.log");
126973
+ const stderrPath = path17.join(path17.dirname(logPath), "truecourse.error.log");
126829
126974
  let envSection = "";
126830
126975
  if (Object.keys(envVars).length > 0) {
126831
126976
  const entries = Object.entries(envVars).map(([k, v]) => ` <key>${escapeXml(k)}</key>
@@ -126861,15 +127006,15 @@ ${entries}
126861
127006
  }
126862
127007
  var MacOSService = class {
126863
127008
  async install(serverPath, logPath) {
126864
- const envFile = path16.join(os5.homedir(), ".truecourse", ".env");
127009
+ const envFile = path17.join(os5.homedir(), ".truecourse", ".env");
126865
127010
  const envVars = parseEnvFile(envFile);
126866
127011
  if (process.env.PATH && !envVars.PATH) {
126867
127012
  envVars.PATH = process.env.PATH;
126868
127013
  }
126869
- fs11.mkdirSync(PLIST_DIR, { recursive: true });
126870
- fs11.mkdirSync(path16.dirname(logPath), { recursive: true });
127014
+ fs12.mkdirSync(PLIST_DIR, { recursive: true });
127015
+ fs12.mkdirSync(path17.dirname(logPath), { recursive: true });
126871
127016
  const plist = buildPlist(serverPath, logPath, envVars);
126872
- fs11.writeFileSync(PLIST_PATH, plist, "utf-8");
127017
+ fs12.writeFileSync(PLIST_PATH, plist, "utf-8");
126873
127018
  execSync2(`launchctl load -w "${PLIST_PATH}"`, { stdio: "pipe" });
126874
127019
  }
126875
127020
  async uninstall() {
@@ -126877,12 +127022,12 @@ var MacOSService = class {
126877
127022
  execSync2(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
126878
127023
  } catch {
126879
127024
  }
126880
- if (fs11.existsSync(PLIST_PATH)) {
126881
- fs11.unlinkSync(PLIST_PATH);
127025
+ if (fs12.existsSync(PLIST_PATH)) {
127026
+ fs12.unlinkSync(PLIST_PATH);
126882
127027
  }
126883
127028
  }
126884
127029
  async start() {
126885
- if (!fs11.existsSync(PLIST_PATH)) {
127030
+ if (!fs12.existsSync(PLIST_PATH)) {
126886
127031
  throw new Error("Service is not installed. Run 'truecourse service install' first.");
126887
127032
  }
126888
127033
  execSync2(`launchctl load -w "${PLIST_PATH}"`, { stdio: "pipe" });
@@ -126912,21 +127057,21 @@ var MacOSService = class {
126912
127057
  }
126913
127058
  }
126914
127059
  async isInstalled() {
126915
- return fs11.existsSync(PLIST_PATH);
127060
+ return fs12.existsSync(PLIST_PATH);
126916
127061
  }
126917
127062
  };
126918
127063
 
126919
127064
  // tools/cli/src/commands/service/linux.ts
126920
- import fs12 from "node:fs";
126921
- import path17 from "node:path";
127065
+ import fs13 from "node:fs";
127066
+ import path18 from "node:path";
126922
127067
  import os6 from "node:os";
126923
127068
  import { execSync as execSync3 } from "node:child_process";
126924
127069
  var SERVICE_NAME = "truecourse";
126925
- var UNIT_DIR = path17.join(os6.homedir(), ".config", "systemd", "user");
126926
- var UNIT_PATH = path17.join(UNIT_DIR, `${SERVICE_NAME}.service`);
127070
+ var UNIT_DIR = path18.join(os6.homedir(), ".config", "systemd", "user");
127071
+ var UNIT_PATH = path18.join(UNIT_DIR, `${SERVICE_NAME}.service`);
126927
127072
  function buildUnitFile(serverPath, logPath) {
126928
- const envFile = path17.join(os6.homedir(), ".truecourse", ".env");
126929
- const logDir = path17.dirname(logPath);
127073
+ const envFile = path18.join(os6.homedir(), ".truecourse", ".env");
127074
+ const logDir = path18.dirname(logPath);
126930
127075
  return `[Unit]
126931
127076
  Description=TrueCourse Server
126932
127077
  After=network.target
@@ -126937,8 +127082,8 @@ ExecStart=${process.execPath} ${serverPath}
126937
127082
  Restart=on-failure
126938
127083
  RestartSec=5
126939
127084
  EnvironmentFile=${envFile}
126940
- StandardOutput=append:${path17.join(logDir, "truecourse.log")}
126941
- StandardError=append:${path17.join(logDir, "truecourse.error.log")}
127085
+ StandardOutput=append:${path18.join(logDir, "truecourse.log")}
127086
+ StandardError=append:${path18.join(logDir, "truecourse.error.log")}
126942
127087
 
126943
127088
  [Install]
126944
127089
  WantedBy=default.target
@@ -126946,10 +127091,10 @@ WantedBy=default.target
126946
127091
  }
126947
127092
  var LinuxService = class {
126948
127093
  async install(serverPath, logPath) {
126949
- fs12.mkdirSync(UNIT_DIR, { recursive: true });
126950
- fs12.mkdirSync(path17.dirname(logPath), { recursive: true });
127094
+ fs13.mkdirSync(UNIT_DIR, { recursive: true });
127095
+ fs13.mkdirSync(path18.dirname(logPath), { recursive: true });
126951
127096
  const unit = buildUnitFile(serverPath, logPath);
126952
- fs12.writeFileSync(UNIT_PATH, unit, "utf-8");
127097
+ fs13.writeFileSync(UNIT_PATH, unit, "utf-8");
126953
127098
  execSync3("systemctl --user daemon-reload", { stdio: "pipe" });
126954
127099
  execSync3(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "pipe" });
126955
127100
  }
@@ -126962,8 +127107,8 @@ var LinuxService = class {
126962
127107
  execSync3(`systemctl --user disable ${SERVICE_NAME}`, { stdio: "pipe" });
126963
127108
  } catch {
126964
127109
  }
126965
- if (fs12.existsSync(UNIT_PATH)) {
126966
- fs12.unlinkSync(UNIT_PATH);
127110
+ if (fs13.existsSync(UNIT_PATH)) {
127111
+ fs13.unlinkSync(UNIT_PATH);
126967
127112
  }
126968
127113
  try {
126969
127114
  execSync3("systemctl --user daemon-reload", { stdio: "pipe" });
@@ -126992,7 +127137,7 @@ var LinuxService = class {
126992
127137
  }
126993
127138
  }
126994
127139
  async isInstalled() {
126995
- return fs12.existsSync(UNIT_PATH);
127140
+ return fs13.existsSync(UNIT_PATH);
126996
127141
  }
126997
127142
  };
126998
127143
 
@@ -127095,72 +127240,72 @@ function getPlatform() {
127095
127240
  }
127096
127241
 
127097
127242
  // tools/cli/src/commands/service/logs.ts
127098
- import fs13 from "node:fs";
127099
- import path18 from "node:path";
127243
+ import fs14 from "node:fs";
127244
+ import path19 from "node:path";
127100
127245
  import os7 from "node:os";
127101
127246
  import { spawn as spawn4 } from "node:child_process";
127102
127247
  var MAX_LOG_SIZE2 = 10 * 1024 * 1024;
127103
127248
  var MAX_LOG_FILES2 = 5;
127104
127249
  function getLogDir() {
127105
- return path18.join(os7.homedir(), ".truecourse", "logs");
127250
+ return path19.join(os7.homedir(), ".truecourse", "logs");
127106
127251
  }
127107
127252
  function getLogPath() {
127108
- return path18.join(getLogDir(), "truecourse.log");
127253
+ return path19.join(getLogDir(), "truecourse.log");
127109
127254
  }
127110
127255
  function rotateLogs(logDir) {
127111
- const logFile = path18.join(logDir, "truecourse.log");
127112
- if (!fs13.existsSync(logFile)) return;
127113
- const stats = fs13.statSync(logFile);
127256
+ const logFile = path19.join(logDir, "truecourse.log");
127257
+ if (!fs14.existsSync(logFile)) return;
127258
+ const stats = fs14.statSync(logFile);
127114
127259
  if (stats.size < MAX_LOG_SIZE2) return;
127115
127260
  for (let i = MAX_LOG_FILES2; i >= 1; i--) {
127116
- const older = path18.join(logDir, `truecourse.log.${i}`);
127261
+ const older = path19.join(logDir, `truecourse.log.${i}`);
127117
127262
  if (i === MAX_LOG_FILES2) {
127118
- if (fs13.existsSync(older)) fs13.unlinkSync(older);
127263
+ if (fs14.existsSync(older)) fs14.unlinkSync(older);
127119
127264
  } else {
127120
- const newer = path18.join(logDir, `truecourse.log.${i + 1}`);
127121
- if (fs13.existsSync(older)) fs13.renameSync(older, newer);
127265
+ const newer = path19.join(logDir, `truecourse.log.${i + 1}`);
127266
+ if (fs14.existsSync(older)) fs14.renameSync(older, newer);
127122
127267
  }
127123
127268
  }
127124
- fs13.renameSync(logFile, path18.join(logDir, "truecourse.log.1"));
127269
+ fs14.renameSync(logFile, path19.join(logDir, "truecourse.log.1"));
127125
127270
  }
127126
127271
  function rotateErrorLogs(logDir) {
127127
- const logFile = path18.join(logDir, "truecourse.error.log");
127128
- if (!fs13.existsSync(logFile)) return;
127129
- const stats = fs13.statSync(logFile);
127272
+ const logFile = path19.join(logDir, "truecourse.error.log");
127273
+ if (!fs14.existsSync(logFile)) return;
127274
+ const stats = fs14.statSync(logFile);
127130
127275
  if (stats.size < MAX_LOG_SIZE2) return;
127131
127276
  for (let i = MAX_LOG_FILES2; i >= 1; i--) {
127132
- const older = path18.join(logDir, `truecourse.error.log.${i}`);
127277
+ const older = path19.join(logDir, `truecourse.error.log.${i}`);
127133
127278
  if (i === MAX_LOG_FILES2) {
127134
- if (fs13.existsSync(older)) fs13.unlinkSync(older);
127279
+ if (fs14.existsSync(older)) fs14.unlinkSync(older);
127135
127280
  } else {
127136
- const newer = path18.join(logDir, `truecourse.error.log.${i + 1}`);
127137
- if (fs13.existsSync(older)) fs13.renameSync(older, newer);
127281
+ const newer = path19.join(logDir, `truecourse.error.log.${i + 1}`);
127282
+ if (fs14.existsSync(older)) fs14.renameSync(older, newer);
127138
127283
  }
127139
127284
  }
127140
- fs13.renameSync(logFile, path18.join(logDir, "truecourse.error.log.1"));
127285
+ fs14.renameSync(logFile, path19.join(logDir, "truecourse.error.log.1"));
127141
127286
  }
127142
127287
  function tailLogs() {
127143
127288
  const logFile = getLogPath();
127144
- if (!fs13.existsSync(logFile)) {
127289
+ if (!fs14.existsSync(logFile)) {
127145
127290
  console.log("No log file found. Is the service running?");
127146
127291
  console.log(`Expected at: ${logFile}`);
127147
127292
  return;
127148
127293
  }
127149
127294
  if (process.platform === "win32") {
127150
- const content = fs13.readFileSync(logFile, "utf-8");
127295
+ const content = fs14.readFileSync(logFile, "utf-8");
127151
127296
  const lines = content.split("\n");
127152
127297
  const tail = lines.slice(-50);
127153
127298
  for (const line of tail) {
127154
127299
  process.stdout.write(line + "\n");
127155
127300
  }
127156
- let lastSize = fs13.statSync(logFile).size;
127157
- fs13.watchFile(logFile, { interval: 500 }, () => {
127158
- const newSize = fs13.statSync(logFile).size;
127301
+ let lastSize = fs14.statSync(logFile).size;
127302
+ fs14.watchFile(logFile, { interval: 500 }, () => {
127303
+ const newSize = fs14.statSync(logFile).size;
127159
127304
  if (newSize > lastSize) {
127160
- const fd = fs13.openSync(logFile, "r");
127305
+ const fd = fs14.openSync(logFile, "r");
127161
127306
  const buf = Buffer.alloc(newSize - lastSize);
127162
- fs13.readSync(fd, buf, 0, buf.length, lastSize);
127163
- fs13.closeSync(fd);
127307
+ fs14.readSync(fd, buf, 0, buf.length, lastSize);
127308
+ fs14.closeSync(fd);
127164
127309
  process.stdout.write(buf.toString("utf-8"));
127165
127310
  lastSize = newSize;
127166
127311
  }
@@ -127181,15 +127326,15 @@ function tailLogs() {
127181
127326
  }
127182
127327
 
127183
127328
  // tools/cli/src/commands/dashboard.ts
127184
- var __dirname3 = path19.dirname(fileURLToPath4(import.meta.url));
127329
+ var __dirname3 = path20.dirname(fileURLToPath5(import.meta.url));
127185
127330
  function resolveServerEntry() {
127186
127331
  const candidates = [
127187
127332
  // Packaged CLI: dist/cli.mjs next to dist/server.mjs
127188
- path19.join(__dirname3, "server.mjs"),
127333
+ path20.join(__dirname3, "server.mjs"),
127189
127334
  // Source build output: tools/cli/dist → ../../../../dist/server.mjs
127190
- path19.resolve(__dirname3, "..", "..", "..", "..", "dist", "server.mjs")
127335
+ path20.resolve(__dirname3, "..", "..", "..", "..", "dist", "server.mjs")
127191
127336
  ];
127192
- return candidates.find((p2) => fs14.existsSync(p2)) ?? null;
127337
+ return candidates.find((p2) => fs15.existsSync(p2)) ?? null;
127193
127338
  }
127194
127339
  async function waitForHealth(url, timeoutMs = 3e4) {
127195
127340
  const start = Date.now();
@@ -127282,6 +127427,27 @@ async function runServiceMode(serverEntry) {
127282
127427
  O2.success(`Dashboard open at ${target}`);
127283
127428
  O2.info("Stop the dashboard with: truecourse dashboard stop");
127284
127429
  }
127430
+ async function probeHealth(url) {
127431
+ try {
127432
+ const res = await fetch(`${url}/api/health`);
127433
+ return res.ok;
127434
+ } catch {
127435
+ return false;
127436
+ }
127437
+ }
127438
+ async function detectRunningState() {
127439
+ const platform = getPlatform();
127440
+ const url = getServerUrl();
127441
+ const healthy = await probeHealth(url);
127442
+ if (await platform.isInstalled()) {
127443
+ const { running, pid } = await platform.status();
127444
+ if (running) return { mode: "service", pid, healthy };
127445
+ if (healthy) return { mode: "console", healthy: true };
127446
+ return { mode: "none" };
127447
+ }
127448
+ if (healthy) return { mode: "console", healthy: true };
127449
+ return { mode: "none" };
127450
+ }
127285
127451
  async function runDashboard(options = {}) {
127286
127452
  mt("Opening TrueCourse dashboard");
127287
127453
  const serverEntry = resolveServerEntry();
@@ -127291,10 +127457,35 @@ async function runDashboard(options = {}) {
127291
127457
  );
127292
127458
  process.exit(1);
127293
127459
  }
127294
- const configured = fs14.existsSync(getConfigPath());
127295
- const shouldPrompt = !configured || options.reconfigure;
127296
- const runMode = shouldPrompt ? await promptRunMode() : readConfig().runMode;
127297
- if (shouldPrompt) writeConfig({ runMode });
127460
+ const configured = fs15.existsSync(getConfigPath());
127461
+ const needsDecision = !configured || options.reconfigure;
127462
+ let runMode;
127463
+ if (options.mode) {
127464
+ runMode = options.mode;
127465
+ } else if (needsDecision) {
127466
+ if (!isInteractive()) {
127467
+ exitMissingNonInteractiveFlag(
127468
+ "Dashboard run mode is not configured.",
127469
+ "Pass --service for the background service or --console to run in this terminal."
127470
+ );
127471
+ }
127472
+ runMode = await promptRunMode();
127473
+ } else {
127474
+ runMode = readConfig().runMode;
127475
+ }
127476
+ const shouldPersist = needsDecision || options.mode !== void 0;
127477
+ if (runMode !== "service") {
127478
+ const platform = getPlatform();
127479
+ if (await platform.isInstalled()) {
127480
+ const { running } = await platform.status();
127481
+ if (running) {
127482
+ O2.error(
127483
+ "A dashboard service is already installed and running. Stop and remove it first: `truecourse dashboard uninstall`, then rerun `truecourse dashboard`."
127484
+ );
127485
+ process.exit(1);
127486
+ }
127487
+ }
127488
+ }
127298
127489
  if (runMode === "service") {
127299
127490
  try {
127300
127491
  await runServiceMode(serverEntry);
@@ -127302,70 +127493,68 @@ async function runDashboard(options = {}) {
127302
127493
  O2.error(`Service mode failed: ${err.message}`);
127303
127494
  O2.info("Falling back to console mode.");
127304
127495
  await runConsoleMode(serverEntry);
127496
+ if (shouldPersist) writeConfig({ runMode: "console" });
127497
+ return;
127305
127498
  }
127499
+ if (shouldPersist) writeConfig({ runMode: "service" });
127306
127500
  } else {
127307
127501
  await runConsoleMode(serverEntry);
127502
+ if (shouldPersist) writeConfig({ runMode: "console" });
127308
127503
  }
127309
127504
  }
127310
127505
  async function runDashboardStop() {
127311
- const config2 = readConfig();
127312
- if (config2.runMode !== "service") {
127313
- O2.info("Dashboard is running in console mode. Press Ctrl+C in its terminal to stop.");
127314
- return;
127315
- }
127316
- const platform = getPlatform();
127317
- const { running } = await platform.status();
127318
- if (!running) {
127319
- O2.info("Dashboard is not running.");
127320
- return;
127506
+ const state = await detectRunningState();
127507
+ switch (state.mode) {
127508
+ case "service": {
127509
+ O2.step("Stopping dashboard service...");
127510
+ await getPlatform().stop();
127511
+ O2.success("Dashboard service stopped.");
127512
+ return;
127513
+ }
127514
+ case "console": {
127515
+ O2.info(
127516
+ "A dashboard is running in console mode (not managed by the service). Press Ctrl+C in its terminal to stop it."
127517
+ );
127518
+ return;
127519
+ }
127520
+ case "none": {
127521
+ O2.info("Dashboard is not running.");
127522
+ return;
127523
+ }
127321
127524
  }
127322
- O2.step("Stopping dashboard...");
127323
- await platform.stop();
127324
- O2.success("Dashboard stopped.");
127325
127525
  }
127326
127526
  async function runDashboardStatus() {
127327
- const config2 = readConfig();
127527
+ const state = await detectRunningState();
127328
127528
  const url = getServerUrl();
127329
- if (config2.runMode !== "service") {
127330
- try {
127331
- const res = await fetch(`${url}/api/health`);
127332
- if (res.ok) {
127333
- O2.success(`Dashboard is running in console mode at ${url}`);
127529
+ switch (state.mode) {
127530
+ case "service": {
127531
+ const pidInfo = state.pid ? ` (PID: ${state.pid})` : "";
127532
+ O2.success(`Dashboard service is running${pidInfo}`);
127533
+ if (state.healthy) {
127534
+ O2.info(`Server is healthy at ${url}`);
127334
127535
  } else {
127335
- O2.info("Dashboard is not running.");
127536
+ O2.warn(`Service process is running but server is not responding at ${url}.`);
127336
127537
  }
127337
- } catch {
127338
- O2.info("Dashboard is not running.");
127538
+ return;
127339
127539
  }
127340
- return;
127341
- }
127342
- const platform = getPlatform();
127343
- const installed = await platform.isInstalled();
127344
- if (!installed) {
127345
- O2.info("Dashboard service is not installed. Run `truecourse dashboard` to set it up.");
127346
- return;
127347
- }
127348
- const { running, pid } = await platform.status();
127349
- if (!running) {
127350
- O2.info("Dashboard service is installed but not running.");
127351
- return;
127352
- }
127353
- const pidInfo = pid ? ` (PID: ${pid})` : "";
127354
- O2.success(`Dashboard service is running${pidInfo}`);
127355
- try {
127356
- const res = await fetch(`${url}/api/health`);
127357
- if (res.ok) {
127358
- O2.info(`Server is healthy at ${url}`);
127359
- } else {
127360
- O2.warn(`Server returned status ${res.status}`);
127540
+ case "console": {
127541
+ O2.success(`Dashboard is running in console mode at ${url}`);
127542
+ return;
127543
+ }
127544
+ case "none": {
127545
+ const platform = getPlatform();
127546
+ if (await platform.isInstalled()) {
127547
+ O2.info("Dashboard service is installed but not running.");
127548
+ } else {
127549
+ O2.info("Dashboard is not running.");
127550
+ }
127551
+ return;
127361
127552
  }
127362
- } catch {
127363
- O2.warn("Service process is running but server is not responding.");
127364
127553
  }
127365
127554
  }
127366
- function runDashboardLogs() {
127367
- const config2 = readConfig();
127368
- if (config2.runMode !== "service") {
127555
+ async function runDashboardLogs() {
127556
+ const state = await detectRunningState();
127557
+ if (state.mode === "console") {
127369
127558
  O2.info("Dashboard is running in console mode \u2014 logs print to its terminal.");
127370
127559
  return;
127371
127560
  }
@@ -127402,27 +127591,38 @@ var SEVERITY_ORDER = {
127402
127591
  low: 3,
127403
127592
  info: 4
127404
127593
  };
127594
+ var SEVERITIES = ["critical", "high", "medium", "low", "info"];
127405
127595
  function listViolations(repoPath, options = {}) {
127406
127596
  const latest = readLatest(repoPath);
127407
127597
  if (!latest)
127408
127598
  return { violations: [], total: 0 };
127409
- if (options.analysisId && latest.analysis.id !== options.analysisId) {
127410
- return { violations: [], total: 0 };
127599
+ let violations;
127600
+ if (!options.analysisId || latest.analysis.id === options.analysisId) {
127601
+ violations = latest.violations;
127602
+ } else {
127603
+ const historical = readActiveViolationsAt(repoPath, options.analysisId);
127604
+ if (!historical)
127605
+ return { violations: [], total: 0 };
127606
+ violations = historical;
127411
127607
  }
127412
127608
  const statusMode = options.status ?? "active";
127413
127609
  let filtered;
127414
127610
  if (statusMode === "resolved") {
127415
- filtered = latest.violations.filter((v) => v.status === "resolved");
127611
+ filtered = violations.filter((v) => v.status === "resolved");
127416
127612
  } else if (statusMode === "all") {
127417
- filtered = latest.violations;
127613
+ filtered = violations;
127418
127614
  } else {
127419
127615
  const active = ["new", "unchanged"];
127420
- filtered = latest.violations.filter((v) => active.includes(v.status));
127616
+ filtered = violations.filter((v) => active.includes(v.status));
127421
127617
  }
127422
127618
  if (options.filePath) {
127423
127619
  const absPath = options.filePath.startsWith("/") ? options.filePath : `${repoPath}/${options.filePath}`;
127424
127620
  filtered = filtered.filter((v) => v.type === "code" && (v.filePath === absPath || v.filePath === options.filePath));
127425
127621
  }
127622
+ if (options.severity !== void 0) {
127623
+ const allowed = (Array.isArray(options.severity) ? options.severity : [options.severity]).map((s) => s.toLowerCase());
127624
+ filtered = filtered.filter((v) => allowed.includes(v.severity.toLowerCase()));
127625
+ }
127426
127626
  filtered.sort((a, b) => {
127427
127627
  const sa = SEVERITY_ORDER[a.severity] ?? 5;
127428
127628
  const sb = SEVERITY_ORDER[b.severity] ?? 5;
@@ -127436,6 +127636,42 @@ function listViolations(repoPath, options = {}) {
127436
127636
  const paged = limit > 0 ? filtered.slice(offset, offset + limit) : filtered;
127437
127637
  return { violations: paged, total };
127438
127638
  }
127639
+ function readActiveViolationsAt(repoPath, analysisId) {
127640
+ const targetFile = findAnalysisFilename(repoPath, analysisId);
127641
+ if (!targetFile)
127642
+ return null;
127643
+ const targetSnap = readAnalysis(repoPath, targetFile);
127644
+ if (!targetSnap)
127645
+ return null;
127646
+ const files = listAnalyses(repoPath);
127647
+ const targetIdx = files.indexOf(targetFile);
127648
+ if (targetIdx === -1)
127649
+ return null;
127650
+ const active = /* @__PURE__ */ new Map();
127651
+ for (let i = 0; i <= targetIdx; i++) {
127652
+ const snap = readAnalysis(repoPath, files[i]);
127653
+ if (!snap)
127654
+ continue;
127655
+ for (const r of snap.violations.resolved)
127656
+ active.delete(r.id);
127657
+ for (const a of snap.violations.added)
127658
+ active.set(a.id, a);
127659
+ }
127660
+ return [...active.values()].map(denormalizeAgainst(targetSnap.graph));
127661
+ }
127662
+ function denormalizeAgainst(graph) {
127663
+ const serviceById = new Map(graph.services.map((s) => [s.id, s.name]));
127664
+ const moduleById = new Map(graph.modules.map((m) => [m.id, m.name]));
127665
+ const methodById = new Map(graph.methods.map((m) => [m.id, m.name]));
127666
+ const databaseById = new Map(graph.databases.map((d3) => [d3.id, d3.name]));
127667
+ return (v) => ({
127668
+ ...v,
127669
+ targetServiceName: v.targetServiceId ? serviceById.get(v.targetServiceId) ?? null : null,
127670
+ targetModuleName: v.targetModuleId ? moduleById.get(v.targetModuleId) ?? null : null,
127671
+ targetMethodName: v.targetMethodId ? methodById.get(v.targetMethodId) ?? null : null,
127672
+ targetDatabaseName: v.targetDatabaseId ? databaseById.get(v.targetDatabaseId) ?? null : null
127673
+ });
127674
+ }
127439
127675
  function getDiffResult(repoPath) {
127440
127676
  const diff = readDiff(repoPath);
127441
127677
  if (!diff)
@@ -127447,19 +127683,37 @@ function getDiffResult(repoPath) {
127447
127683
 
127448
127684
  // tools/cli/src/commands/list.ts
127449
127685
  init_helpers();
127450
- async function runList({ limit = 20, offset = 0 } = {}) {
127686
+ async function runList({ limit = 20, offset = 0, severity } = {}) {
127451
127687
  mt("Violations");
127452
127688
  const repo = requireRegisteredRepo();
127453
127689
  const { violations, total } = listViolations(repo.path, {
127454
127690
  limit: isFinite(limit) ? limit : 0,
127455
- offset
127691
+ offset,
127692
+ severity
127456
127693
  });
127457
127694
  if (total === 0 && violations.length === 0) {
127458
- O2.info("No violations. Run `truecourse analyze` if you haven't yet.");
127695
+ if (severity && severity.length > 0) {
127696
+ O2.info(`No violations match severity filter: ${severity.join(", ")}.`);
127697
+ } else {
127698
+ O2.info("No violations. Run `truecourse analyze` if you haven't yet.");
127699
+ }
127459
127700
  return;
127460
127701
  }
127461
127702
  renderViolations(violations, { total, offset });
127462
127703
  }
127704
+ function parseSeverityFlag(raw) {
127705
+ if (!raw) return void 0;
127706
+ const parts = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
127707
+ if (parts.length === 0) return void 0;
127708
+ const invalid = parts.filter((s) => !SEVERITIES.includes(s));
127709
+ if (invalid.length > 0) {
127710
+ console.error(
127711
+ `error: unknown severity value(s): ${invalid.join(", ")}. Valid: ${SEVERITIES.join(", ")}`
127712
+ );
127713
+ process.exit(1);
127714
+ }
127715
+ return parts;
127716
+ }
127463
127717
  async function runListDiff() {
127464
127718
  mt("Diff check results");
127465
127719
  const repo = requireRegisteredRepo();
@@ -127552,8 +127806,8 @@ async function runRulesLlm(options) {
127552
127806
 
127553
127807
  // tools/cli/src/commands/hooks.ts
127554
127808
  import { execSync as execSync5 } from "node:child_process";
127555
- import fs15 from "node:fs";
127556
- import path20 from "node:path";
127809
+ import fs16 from "node:fs";
127810
+ import path21 from "node:path";
127557
127811
 
127558
127812
  // node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/dist/js-yaml.mjs
127559
127813
  function isNothing(subject) {
@@ -130197,22 +130451,22 @@ var DEFAULT_TIMEOUT_MS = 3e4;
130197
130451
  function findGitDir(from) {
130198
130452
  let dir = from;
130199
130453
  while (true) {
130200
- const gitPath = path20.join(dir, ".git");
130201
- if (fs15.existsSync(gitPath)) {
130202
- const stat = fs15.statSync(gitPath);
130454
+ const gitPath = path21.join(dir, ".git");
130455
+ if (fs16.existsSync(gitPath)) {
130456
+ const stat = fs16.statSync(gitPath);
130203
130457
  if (stat.isDirectory()) {
130204
130458
  return gitPath;
130205
130459
  }
130206
130460
  if (stat.isFile()) {
130207
- const content = fs15.readFileSync(gitPath, "utf-8").trim();
130461
+ const content = fs16.readFileSync(gitPath, "utf-8").trim();
130208
130462
  const match2 = content.match(/^gitdir:\s*(.+)$/);
130209
130463
  if (match2) {
130210
- const resolved = path20.resolve(dir, match2[1]);
130464
+ const resolved = path21.resolve(dir, match2[1]);
130211
130465
  return resolved;
130212
130466
  }
130213
130467
  }
130214
130468
  }
130215
- const parent = path20.dirname(dir);
130469
+ const parent = path21.dirname(dir);
130216
130470
  if (parent === dir) return null;
130217
130471
  dir = parent;
130218
130472
  }
@@ -130220,10 +130474,10 @@ function findGitDir(from) {
130220
130474
  function findProjectRoot(from) {
130221
130475
  let dir = from;
130222
130476
  while (true) {
130223
- if (fs15.existsSync(path20.join(dir, ".git"))) {
130477
+ if (fs16.existsSync(path21.join(dir, ".git"))) {
130224
130478
  return dir;
130225
130479
  }
130226
- const parent = path20.dirname(dir);
130480
+ const parent = path21.dirname(dir);
130227
130481
  if (parent === dir) return null;
130228
130482
  dir = parent;
130229
130483
  }
@@ -130238,10 +130492,10 @@ function parseTimeout(value) {
130238
130492
  return num * 1e3;
130239
130493
  }
130240
130494
  function loadConfig(projectRoot) {
130241
- const configPath = path20.join(projectRoot, ".truecourse", "hooks.yaml");
130242
- if (!fs15.existsSync(configPath)) return {};
130495
+ const configPath = path21.join(projectRoot, ".truecourse", "hooks.yaml");
130496
+ if (!fs16.existsSync(configPath)) return {};
130243
130497
  try {
130244
- const raw = fs15.readFileSync(configPath, "utf-8");
130498
+ const raw = fs16.readFileSync(configPath, "utf-8");
130245
130499
  return jsYaml.load(raw) || {};
130246
130500
  } catch {
130247
130501
  return {};
@@ -130260,11 +130514,11 @@ function runHooksInstall() {
130260
130514
  console.error("Error: Not a git repository.");
130261
130515
  process.exit(1);
130262
130516
  }
130263
- const hooksDir = path20.join(gitDir, "hooks");
130264
- fs15.mkdirSync(hooksDir, { recursive: true });
130265
- const hookPath = path20.join(hooksDir, "pre-commit");
130266
- if (fs15.existsSync(hookPath)) {
130267
- const existing = fs15.readFileSync(hookPath, "utf-8");
130517
+ const hooksDir = path21.join(gitDir, "hooks");
130518
+ fs16.mkdirSync(hooksDir, { recursive: true });
130519
+ const hookPath = path21.join(hooksDir, "pre-commit");
130520
+ if (fs16.existsSync(hookPath)) {
130521
+ const existing = fs16.readFileSync(hookPath, "utf-8");
130268
130522
  if (!existing.includes(HOOK_IDENTIFIER)) {
130269
130523
  console.error(
130270
130524
  "Error: A pre-commit hook already exists and was not installed by TrueCourse."
@@ -130274,7 +130528,7 @@ function runHooksInstall() {
130274
130528
  process.exit(1);
130275
130529
  }
130276
130530
  }
130277
- fs15.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 493 });
130531
+ fs16.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 493 });
130278
130532
  console.log("TrueCourse pre-commit hook installed.");
130279
130533
  console.log(` ${hookPath}`);
130280
130534
  }
@@ -130284,17 +130538,17 @@ function runHooksUninstall() {
130284
130538
  console.error("Error: Not a git repository.");
130285
130539
  process.exit(1);
130286
130540
  }
130287
- const hookPath = path20.join(gitDir, "hooks", "pre-commit");
130288
- if (!fs15.existsSync(hookPath)) {
130541
+ const hookPath = path21.join(gitDir, "hooks", "pre-commit");
130542
+ if (!fs16.existsSync(hookPath)) {
130289
130543
  console.log("No pre-commit hook installed.");
130290
130544
  return;
130291
130545
  }
130292
- const content = fs15.readFileSync(hookPath, "utf-8");
130546
+ const content = fs16.readFileSync(hookPath, "utf-8");
130293
130547
  if (!content.includes(HOOK_IDENTIFIER)) {
130294
130548
  console.error("Error: The pre-commit hook was not installed by TrueCourse. Leaving it in place.");
130295
130549
  process.exit(1);
130296
130550
  }
130297
- fs15.unlinkSync(hookPath);
130551
+ fs16.unlinkSync(hookPath);
130298
130552
  console.log("TrueCourse pre-commit hook removed.");
130299
130553
  }
130300
130554
  function runHooksStatus() {
@@ -130303,8 +130557,8 @@ function runHooksStatus() {
130303
130557
  console.error("Error: Not a git repository.");
130304
130558
  process.exit(1);
130305
130559
  }
130306
- const hookPath = path20.join(gitDir, "hooks", "pre-commit");
130307
- const installed = fs15.existsSync(hookPath) && fs15.readFileSync(hookPath, "utf-8").includes(HOOK_IDENTIFIER);
130560
+ const hookPath = path21.join(gitDir, "hooks", "pre-commit");
130561
+ const installed = fs16.existsSync(hookPath) && fs16.readFileSync(hookPath, "utf-8").includes(HOOK_IDENTIFIER);
130308
130562
  if (installed) {
130309
130563
  console.log("TrueCourse pre-commit hook: installed");
130310
130564
  console.log(` ${hookPath}`);
@@ -130314,8 +130568,8 @@ function runHooksStatus() {
130314
130568
  }
130315
130569
  const projectRoot = findProjectRoot(process.cwd());
130316
130570
  if (projectRoot) {
130317
- const configPath = path20.join(projectRoot, ".truecourse", "hooks.yaml");
130318
- if (fs15.existsSync(configPath)) {
130571
+ const configPath = path21.join(projectRoot, ".truecourse", "hooks.yaml");
130572
+ if (fs16.existsSync(configPath)) {
130319
130573
  console.log(`
130320
130574
  Config: ${configPath}`);
130321
130575
  const config2 = loadConfig(projectRoot);
@@ -130385,6 +130639,7 @@ async function runHooksRun() {
130385
130639
  detectLanguage2 = analyzer.detectLanguage;
130386
130640
  checkCodeRules2 = analyzer.checkCodeRules;
130387
130641
  CODE_RULES2 = analyzer.CODE_RULES;
130642
+ await analyzer.initParsers();
130388
130643
  } catch {
130389
130644
  console.log(" skipped (analyzer not available)");
130390
130645
  process.exit(0);
@@ -130440,9 +130695,14 @@ async function runHooksRun() {
130440
130695
 
130441
130696
  // tools/cli/src/index.ts
130442
130697
  var program2 = new Command();
130443
- program2.name("truecourse").version("0.4.5").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130444
- var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").option("--reconfigure", "Re-prompt for console vs background service mode").action(async (options) => {
130445
- await runDashboard({ reconfigure: options.reconfigure });
130698
+ program2.name("truecourse").version("0.5.1").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130699
+ var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").option("--reconfigure", "Re-prompt for console vs background service mode").option("--service", "Run as a background service (skips mode prompt)").option("--console", "Run in this terminal (skips mode prompt)").action(async (options) => {
130700
+ if (options.service && options.console) {
130701
+ console.error("error: --service and --console are mutually exclusive");
130702
+ process.exit(1);
130703
+ }
130704
+ const mode = options.service ? "service" : options.console ? "console" : void 0;
130705
+ await runDashboard({ reconfigure: options.reconfigure, mode });
130446
130706
  });
130447
130707
  dashboardCmd.command("stop").description("Stop the dashboard").action(async () => {
130448
130708
  await runDashboardStop();
@@ -130450,29 +130710,40 @@ dashboardCmd.command("stop").description("Stop the dashboard").action(async () =
130450
130710
  dashboardCmd.command("status").description("Show dashboard status").action(async () => {
130451
130711
  await runDashboardStatus();
130452
130712
  });
130453
- dashboardCmd.command("logs").description("Tail dashboard logs (service mode only)").action(() => {
130454
- runDashboardLogs();
130713
+ dashboardCmd.command("logs").description("Tail dashboard logs (service mode only)").action(async () => {
130714
+ await runDashboardLogs();
130455
130715
  });
130456
130716
  dashboardCmd.command("uninstall").description("Remove the background service and revert to console mode").action(async () => {
130457
130717
  await runDashboardUninstall();
130458
130718
  });
130459
- program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").action(async (options) => {
130719
+ function resolveInstallSkills(options) {
130720
+ if (options.installSkills === true) return true;
130721
+ if (options.skills === false) return false;
130722
+ return void 0;
130723
+ }
130724
+ program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").option("--llm", "Run LLM-powered rules (pre-approves the cost estimate)").option("--no-llm", "Skip LLM-powered rules for this run").option("--install-skills", "Install Claude Code skills without prompting").option("--no-skills", "Skip the Claude Code skills prompt").action(async (options) => {
130725
+ const llm = typeof options.llm === "boolean" ? options.llm : void 0;
130726
+ const installSkills = resolveInstallSkills(options);
130460
130727
  if (options.diff) {
130461
- await runAnalyzeDiff();
130728
+ await runAnalyzeDiff({ llm, installSkills });
130462
130729
  } else {
130463
- await runAnalyze();
130730
+ await runAnalyze({ llm, installSkills });
130464
130731
  }
130465
130732
  });
130466
- program2.command("add").description("Register the current directory with TrueCourse").action(async () => {
130467
- await runAdd();
130733
+ program2.command("add").description("Register the current directory with TrueCourse").option("--install-skills", "Install Claude Code skills without prompting").option("--no-skills", "Skip the Claude Code skills prompt").action(async (options) => {
130734
+ await runAdd({ installSkills: resolveInstallSkills(options) });
130468
130735
  });
130469
- program2.command("list").description("List violations from the latest analysis").option("--diff", "Show diff check results (new and resolved)").option("--limit <n>", "Number of violations to show (default: 20)", parseInt).option("--offset <n>", "Skip first N violations", parseInt).option("--all", "Show all violations").action(async (options) => {
130736
+ program2.command("list").description("List violations from the latest analysis").option("--diff", "Show diff check results (new and resolved)").option("--limit <n>", "Number of violations to show (default: 20)", parseInt).option("--offset <n>", "Skip first N violations", parseInt).option("--all", "Show all violations").option(
130737
+ "--severity <list>",
130738
+ "Comma-separated severities to include (critical,high,medium,low,info)"
130739
+ ).action(async (options) => {
130470
130740
  if (options.diff) {
130471
130741
  await runListDiff();
130472
130742
  } else {
130473
130743
  await runList({
130474
130744
  limit: options.all ? Infinity : options.limit ?? 20,
130475
- offset: options.offset ?? 0
130745
+ offset: options.offset ?? 0,
130746
+ severity: parseSeverityFlag(options.severity)
130476
130747
  });
130477
130748
  }
130478
130749
  });