truecourse 0.5.6 → 0.5.7
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/README.md +20 -1
- package/cli.mjs +402 -255
- package/package.json +1 -1
- package/public/assets/index-DU0Bp1u7.css +1 -0
- package/public/assets/{index-BCi5yD5r.js → index-DaPvUT-_.js} +94 -94
- package/public/index.html +2 -2
- package/server.mjs +394 -249
- package/public/assets/index-C6z6cTBb.css +0 -1
package/cli.mjs
CHANGED
|
@@ -10586,7 +10586,72 @@ var init_language_config = __esm({
|
|
|
10586
10586
|
|
|
10587
10587
|
// packages/analyzer/dist/file-discovery.js
|
|
10588
10588
|
import { existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync } from "fs";
|
|
10589
|
+
import { execFileSync } from "child_process";
|
|
10589
10590
|
import { join, relative, resolve as resolve2 } from "path";
|
|
10591
|
+
function isInsideGitWorkTree(dir) {
|
|
10592
|
+
try {
|
|
10593
|
+
const out = execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
|
|
10594
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
10595
|
+
});
|
|
10596
|
+
return out.toString("utf8").trim() === "true";
|
|
10597
|
+
} catch {
|
|
10598
|
+
return false;
|
|
10599
|
+
}
|
|
10600
|
+
}
|
|
10601
|
+
function buildPostGitFilter(dir) {
|
|
10602
|
+
const ig = (0, import_ignore.default)();
|
|
10603
|
+
const tcPath = join(dir, ".truecourseignore");
|
|
10604
|
+
if (existsSync2(tcPath))
|
|
10605
|
+
ig.add(readFileSync(tcPath, "utf8"));
|
|
10606
|
+
ig.add(".git");
|
|
10607
|
+
for (const p2 of getAllTestPatterns())
|
|
10608
|
+
ig.add(p2);
|
|
10609
|
+
for (const p2 of getAllIgnorePatterns())
|
|
10610
|
+
ig.add(p2);
|
|
10611
|
+
return ig;
|
|
10612
|
+
}
|
|
10613
|
+
function discoverFilesViaGit(dir) {
|
|
10614
|
+
if (!isInsideGitWorkTree(dir))
|
|
10615
|
+
return null;
|
|
10616
|
+
let stdout;
|
|
10617
|
+
try {
|
|
10618
|
+
stdout = execFileSync("git", ["-C", dir, "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "."], { maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
10619
|
+
} catch {
|
|
10620
|
+
return null;
|
|
10621
|
+
}
|
|
10622
|
+
const relPaths = stdout.toString("utf8").split("\0").filter(Boolean);
|
|
10623
|
+
relPaths.sort(compareDepthFirst);
|
|
10624
|
+
const ig = buildPostGitFilter(dir);
|
|
10625
|
+
const out = [];
|
|
10626
|
+
for (const rel of relPaths) {
|
|
10627
|
+
if (ig.ignores(rel))
|
|
10628
|
+
continue;
|
|
10629
|
+
const abs = join(dir, rel);
|
|
10630
|
+
let isFile = false;
|
|
10631
|
+
try {
|
|
10632
|
+
isFile = statSync(abs).isFile();
|
|
10633
|
+
} catch {
|
|
10634
|
+
continue;
|
|
10635
|
+
}
|
|
10636
|
+
if (!isFile)
|
|
10637
|
+
continue;
|
|
10638
|
+
if (detectLanguage(abs))
|
|
10639
|
+
out.push(abs);
|
|
10640
|
+
}
|
|
10641
|
+
return out;
|
|
10642
|
+
}
|
|
10643
|
+
function compareDepthFirst(a, b) {
|
|
10644
|
+
const ap = a.split("/");
|
|
10645
|
+
const bp = b.split("/");
|
|
10646
|
+
const len = Math.min(ap.length, bp.length);
|
|
10647
|
+
for (let i = 0; i < len; i++) {
|
|
10648
|
+
const x = ap[i];
|
|
10649
|
+
const y = bp[i];
|
|
10650
|
+
if (x !== y)
|
|
10651
|
+
return x < y ? -1 : 1;
|
|
10652
|
+
}
|
|
10653
|
+
return ap.length - bp.length;
|
|
10654
|
+
}
|
|
10590
10655
|
function findAllGitignores(startDir) {
|
|
10591
10656
|
const gitignores = [];
|
|
10592
10657
|
let currentDir = resolve2(startDir);
|
|
@@ -10596,25 +10661,46 @@ function findAllGitignores(startDir) {
|
|
|
10596
10661
|
gitignores.unshift({ path: gitignorePath, dir: currentDir });
|
|
10597
10662
|
}
|
|
10598
10663
|
const parentDir = resolve2(currentDir, "..");
|
|
10599
|
-
if (parentDir === currentDir)
|
|
10664
|
+
if (parentDir === currentDir)
|
|
10600
10665
|
break;
|
|
10601
|
-
}
|
|
10602
10666
|
currentDir = parentDir;
|
|
10603
10667
|
}
|
|
10604
10668
|
return gitignores;
|
|
10605
10669
|
}
|
|
10670
|
+
function reanchorTruecourseignore(content, prefix) {
|
|
10671
|
+
if (prefix === "" || prefix === ".")
|
|
10672
|
+
return content;
|
|
10673
|
+
return content.split("\n").map((line) => {
|
|
10674
|
+
const raw = line;
|
|
10675
|
+
const trimmed2 = raw.trim();
|
|
10676
|
+
if (trimmed2 === "" || trimmed2.startsWith("#"))
|
|
10677
|
+
return raw;
|
|
10678
|
+
const negate = trimmed2.startsWith("!");
|
|
10679
|
+
const body = negate ? trimmed2.slice(1) : trimmed2;
|
|
10680
|
+
if (body.startsWith("**/"))
|
|
10681
|
+
return raw;
|
|
10682
|
+
const hasLeadingSlash = body.startsWith("/");
|
|
10683
|
+
const withoutTrailing = body.endsWith("/") ? body.slice(0, -1) : body;
|
|
10684
|
+
const inner = hasLeadingSlash ? withoutTrailing.slice(1) : withoutTrailing;
|
|
10685
|
+
const hasInternalSlash = inner.includes("/");
|
|
10686
|
+
if (!hasLeadingSlash && !hasInternalSlash)
|
|
10687
|
+
return raw;
|
|
10688
|
+
const stripped = hasLeadingSlash ? body.slice(1) : body;
|
|
10689
|
+
return `${negate ? "!" : ""}${prefix}/${stripped}`;
|
|
10690
|
+
}).join("\n");
|
|
10691
|
+
}
|
|
10606
10692
|
function loadIgnorePatterns(baseDir) {
|
|
10607
10693
|
const ig = (0, import_ignore.default)();
|
|
10608
10694
|
const gitignores = findAllGitignores(baseDir);
|
|
10609
10695
|
const rootDir = gitignores.length > 0 && gitignores[0] ? gitignores[0].dir : baseDir;
|
|
10610
10696
|
for (const { path: gitignorePath } of gitignores) {
|
|
10611
|
-
|
|
10612
|
-
ig.add(content);
|
|
10697
|
+
ig.add(readFileSync(gitignorePath, "utf8"));
|
|
10613
10698
|
}
|
|
10614
10699
|
const truecourseignorePath = join(baseDir, ".truecourseignore");
|
|
10615
10700
|
if (existsSync2(truecourseignorePath)) {
|
|
10616
10701
|
const content = readFileSync(truecourseignorePath, "utf8");
|
|
10617
|
-
|
|
10702
|
+
const prefix = relative(rootDir, baseDir).replace(/\\/g, "/");
|
|
10703
|
+
ig.add(reanchorTruecourseignore(content, prefix));
|
|
10618
10704
|
}
|
|
10619
10705
|
ig.add(".git");
|
|
10620
10706
|
for (const pattern of getAllTestPatterns())
|
|
@@ -10623,7 +10709,7 @@ function loadIgnorePatterns(baseDir) {
|
|
|
10623
10709
|
ig.add(pattern);
|
|
10624
10710
|
return { ig, rootDir };
|
|
10625
10711
|
}
|
|
10626
|
-
function
|
|
10712
|
+
function discoverFilesViaWalker(dir) {
|
|
10627
10713
|
const files = [];
|
|
10628
10714
|
const { ig, rootDir } = loadIgnorePatterns(dir);
|
|
10629
10715
|
function traverse(currentPath) {
|
|
@@ -10634,23 +10720,27 @@ function discoverFiles(dir) {
|
|
|
10634
10720
|
const relativePath = relative(rootDir, fullPath);
|
|
10635
10721
|
const stat = statSync(fullPath);
|
|
10636
10722
|
const pathToCheck = stat.isDirectory() ? relativePath + "/" : relativePath;
|
|
10637
|
-
if (ig.ignores(pathToCheck))
|
|
10723
|
+
if (ig.ignores(pathToCheck))
|
|
10638
10724
|
continue;
|
|
10639
|
-
}
|
|
10640
10725
|
if (stat.isDirectory()) {
|
|
10641
10726
|
traverse(fullPath);
|
|
10642
10727
|
} else if (stat.isFile()) {
|
|
10643
|
-
if (detectLanguage(fullPath))
|
|
10728
|
+
if (detectLanguage(fullPath))
|
|
10644
10729
|
files.push(fullPath);
|
|
10645
|
-
}
|
|
10646
10730
|
}
|
|
10647
10731
|
}
|
|
10648
|
-
} catch
|
|
10732
|
+
} catch {
|
|
10649
10733
|
}
|
|
10650
10734
|
}
|
|
10651
10735
|
traverse(dir);
|
|
10652
10736
|
return files;
|
|
10653
10737
|
}
|
|
10738
|
+
function discoverFiles(dir) {
|
|
10739
|
+
const viaGit = discoverFilesViaGit(dir);
|
|
10740
|
+
if (viaGit !== null)
|
|
10741
|
+
return viaGit;
|
|
10742
|
+
return discoverFilesViaWalker(dir);
|
|
10743
|
+
}
|
|
10654
10744
|
var import_ignore;
|
|
10655
10745
|
var init_file_discovery = __esm({
|
|
10656
10746
|
"packages/analyzer/dist/file-discovery.js"() {
|
|
@@ -93831,7 +93921,6 @@ var init_dist6 = __esm({
|
|
|
93831
93921
|
});
|
|
93832
93922
|
|
|
93833
93923
|
// packages/core/dist/services/analyzer.service.js
|
|
93834
|
-
import path11 from "node:path";
|
|
93835
93924
|
function runDeterministicModuleChecks(result, enabledDeterministic) {
|
|
93836
93925
|
if (!result.modules || !result.methods)
|
|
93837
93926
|
return [];
|
|
@@ -93864,150 +93953,122 @@ function runDeterministicServiceChecks(result, enabledDeterministic) {
|
|
|
93864
93953
|
}
|
|
93865
93954
|
async function runAnalysis(repoPath, _branch, onProgress, options) {
|
|
93866
93955
|
let currentBranch = "unknown";
|
|
93867
|
-
let didStash = false;
|
|
93868
|
-
let isSubdirectory = false;
|
|
93869
|
-
let hasChanges = false;
|
|
93870
|
-
let git;
|
|
93871
93956
|
if (!options?.skipGit) {
|
|
93872
|
-
git = await getGit(repoPath);
|
|
93957
|
+
const git = await getGit(repoPath);
|
|
93873
93958
|
currentBranch = (await git.branch()).current;
|
|
93874
|
-
const statusResult = await git.status();
|
|
93875
|
-
hasChanges = !statusResult.isClean();
|
|
93876
|
-
const gitRoot = (await git.revparse(["--show-toplevel"])).trim();
|
|
93877
|
-
isSubdirectory = path11.resolve(repoPath) !== path11.resolve(gitRoot);
|
|
93878
93959
|
}
|
|
93879
|
-
|
|
93880
|
-
|
|
93960
|
+
onProgress({ step: "discover", percent: 10, detail: "Discovering files..." });
|
|
93961
|
+
const analyzer = await Promise.resolve().then(() => (init_dist6(), dist_exports2));
|
|
93962
|
+
await analyzer.initParsers();
|
|
93963
|
+
const files = await analyzer.discoverFiles(repoPath);
|
|
93964
|
+
onProgress({
|
|
93965
|
+
step: "discover",
|
|
93966
|
+
percent: 15,
|
|
93967
|
+
detail: `Found ${files.length} files`
|
|
93968
|
+
});
|
|
93969
|
+
onProgress({ step: "analyze", percent: 20, detail: "Analyzing files..." });
|
|
93970
|
+
const fileAnalyses = [];
|
|
93971
|
+
const totalFiles = files.length;
|
|
93972
|
+
for (let i = 0; i < totalFiles; i++) {
|
|
93973
|
+
if (options?.signal?.aborted)
|
|
93974
|
+
throw new DOMException("Analysis cancelled", "AbortError");
|
|
93975
|
+
const file = files[i];
|
|
93881
93976
|
try {
|
|
93882
|
-
const
|
|
93883
|
-
|
|
93977
|
+
const analysis = await analyzer.analyzeFile(file);
|
|
93978
|
+
if (analysis) {
|
|
93979
|
+
fileAnalyses.push(analysis);
|
|
93980
|
+
}
|
|
93884
93981
|
} catch (error) {
|
|
93885
|
-
log.warn(`[Analyzer] Failed to
|
|
93982
|
+
log.warn(`[Analyzer] Failed to analyze ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
93983
|
+
}
|
|
93984
|
+
if (i % 10 === 0 || i === totalFiles - 1) {
|
|
93985
|
+
const analyzePercent = 20 + Math.round((i + 1) / totalFiles * 40);
|
|
93986
|
+
onProgress({
|
|
93987
|
+
step: "analyze",
|
|
93988
|
+
percent: analyzePercent,
|
|
93989
|
+
detail: `Analyzed ${i + 1}/${totalFiles} files`
|
|
93990
|
+
});
|
|
93886
93991
|
}
|
|
93887
93992
|
}
|
|
93888
|
-
|
|
93889
|
-
|
|
93890
|
-
|
|
93891
|
-
|
|
93892
|
-
|
|
93893
|
-
|
|
93894
|
-
|
|
93895
|
-
|
|
93896
|
-
|
|
93897
|
-
|
|
93898
|
-
|
|
93899
|
-
|
|
93900
|
-
|
|
93901
|
-
|
|
93902
|
-
|
|
93903
|
-
|
|
93904
|
-
const file = files[i];
|
|
93905
|
-
try {
|
|
93906
|
-
const analysis = await analyzer.analyzeFile(file);
|
|
93907
|
-
if (analysis) {
|
|
93908
|
-
fileAnalyses.push(analysis);
|
|
93909
|
-
}
|
|
93910
|
-
} catch (error) {
|
|
93911
|
-
log.warn(`[Analyzer] Failed to analyze ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
93912
|
-
}
|
|
93913
|
-
if (i % 10 === 0 || i === totalFiles - 1) {
|
|
93914
|
-
const analyzePercent = 20 + Math.round((i + 1) / totalFiles * 40);
|
|
93915
|
-
onProgress({
|
|
93916
|
-
step: "analyze",
|
|
93917
|
-
percent: analyzePercent,
|
|
93918
|
-
detail: `Analyzed ${i + 1}/${totalFiles} files`
|
|
93919
|
-
});
|
|
93993
|
+
onProgress({
|
|
93994
|
+
step: "dependencies",
|
|
93995
|
+
percent: 65,
|
|
93996
|
+
detail: "Building dependency graph..."
|
|
93997
|
+
});
|
|
93998
|
+
const scopedOptions = buildScopedCompilerOptions(repoPath);
|
|
93999
|
+
if (scopedOptions.length > 0) {
|
|
94000
|
+
const filePaths = fileAnalyses.map((fa) => fa.filePath);
|
|
94001
|
+
const { exportMap } = analyzeSemantics(filePaths, scopedOptions);
|
|
94002
|
+
for (const fa of fileAnalyses) {
|
|
94003
|
+
const fileExports = exportMap.get(fa.filePath);
|
|
94004
|
+
if (!fileExports)
|
|
94005
|
+
continue;
|
|
94006
|
+
const defaultExportedFn = fa.exports.find((e) => e.isDefault)?.name;
|
|
94007
|
+
for (const fn of fa.functions) {
|
|
94008
|
+
fn.isExported = fileExports.has(fn.name) || fileExports.has("default") && fn.name === defaultExportedFn;
|
|
93920
94009
|
}
|
|
93921
94010
|
}
|
|
93922
|
-
|
|
93923
|
-
|
|
93924
|
-
|
|
93925
|
-
|
|
93926
|
-
|
|
93927
|
-
|
|
93928
|
-
|
|
93929
|
-
|
|
93930
|
-
|
|
93931
|
-
|
|
94011
|
+
}
|
|
94012
|
+
const filesByLanguage = /* @__PURE__ */ new Map();
|
|
94013
|
+
for (const fa of fileAnalyses) {
|
|
94014
|
+
const list = filesByLanguage.get(fa.language) || [];
|
|
94015
|
+
list.push(fa);
|
|
94016
|
+
filesByLanguage.set(fa.language, list);
|
|
94017
|
+
}
|
|
94018
|
+
for (const [language, files2] of filesByLanguage) {
|
|
94019
|
+
const serverConfig = getLspServerConfig(language);
|
|
94020
|
+
if (!serverConfig)
|
|
94021
|
+
continue;
|
|
94022
|
+
try {
|
|
94023
|
+
const lspClient = new LspClient(serverConfig);
|
|
94024
|
+
await lspClient.start(repoPath);
|
|
94025
|
+
const filePaths = files2.map((fa) => fa.filePath);
|
|
94026
|
+
const { exportMap } = await lspClient.analyzeSemantics(filePaths.map((fp) => fp.startsWith(repoPath) ? fp.slice(repoPath.length + 1) : fp));
|
|
94027
|
+
for (const fa of files2) {
|
|
93932
94028
|
const fileExports = exportMap.get(fa.filePath);
|
|
93933
94029
|
if (!fileExports)
|
|
93934
94030
|
continue;
|
|
93935
|
-
const defaultExportedFn = fa.exports.find((e) => e.isDefault)?.name;
|
|
93936
94031
|
for (const fn of fa.functions) {
|
|
93937
|
-
fn.isExported = fileExports.has(fn.name)
|
|
93938
|
-
}
|
|
93939
|
-
}
|
|
93940
|
-
}
|
|
93941
|
-
const filesByLanguage = /* @__PURE__ */ new Map();
|
|
93942
|
-
for (const fa of fileAnalyses) {
|
|
93943
|
-
const list = filesByLanguage.get(fa.language) || [];
|
|
93944
|
-
list.push(fa);
|
|
93945
|
-
filesByLanguage.set(fa.language, list);
|
|
93946
|
-
}
|
|
93947
|
-
for (const [language, files2] of filesByLanguage) {
|
|
93948
|
-
const serverConfig = getLspServerConfig(language);
|
|
93949
|
-
if (!serverConfig)
|
|
93950
|
-
continue;
|
|
93951
|
-
try {
|
|
93952
|
-
const lspClient = new LspClient(serverConfig);
|
|
93953
|
-
await lspClient.start(repoPath);
|
|
93954
|
-
const filePaths = files2.map((fa) => fa.filePath);
|
|
93955
|
-
const { exportMap } = await lspClient.analyzeSemantics(filePaths.map((fp) => fp.startsWith(repoPath) ? fp.slice(repoPath.length + 1) : fp));
|
|
93956
|
-
for (const fa of files2) {
|
|
93957
|
-
const fileExports = exportMap.get(fa.filePath);
|
|
93958
|
-
if (!fileExports)
|
|
93959
|
-
continue;
|
|
93960
|
-
for (const fn of fa.functions) {
|
|
93961
|
-
fn.isExported = fileExports.has(fn.name);
|
|
93962
|
-
}
|
|
94032
|
+
fn.isExported = fileExports.has(fn.name);
|
|
93963
94033
|
}
|
|
93964
|
-
await lspClient.stop();
|
|
93965
|
-
} catch (error) {
|
|
93966
|
-
log.warn(`[Analyzer] ${serverConfig.name} LSP analysis failed, using tree-sitter heuristics: ${error instanceof Error ? error.message : String(error)}`);
|
|
93967
|
-
}
|
|
93968
|
-
}
|
|
93969
|
-
const moduleDependencies = analyzer.buildDependencyGraph(fileAnalyses, repoPath);
|
|
93970
|
-
onProgress({
|
|
93971
|
-
step: "services",
|
|
93972
|
-
percent: 75,
|
|
93973
|
-
detail: "Detecting services..."
|
|
93974
|
-
});
|
|
93975
|
-
const splitResult = analyzer.performSplitAnalysis(repoPath, fileAnalyses, moduleDependencies);
|
|
93976
|
-
onProgress({
|
|
93977
|
-
step: "saving",
|
|
93978
|
-
percent: 80,
|
|
93979
|
-
detail: `Saving results: ${splitResult.services.length} services detected`
|
|
93980
|
-
});
|
|
93981
|
-
return {
|
|
93982
|
-
architecture: splitResult.architecture,
|
|
93983
|
-
services: splitResult.services,
|
|
93984
|
-
dependencies: splitResult.dependencies,
|
|
93985
|
-
layerDetails: splitResult.layerDetails,
|
|
93986
|
-
databaseResult: splitResult.databaseResult,
|
|
93987
|
-
modules: splitResult.modules,
|
|
93988
|
-
methods: splitResult.methods,
|
|
93989
|
-
moduleLevelDependencies: splitResult.moduleLevelDependencies,
|
|
93990
|
-
methodLevelDependencies: splitResult.methodLevelDependencies,
|
|
93991
|
-
fileAnalyses,
|
|
93992
|
-
moduleDependencies,
|
|
93993
|
-
entryPointFiles: new Set(findEntryPoints(fileAnalyses, moduleDependencies)),
|
|
93994
|
-
metadata: {
|
|
93995
|
-
totalFiles: files.length,
|
|
93996
|
-
analyzedFiles: fileAnalyses.length,
|
|
93997
|
-
branch: currentBranch || "HEAD",
|
|
93998
|
-
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
93999
|
-
}
|
|
94000
|
-
};
|
|
94001
|
-
} finally {
|
|
94002
|
-
if (didStash && git) {
|
|
94003
|
-
onProgress({ step: "unstash", percent: 80, detail: "Restoring pending changes..." });
|
|
94004
|
-
try {
|
|
94005
|
-
await git.stash(["pop"]);
|
|
94006
|
-
} catch (error) {
|
|
94007
|
-
log.error(`[Analyzer] Failed to restore stashed changes. Run "git stash pop" manually. ${error instanceof Error ? error.message : String(error)}`);
|
|
94008
94034
|
}
|
|
94035
|
+
await lspClient.stop();
|
|
94036
|
+
} catch (error) {
|
|
94037
|
+
log.warn(`[Analyzer] ${serverConfig.name} LSP analysis failed, using tree-sitter heuristics: ${error instanceof Error ? error.message : String(error)}`);
|
|
94009
94038
|
}
|
|
94010
94039
|
}
|
|
94040
|
+
const moduleDependencies = analyzer.buildDependencyGraph(fileAnalyses, repoPath);
|
|
94041
|
+
onProgress({
|
|
94042
|
+
step: "services",
|
|
94043
|
+
percent: 75,
|
|
94044
|
+
detail: "Detecting services..."
|
|
94045
|
+
});
|
|
94046
|
+
const splitResult = analyzer.performSplitAnalysis(repoPath, fileAnalyses, moduleDependencies);
|
|
94047
|
+
onProgress({
|
|
94048
|
+
step: "saving",
|
|
94049
|
+
percent: 80,
|
|
94050
|
+
detail: `Saving results: ${splitResult.services.length} services detected`
|
|
94051
|
+
});
|
|
94052
|
+
return {
|
|
94053
|
+
architecture: splitResult.architecture,
|
|
94054
|
+
services: splitResult.services,
|
|
94055
|
+
dependencies: splitResult.dependencies,
|
|
94056
|
+
layerDetails: splitResult.layerDetails,
|
|
94057
|
+
databaseResult: splitResult.databaseResult,
|
|
94058
|
+
modules: splitResult.modules,
|
|
94059
|
+
methods: splitResult.methods,
|
|
94060
|
+
moduleLevelDependencies: splitResult.moduleLevelDependencies,
|
|
94061
|
+
methodLevelDependencies: splitResult.methodLevelDependencies,
|
|
94062
|
+
fileAnalyses,
|
|
94063
|
+
moduleDependencies,
|
|
94064
|
+
entryPointFiles: new Set(findEntryPoints(fileAnalyses, moduleDependencies)),
|
|
94065
|
+
metadata: {
|
|
94066
|
+
totalFiles: files.length,
|
|
94067
|
+
analyzedFiles: fileAnalyses.length,
|
|
94068
|
+
branch: currentBranch || "HEAD",
|
|
94069
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
94070
|
+
}
|
|
94071
|
+
};
|
|
94011
94072
|
}
|
|
94012
94073
|
var init_analyzer_service = __esm({
|
|
94013
94074
|
"packages/core/dist/services/analyzer.service.js"() {
|
|
@@ -101346,14 +101407,14 @@ var require_main = __commonJS({
|
|
|
101346
101407
|
});
|
|
101347
101408
|
|
|
101348
101409
|
// packages/core/dist/config/env.js
|
|
101349
|
-
import
|
|
101410
|
+
import path11 from "node:path";
|
|
101350
101411
|
import os3 from "node:os";
|
|
101351
101412
|
var import_dotenv;
|
|
101352
101413
|
var init_env = __esm({
|
|
101353
101414
|
"packages/core/dist/config/env.js"() {
|
|
101354
101415
|
"use strict";
|
|
101355
101416
|
import_dotenv = __toESM(require_main(), 1);
|
|
101356
|
-
import_dotenv.default.config({ path:
|
|
101417
|
+
import_dotenv.default.config({ path: path11.join(os3.homedir(), ".truecourse", ".env") });
|
|
101357
101418
|
import_dotenv.default.config({ path: "../../.env" });
|
|
101358
101419
|
}
|
|
101359
101420
|
});
|
|
@@ -103546,7 +103607,7 @@ var init_violation_lifecycle_service = __esm({
|
|
|
103546
103607
|
// packages/core/dist/services/violation-pipeline.service.js
|
|
103547
103608
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
103548
103609
|
import fs9 from "node:fs";
|
|
103549
|
-
import
|
|
103610
|
+
import path12 from "node:path";
|
|
103550
103611
|
function throwIfAborted(signal) {
|
|
103551
103612
|
if (signal?.aborted)
|
|
103552
103613
|
throw new DOMException("Analysis cancelled", "AbortError");
|
|
@@ -103649,7 +103710,7 @@ async function runViolationPipeline(input) {
|
|
|
103649
103710
|
const enabledLlmCodeRules = enableLlmRules !== false ? allRules.filter((r) => (r.domain ? codeDomains.has(r.domain) : r.category === "code") && r.type === "llm" && r.prompt) : [];
|
|
103650
103711
|
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 }));
|
|
103651
103712
|
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 }));
|
|
103652
|
-
const filesToScan = changedFileSet ? [...changedFileSet].map((relPath) => ({ filePath: relPath, resolve: true })) : (result.fileAnalyses || []).map((fa) => ({ filePath: fa.filePath, resolve: !
|
|
103713
|
+
const filesToScan = changedFileSet ? [...changedFileSet].map((relPath) => ({ filePath: relPath, resolve: true })) : (result.fileAnalyses || []).map((fa) => ({ filePath: fa.filePath, resolve: !path12.isAbsolute(fa.filePath) }));
|
|
103653
103714
|
const hasLlm = enabledLlm.length > 0;
|
|
103654
103715
|
if (hasLlm)
|
|
103655
103716
|
tracker?.start("scan", "Reading files...");
|
|
@@ -103661,7 +103722,7 @@ async function runViolationPipeline(input) {
|
|
|
103661
103722
|
const lang = detectLanguage(filePath);
|
|
103662
103723
|
if (!lang)
|
|
103663
103724
|
continue;
|
|
103664
|
-
const absPath = resolve8 ?
|
|
103725
|
+
const absPath = resolve8 ? path12.resolve(repoPath, filePath) : path12.isAbsolute(filePath) ? filePath : path12.join(repoPath, filePath);
|
|
103665
103726
|
if (!fs9.existsSync(absPath))
|
|
103666
103727
|
continue;
|
|
103667
103728
|
const content = fs9.readFileSync(absPath, "utf-8");
|
|
@@ -103677,7 +103738,7 @@ async function runViolationPipeline(input) {
|
|
|
103677
103738
|
let typeQuery;
|
|
103678
103739
|
const enabledCodeKeys = new Set(enabledCodeRules.filter((r) => r.type === "deterministic" && r.enabled).map((r) => r.key));
|
|
103679
103740
|
if (hasTypeAwareVisitors(enabledCodeKeys)) {
|
|
103680
|
-
const tsFiles = filesToScan.filter(({ filePath: fp }) => /\.(ts|tsx|js|jsx)$/.test(fp)).map(({ filePath: fp, resolve: res }) => res ?
|
|
103741
|
+
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));
|
|
103681
103742
|
if (tsFiles.length > 0) {
|
|
103682
103743
|
const scoped = buildScopedCompilerOptions(repoPath);
|
|
103683
103744
|
typeQuery = createTypeQueryService(tsFiles, scoped);
|
|
@@ -103756,7 +103817,7 @@ async function runViolationPipeline(input) {
|
|
|
103756
103817
|
const lang = detectLanguage(filePath);
|
|
103757
103818
|
if (!lang)
|
|
103758
103819
|
continue;
|
|
103759
|
-
const absPath = resolve8 ?
|
|
103820
|
+
const absPath = resolve8 ? path12.resolve(repoPath, filePath) : path12.isAbsolute(filePath) ? filePath : path12.join(repoPath, filePath);
|
|
103760
103821
|
const key = changedFileSet ? absPath : filePath;
|
|
103761
103822
|
const fc = fileContents.get(key);
|
|
103762
103823
|
if (!fc)
|
|
@@ -103770,7 +103831,7 @@ async function runViolationPipeline(input) {
|
|
|
103770
103831
|
}
|
|
103771
103832
|
log.info(`[Pipeline] Code scan: ${allCodeViolations.length} violations from ${filesToScan.length} files (${enabledCodeRules.length} det rules, ${enabledLlmCodeRules.length} LLM rules)`);
|
|
103772
103833
|
if (enabledCodeRules.some((r) => r.key === "bugs/deterministic/invalid-pyproject-toml")) {
|
|
103773
|
-
const pyprojectPath =
|
|
103834
|
+
const pyprojectPath = path12.join(repoPath, "pyproject.toml");
|
|
103774
103835
|
if (fs9.existsSync(pyprojectPath)) {
|
|
103775
103836
|
try {
|
|
103776
103837
|
const { checkPyprojectToml: checkPyprojectToml2 } = await Promise.resolve().then(() => (init_dist6(), dist_exports2));
|
|
@@ -104593,7 +104654,7 @@ function processLlmCodeViolations(codeResult, validFilePaths, fileContents, allC
|
|
|
104593
104654
|
for (const v of codeResult.violations) {
|
|
104594
104655
|
let filePath = v.filePath;
|
|
104595
104656
|
if (!validFilePaths.has(filePath)) {
|
|
104596
|
-
const resolved =
|
|
104657
|
+
const resolved = path12.resolve(repoPath, filePath);
|
|
104597
104658
|
if (validFilePaths.has(resolved)) {
|
|
104598
104659
|
filePath = resolved;
|
|
104599
104660
|
} else {
|
|
@@ -104663,6 +104724,7 @@ var init_usage_service = __esm({
|
|
|
104663
104724
|
|
|
104664
104725
|
// packages/core/dist/commands/analyze-core.js
|
|
104665
104726
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
104727
|
+
import path13 from "node:path";
|
|
104666
104728
|
async function analyzeCore(project, options) {
|
|
104667
104729
|
try {
|
|
104668
104730
|
acquireAnalyzeLock(project.path);
|
|
@@ -104704,116 +104766,153 @@ async function analyzeCore(project, options) {
|
|
|
104704
104766
|
const start = Date.now();
|
|
104705
104767
|
const effectiveCategories = options.enabledCategoriesOverride?.length ? options.enabledCategoriesOverride : projectConfig.enabledCategories ?? void 0;
|
|
104706
104768
|
const effectiveLlmRules = projectConfig.enableLlmRules ?? options.enableLlmRulesOverride ?? true;
|
|
104707
|
-
|
|
104708
|
-
|
|
104709
|
-
|
|
104710
|
-
options.onProgress?.({ detail: progress.detail });
|
|
104711
|
-
}, { signal, skipStash: isDiff });
|
|
104712
|
-
if (signal?.aborted) {
|
|
104713
|
-
throw new DOMException(isDiff ? "Diff cancelled" : "Analysis cancelled", "AbortError");
|
|
104714
|
-
}
|
|
104715
|
-
const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
|
|
104716
|
-
let changedFiles = [];
|
|
104717
|
-
let changedFileSet;
|
|
104718
|
-
if (isDiff) {
|
|
104769
|
+
let didStash = false;
|
|
104770
|
+
let stashGit;
|
|
104771
|
+
if (!isDiff && !skipGit && !options.skipStash) {
|
|
104719
104772
|
try {
|
|
104720
|
-
|
|
104721
|
-
const
|
|
104722
|
-
|
|
104723
|
-
|
|
104724
|
-
|
|
104725
|
-
|
|
104726
|
-
|
|
104727
|
-
|
|
104728
|
-
|
|
104729
|
-
|
|
104773
|
+
stashGit = await getGit(project.path);
|
|
104774
|
+
const status = await stashGit.status();
|
|
104775
|
+
if (!status.isClean()) {
|
|
104776
|
+
const gitRoot = (await stashGit.revparse(["--show-toplevel"])).trim();
|
|
104777
|
+
const isSubdirectory = path13.resolve(project.path) !== path13.resolve(gitRoot);
|
|
104778
|
+
if (!isSubdirectory) {
|
|
104779
|
+
options.tracker?.detail("parse", "Stashing pending changes...");
|
|
104780
|
+
options.onProgress?.({ detail: "Stashing pending changes to analyze committed state..." });
|
|
104781
|
+
const stashResult = await stashGit.stash([
|
|
104782
|
+
"push",
|
|
104783
|
+
"--include-untracked",
|
|
104784
|
+
"-m",
|
|
104785
|
+
"truecourse-analysis-stash"
|
|
104786
|
+
]);
|
|
104787
|
+
didStash = !stashResult.includes("No local changes");
|
|
104788
|
+
}
|
|
104789
|
+
}
|
|
104790
|
+
} catch (error) {
|
|
104791
|
+
log.warn(`[Analyzer] Failed to stash changes, analyzing current state: ${error instanceof Error ? error.message : String(error)}`);
|
|
104792
|
+
}
|
|
104793
|
+
}
|
|
104794
|
+
try {
|
|
104795
|
+
options.tracker?.start("parse", isDiff ? "Analyzing working tree..." : "Starting analysis...");
|
|
104796
|
+
const result = await runAnalysis(project.path, branch ?? void 0, (progress) => {
|
|
104797
|
+
options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
|
|
104798
|
+
options.onProgress?.({ detail: progress.detail });
|
|
104799
|
+
}, { signal });
|
|
104800
|
+
if (signal?.aborted) {
|
|
104801
|
+
throw new DOMException(isDiff ? "Diff cancelled" : "Analysis cancelled", "AbortError");
|
|
104802
|
+
}
|
|
104803
|
+
const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
|
|
104804
|
+
let changedFiles = [];
|
|
104805
|
+
let changedFileSet;
|
|
104806
|
+
if (isDiff) {
|
|
104807
|
+
try {
|
|
104808
|
+
const git = await getGit(project.path);
|
|
104809
|
+
const statusResult = await git.status();
|
|
104810
|
+
for (const f of statusResult.not_added)
|
|
104811
|
+
changedFiles.push({ path: f, status: "new" });
|
|
104812
|
+
for (const f of statusResult.created)
|
|
104813
|
+
changedFiles.push({ path: f, status: "new" });
|
|
104814
|
+
for (const f of statusResult.modified)
|
|
104730
104815
|
changedFiles.push({ path: f, status: "modified" });
|
|
104816
|
+
for (const f of statusResult.staged) {
|
|
104817
|
+
if (!changedFiles.some((cf) => cf.path === f)) {
|
|
104818
|
+
changedFiles.push({ path: f, status: "modified" });
|
|
104819
|
+
}
|
|
104731
104820
|
}
|
|
104821
|
+
for (const f of statusResult.deleted)
|
|
104822
|
+
changedFiles.push({ path: f, status: "deleted" });
|
|
104823
|
+
} catch (err) {
|
|
104824
|
+
log.warn(`[Diff] git status failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
104825
|
+
}
|
|
104826
|
+
} else if (latestBaseline?.analysis.commitHash && !skipGit) {
|
|
104827
|
+
try {
|
|
104828
|
+
const git = await getGit(project.path);
|
|
104829
|
+
const diffOutput = await git.diff([latestBaseline.analysis.commitHash, "HEAD", "--name-only"]);
|
|
104830
|
+
const files = diffOutput.trim().split("\n").filter(Boolean);
|
|
104831
|
+
if (files.length > 0)
|
|
104832
|
+
changedFileSet = new Set(files);
|
|
104833
|
+
} catch {
|
|
104732
104834
|
}
|
|
104733
|
-
for (const f of statusResult.deleted)
|
|
104734
|
-
changedFiles.push({ path: f, status: "deleted" });
|
|
104735
|
-
} catch (err) {
|
|
104736
|
-
log.warn(`[Diff] git status failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
104737
104835
|
}
|
|
104738
|
-
|
|
104739
|
-
|
|
104740
|
-
|
|
104741
|
-
|
|
104742
|
-
|
|
104743
|
-
|
|
104744
|
-
|
|
104745
|
-
|
|
104836
|
+
if (!isDiff) {
|
|
104837
|
+
try {
|
|
104838
|
+
graph.flows = detectFlows(result);
|
|
104839
|
+
} catch (flowError) {
|
|
104840
|
+
log.error(`[Flows] Detection failed: ${flowError instanceof Error ? flowError.message : String(flowError)}`);
|
|
104841
|
+
graph.flows = [];
|
|
104842
|
+
}
|
|
104843
|
+
touchProject(project.slug);
|
|
104844
|
+
}
|
|
104845
|
+
options.tracker?.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
|
|
104846
|
+
const previousActiveViolations = latestBaseline ? latestBaseline.violations.filter((v) => branch == null || latestBaseline.analysis.branch == null || latestBaseline.analysis.branch === branch) : [];
|
|
104847
|
+
const previousAnalysisId = latestBaseline?.analysis.id ?? null;
|
|
104848
|
+
const provider = options.provider ?? (effectiveLlmRules ? createLLMProvider() : void 0);
|
|
104849
|
+
if (provider) {
|
|
104850
|
+
provider.setAnalysisId(analysisId);
|
|
104851
|
+
provider.setRepoPath(project.path);
|
|
104852
|
+
if (signal)
|
|
104853
|
+
provider.setAbortSignal(signal);
|
|
104854
|
+
}
|
|
104855
|
+
const pipelineResult = await runViolationPipeline({
|
|
104856
|
+
repoPath: project.path,
|
|
104857
|
+
analysisId,
|
|
104858
|
+
now,
|
|
104859
|
+
result,
|
|
104860
|
+
serviceIdMap,
|
|
104861
|
+
moduleIdMap,
|
|
104862
|
+
methodIdMap,
|
|
104863
|
+
dbIdMap,
|
|
104864
|
+
previousActiveViolations,
|
|
104865
|
+
changedFileSet,
|
|
104866
|
+
tracker: options.tracker,
|
|
104867
|
+
enabledCategories: effectiveCategories,
|
|
104868
|
+
enableLlmRules: effectiveLlmRules,
|
|
104869
|
+
provider,
|
|
104870
|
+
signal,
|
|
104871
|
+
onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
|
|
104872
|
+
const proceed = await options.onLlmEstimate(estimate);
|
|
104873
|
+
options.onLlmResolved?.(proceed);
|
|
104874
|
+
return proceed;
|
|
104875
|
+
} : void 0
|
|
104876
|
+
});
|
|
104877
|
+
if (pipelineResult.serviceDescriptions.length > 0) {
|
|
104878
|
+
for (const desc of pipelineResult.serviceDescriptions) {
|
|
104879
|
+
const svc = graph.services.find((s) => s.id === desc.id);
|
|
104880
|
+
if (svc)
|
|
104881
|
+
svc.description = desc.description;
|
|
104882
|
+
}
|
|
104883
|
+
}
|
|
104884
|
+
const usage = provider ? toUsageRecords(provider.flushUsage()) : [];
|
|
104885
|
+
enforceLocationInvariant(pipelineResult.added);
|
|
104886
|
+
enforceLocationInvariant(pipelineResult.unchanged);
|
|
104887
|
+
enforceLocationInvariant(pipelineResult.resolved);
|
|
104888
|
+
log.info(`[${isDiff ? "Diff" : "Analysis"}] core complete in ${Date.now() - start}ms \u2014 ${pipelineResult.added.length} added, ${pipelineResult.unchanged.length} unchanged, ${pipelineResult.resolvedRefs.length} resolved`);
|
|
104889
|
+
return {
|
|
104890
|
+
mode,
|
|
104891
|
+
analysisId,
|
|
104892
|
+
now,
|
|
104893
|
+
branch,
|
|
104894
|
+
commitHash,
|
|
104895
|
+
architecture: result.architecture,
|
|
104896
|
+
metadata: result.metadata ?? null,
|
|
104897
|
+
graph,
|
|
104898
|
+
changedFiles,
|
|
104899
|
+
pipelineResult,
|
|
104900
|
+
usage,
|
|
104901
|
+
latestBaseline,
|
|
104902
|
+
previousAnalysisId,
|
|
104903
|
+
analysisResult: result
|
|
104904
|
+
};
|
|
104905
|
+
} finally {
|
|
104906
|
+
if (didStash && stashGit) {
|
|
104907
|
+
options.tracker?.detail("parse", "Restoring pending changes...");
|
|
104908
|
+
options.onProgress?.({ detail: "Restoring pending changes..." });
|
|
104909
|
+
try {
|
|
104910
|
+
await stashGit.stash(["pop"]);
|
|
104911
|
+
} catch (error) {
|
|
104912
|
+
log.error(`[Analyzer] Failed to restore stashed changes. Run "git stash pop" manually. ${error instanceof Error ? error.message : String(error)}`);
|
|
104913
|
+
}
|
|
104746
104914
|
}
|
|
104747
104915
|
}
|
|
104748
|
-
if (!isDiff) {
|
|
104749
|
-
try {
|
|
104750
|
-
graph.flows = detectFlows(result);
|
|
104751
|
-
} catch (flowError) {
|
|
104752
|
-
log.error(`[Flows] Detection failed: ${flowError instanceof Error ? flowError.message : String(flowError)}`);
|
|
104753
|
-
graph.flows = [];
|
|
104754
|
-
}
|
|
104755
|
-
touchProject(project.slug);
|
|
104756
|
-
}
|
|
104757
|
-
options.tracker?.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
|
|
104758
|
-
const previousActiveViolations = latestBaseline ? latestBaseline.violations.filter((v) => branch == null || latestBaseline.analysis.branch == null || latestBaseline.analysis.branch === branch) : [];
|
|
104759
|
-
const previousAnalysisId = latestBaseline?.analysis.id ?? null;
|
|
104760
|
-
const provider = options.provider ?? (effectiveLlmRules ? createLLMProvider() : void 0);
|
|
104761
|
-
if (provider) {
|
|
104762
|
-
provider.setAnalysisId(analysisId);
|
|
104763
|
-
provider.setRepoPath(project.path);
|
|
104764
|
-
if (signal)
|
|
104765
|
-
provider.setAbortSignal(signal);
|
|
104766
|
-
}
|
|
104767
|
-
const pipelineResult = await runViolationPipeline({
|
|
104768
|
-
repoPath: project.path,
|
|
104769
|
-
analysisId,
|
|
104770
|
-
now,
|
|
104771
|
-
result,
|
|
104772
|
-
serviceIdMap,
|
|
104773
|
-
moduleIdMap,
|
|
104774
|
-
methodIdMap,
|
|
104775
|
-
dbIdMap,
|
|
104776
|
-
previousActiveViolations,
|
|
104777
|
-
changedFileSet,
|
|
104778
|
-
tracker: options.tracker,
|
|
104779
|
-
enabledCategories: effectiveCategories,
|
|
104780
|
-
enableLlmRules: effectiveLlmRules,
|
|
104781
|
-
provider,
|
|
104782
|
-
signal,
|
|
104783
|
-
onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
|
|
104784
|
-
const proceed = await options.onLlmEstimate(estimate);
|
|
104785
|
-
options.onLlmResolved?.(proceed);
|
|
104786
|
-
return proceed;
|
|
104787
|
-
} : void 0
|
|
104788
|
-
});
|
|
104789
|
-
if (pipelineResult.serviceDescriptions.length > 0) {
|
|
104790
|
-
for (const desc of pipelineResult.serviceDescriptions) {
|
|
104791
|
-
const svc = graph.services.find((s) => s.id === desc.id);
|
|
104792
|
-
if (svc)
|
|
104793
|
-
svc.description = desc.description;
|
|
104794
|
-
}
|
|
104795
|
-
}
|
|
104796
|
-
const usage = provider ? toUsageRecords(provider.flushUsage()) : [];
|
|
104797
|
-
enforceLocationInvariant(pipelineResult.added);
|
|
104798
|
-
enforceLocationInvariant(pipelineResult.unchanged);
|
|
104799
|
-
enforceLocationInvariant(pipelineResult.resolved);
|
|
104800
|
-
log.info(`[${isDiff ? "Diff" : "Analysis"}] core complete in ${Date.now() - start}ms \u2014 ${pipelineResult.added.length} added, ${pipelineResult.unchanged.length} unchanged, ${pipelineResult.resolvedRefs.length} resolved`);
|
|
104801
|
-
return {
|
|
104802
|
-
mode,
|
|
104803
|
-
analysisId,
|
|
104804
|
-
now,
|
|
104805
|
-
branch,
|
|
104806
|
-
commitHash,
|
|
104807
|
-
architecture: result.architecture,
|
|
104808
|
-
metadata: result.metadata ?? null,
|
|
104809
|
-
graph,
|
|
104810
|
-
changedFiles,
|
|
104811
|
-
pipelineResult,
|
|
104812
|
-
usage,
|
|
104813
|
-
latestBaseline,
|
|
104814
|
-
previousAnalysisId,
|
|
104815
|
-
analysisResult: result
|
|
104816
|
-
};
|
|
104817
104916
|
} finally {
|
|
104818
104917
|
releaseAnalyzeLock(project.path);
|
|
104819
104918
|
}
|
|
@@ -105134,6 +105233,7 @@ init_progress();
|
|
|
105134
105233
|
init_paths();
|
|
105135
105234
|
init_registry();
|
|
105136
105235
|
init_project_config();
|
|
105236
|
+
init_git();
|
|
105137
105237
|
init_logger();
|
|
105138
105238
|
init_helpers();
|
|
105139
105239
|
|
|
@@ -105311,6 +105411,50 @@ function resolveLlmDecision(options, configDefault) {
|
|
|
105311
105411
|
}
|
|
105312
105412
|
return { enabled: configDefault, autoApproveEstimate: false };
|
|
105313
105413
|
}
|
|
105414
|
+
async function resolveStashDecision(options, repoPath) {
|
|
105415
|
+
if (options.stash === true) return { skipStash: false };
|
|
105416
|
+
if (options.stash === false) return { skipStash: true };
|
|
105417
|
+
let modifiedCount = 0;
|
|
105418
|
+
let untrackedCount = 0;
|
|
105419
|
+
try {
|
|
105420
|
+
const git = await getGit(repoPath);
|
|
105421
|
+
const status = await git.status();
|
|
105422
|
+
if (status.isClean()) return { skipStash: false };
|
|
105423
|
+
modifiedCount = status.modified.length + status.staged.length + status.deleted.length + status.created.length;
|
|
105424
|
+
untrackedCount = status.not_added.length;
|
|
105425
|
+
} catch {
|
|
105426
|
+
return { skipStash: false };
|
|
105427
|
+
}
|
|
105428
|
+
if (!isInteractive()) {
|
|
105429
|
+
exitMissingNonInteractiveFlag(
|
|
105430
|
+
"analyze needs a decision on stashing before running non-interactively.",
|
|
105431
|
+
"Pass --stash to stash pending changes (analyze committed state) or --no-stash to analyze the working tree as-is."
|
|
105432
|
+
);
|
|
105433
|
+
}
|
|
105434
|
+
O2.warn(
|
|
105435
|
+
`Your repository has ${modifiedCount} modified and ${untrackedCount} untracked file(s).`
|
|
105436
|
+
);
|
|
105437
|
+
const choice = await _t({
|
|
105438
|
+
message: "How should TrueCourse handle them?",
|
|
105439
|
+
options: [
|
|
105440
|
+
{
|
|
105441
|
+
value: "stash",
|
|
105442
|
+
label: "Stash and analyze committed state (recommended)",
|
|
105443
|
+
hint: "changes are temporarily stashed and restored after the run"
|
|
105444
|
+
},
|
|
105445
|
+
{
|
|
105446
|
+
value: "no-stash",
|
|
105447
|
+
label: "Don't stash \u2014 analyze the working tree as-is",
|
|
105448
|
+
hint: "uncommitted changes are included in the analysis"
|
|
105449
|
+
}
|
|
105450
|
+
]
|
|
105451
|
+
});
|
|
105452
|
+
if (q(choice)) {
|
|
105453
|
+
pt("Cancelled \u2014 no changes made");
|
|
105454
|
+
process.exit(0);
|
|
105455
|
+
}
|
|
105456
|
+
return { skipStash: choice === "no-stash" };
|
|
105457
|
+
}
|
|
105314
105458
|
async function runAnalyze(options = {}) {
|
|
105315
105459
|
mt("Analyzing repository");
|
|
105316
105460
|
ensureClaudeCli();
|
|
@@ -105325,6 +105469,7 @@ async function runAnalyze(options = {}) {
|
|
|
105325
105469
|
const enabledCategories = config2.enabledCategories ?? void 0;
|
|
105326
105470
|
const llmDecision = resolveLlmDecision(options, config2.enableLlmRules ?? true);
|
|
105327
105471
|
const enableLlmRules = llmDecision.enabled;
|
|
105472
|
+
const stashDecision = await resolveStashDecision(options, project.path);
|
|
105328
105473
|
renderPhase = enableLlmRules ? "pre-llm" : "all";
|
|
105329
105474
|
if (wipeLegacyPostgresData()) {
|
|
105330
105475
|
O2.info("Legacy Postgres data wiped. Re-analyze to repopulate.");
|
|
@@ -105340,6 +105485,7 @@ async function runAnalyze(options = {}) {
|
|
|
105340
105485
|
const result = await analyzeInProcess(project, {
|
|
105341
105486
|
tracker,
|
|
105342
105487
|
signal: abortController.signal,
|
|
105488
|
+
skipStash: stashDecision.skipStash,
|
|
105343
105489
|
enabledCategoriesOverride: enabledCategories,
|
|
105344
105490
|
enableLlmRulesOverride: enableLlmRules,
|
|
105345
105491
|
onLlmEstimate: async (estimate) => {
|
|
@@ -109221,7 +109367,7 @@ async function runHooksRun() {
|
|
|
109221
109367
|
|
|
109222
109368
|
// tools/cli/src/index.ts
|
|
109223
109369
|
var program2 = new Command();
|
|
109224
|
-
program2.name("truecourse").version("0.5.
|
|
109370
|
+
program2.name("truecourse").version("0.5.7").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
|
|
109225
109371
|
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) => {
|
|
109226
109372
|
if (options.service && options.console) {
|
|
109227
109373
|
console.error("error: --service and --console are mutually exclusive");
|
|
@@ -109247,13 +109393,14 @@ function resolveInstallSkills(options) {
|
|
|
109247
109393
|
if (options.skills === false) return false;
|
|
109248
109394
|
return void 0;
|
|
109249
109395
|
}
|
|
109250
|
-
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) => {
|
|
109396
|
+
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("--stash", "Pre-approve stashing pending changes before analysis").option("--no-stash", "Analyze the working tree as-is without stashing").option("--install-skills", "Install Claude Code skills without prompting").option("--no-skills", "Skip the Claude Code skills prompt").action(async (options) => {
|
|
109251
109397
|
const llm = typeof options.llm === "boolean" ? options.llm : void 0;
|
|
109398
|
+
const stash = typeof options.stash === "boolean" ? options.stash : void 0;
|
|
109252
109399
|
const installSkills = resolveInstallSkills(options);
|
|
109253
109400
|
if (options.diff) {
|
|
109254
|
-
await runAnalyzeDiff({ llm, installSkills });
|
|
109401
|
+
await runAnalyzeDiff({ llm, stash, installSkills });
|
|
109255
109402
|
} else {
|
|
109256
|
-
await runAnalyze({ llm, installSkills });
|
|
109403
|
+
await runAnalyze({ llm, stash, installSkills });
|
|
109257
109404
|
}
|
|
109258
109405
|
});
|
|
109259
109406
|
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) => {
|