@ctxo/cli 0.9.3 → 0.11.0
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 +57 -0
- package/dist/{atomic-write-AQNYJFAG.js → atomic-write-BAFH464Q.js} +3 -2
- package/dist/chunk-5DSKZJNN.js +38 -0
- package/dist/chunk-5DSKZJNN.js.map +1 -0
- package/dist/{chunk-2TVHKCCU.js → chunk-CDGQGCVN.js} +55 -5
- package/dist/chunk-CDGQGCVN.js.map +1 -0
- package/dist/{chunk-HXEPS2XY.js → chunk-HZ4UGA6J.js} +142 -11
- package/dist/chunk-HZ4UGA6J.js.map +1 -0
- package/dist/{chunk-6EKERTJ7.js → chunk-RFEXMBTG.js} +4 -36
- package/dist/chunk-RFEXMBTG.js.map +1 -0
- package/dist/{cli-router-CQJ7CN6E.js → cli-router-NFAGLJ46.js} +802 -198
- package/dist/cli-router-NFAGLJ46.js.map +1 -0
- package/dist/index.js +8 -94
- package/dist/index.js.map +1 -1
- package/dist/json-index-reader-UYS74USB.js +9 -0
- package/llms-full.txt +48 -1
- package/llms.txt +8 -0
- package/package.json +6 -5
- package/skills/ctxo-review-pr/SKILL.md +10 -0
- package/skills/ctxo-safe-edit/SKILL.md +15 -0
- package/skills/ctxo-understand/SKILL.md +14 -0
- package/dist/chunk-2TVHKCCU.js.map +0 -1
- package/dist/chunk-6EKERTJ7.js.map +0 -1
- package/dist/chunk-HXEPS2XY.js.map +0 -1
- package/dist/cli-router-CQJ7CN6E.js.map +0 -1
- package/dist/json-index-reader-XOEE6VMD.js +0 -8
- /package/dist/{atomic-write-AQNYJFAG.js.map → atomic-write-BAFH464Q.js.map} +0 -0
- /package/dist/{json-index-reader-XOEE6VMD.js.map → json-index-reader-UYS74USB.js.map} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ArchitecturalOverlay,
|
|
4
|
+
BlastRadiusCalculator,
|
|
4
5
|
BoundaryViolationDetector,
|
|
5
6
|
CommunitySnapshotWriter,
|
|
6
7
|
DeadCodeDetector,
|
|
@@ -18,26 +19,28 @@ import {
|
|
|
18
19
|
loadConfig,
|
|
19
20
|
makeGlobMatcher,
|
|
20
21
|
watchSnapshotMinFileChanges
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-HZ4UGA6J.js";
|
|
22
23
|
import {
|
|
23
|
-
atomicWrite
|
|
24
|
-
|
|
25
|
-
} from "./chunk-6EKERTJ7.js";
|
|
24
|
+
atomicWrite
|
|
25
|
+
} from "./chunk-RFEXMBTG.js";
|
|
26
26
|
import {
|
|
27
27
|
JsonIndexReader
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-CDGQGCVN.js";
|
|
29
|
+
import {
|
|
30
|
+
createLogger
|
|
31
|
+
} from "./chunk-5DSKZJNN.js";
|
|
29
32
|
import {
|
|
30
33
|
StalenessDetector
|
|
31
34
|
} from "./chunk-PSKDDLSF.js";
|
|
32
35
|
|
|
33
36
|
// src/cli/cli-router.ts
|
|
34
|
-
import { readFileSync as
|
|
35
|
-
import { dirname as
|
|
37
|
+
import { readFileSync as readFileSync17, existsSync as existsSync27 } from "fs";
|
|
38
|
+
import { dirname as dirname9, join as join31 } from "path";
|
|
36
39
|
|
|
37
40
|
// src/cli/index-command.ts
|
|
38
41
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
39
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as
|
|
40
|
-
import { join as
|
|
42
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync10, statSync, readdirSync as readdirSync2 } from "fs";
|
|
43
|
+
import { join as join9, relative, extname as extname3 } from "path";
|
|
41
44
|
import { createRequire as createRequire3 } from "module";
|
|
42
45
|
|
|
43
46
|
// src/core/staleness/content-hasher.ts
|
|
@@ -263,9 +266,9 @@ async function loadPlugins(projectRoot, options = {}) {
|
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
// src/cli/install-command.ts
|
|
266
|
-
import { existsSync as
|
|
269
|
+
import { existsSync as existsSync6 } from "fs";
|
|
267
270
|
import { createRequire as createRequire2 } from "module";
|
|
268
|
-
import { join as
|
|
271
|
+
import { join as join5 } from "path";
|
|
269
272
|
|
|
270
273
|
// src/core/detection/detect-languages.ts
|
|
271
274
|
import { execFileSync } from "child_process";
|
|
@@ -574,7 +577,53 @@ function resolveBinary(command) {
|
|
|
574
577
|
return `${command}.cmd`;
|
|
575
578
|
}
|
|
576
579
|
|
|
580
|
+
// src/core/detection/detect-java-runtime.ts
|
|
581
|
+
import { spawnSync } from "child_process";
|
|
582
|
+
import { existsSync as existsSync5 } from "fs";
|
|
583
|
+
import { join as join4 } from "path";
|
|
584
|
+
function parseJavaMajor(output) {
|
|
585
|
+
const m = output.match(/version "([^"]+)"/);
|
|
586
|
+
if (!m) return void 0;
|
|
587
|
+
const parts = m[1].split(".");
|
|
588
|
+
const major = parts[0] === "1" && parts[1] ? parseInt(parts[1], 10) : parseInt(parts[0], 10);
|
|
589
|
+
return Number.isNaN(major) ? void 0 : major;
|
|
590
|
+
}
|
|
591
|
+
var _javaMajorCache = void 0;
|
|
592
|
+
function detectJavaMajor() {
|
|
593
|
+
if (_javaMajorCache !== void 0) {
|
|
594
|
+
return _javaMajorCache === null ? void 0 : _javaMajorCache;
|
|
595
|
+
}
|
|
596
|
+
const home = process.env["CTXO_JAVA_HOME"] || process.env["JAVA_HOME"];
|
|
597
|
+
const bin = home ? join4(home, "bin", process.platform === "win32" ? "java.exe" : "java") : "java";
|
|
598
|
+
if (home && !existsSync5(bin)) {
|
|
599
|
+
_javaMajorCache = null;
|
|
600
|
+
return void 0;
|
|
601
|
+
}
|
|
602
|
+
const r = spawnSync(bin, ["-version"], {
|
|
603
|
+
encoding: "utf-8",
|
|
604
|
+
timeout: 1e4,
|
|
605
|
+
windowsHide: true
|
|
606
|
+
});
|
|
607
|
+
if (r.error || r.status === null) {
|
|
608
|
+
_javaMajorCache = null;
|
|
609
|
+
return void 0;
|
|
610
|
+
}
|
|
611
|
+
const major = parseJavaMajor(`${r.stderr ?? ""}${r.stdout ?? ""}`);
|
|
612
|
+
_javaMajorCache = major ?? null;
|
|
613
|
+
return major;
|
|
614
|
+
}
|
|
615
|
+
function javaRuntimeAvailable() {
|
|
616
|
+
return (detectJavaMajor() ?? 0) >= 17;
|
|
617
|
+
}
|
|
618
|
+
|
|
577
619
|
// src/cli/install-command.ts
|
|
620
|
+
function resolveJavaPackages(opts) {
|
|
621
|
+
if (opts.fullTier && opts.syntaxOnly) throw new Error("Cannot combine --full-tier with --syntax-only.");
|
|
622
|
+
const base = ["@ctxo/lang-java"];
|
|
623
|
+
if (opts.syntaxOnly) return base;
|
|
624
|
+
if (opts.fullTier || opts.jreAvailable) return [...base, "@ctxo/lang-java-analyzer"];
|
|
625
|
+
return base;
|
|
626
|
+
}
|
|
578
627
|
var InstallCommand = class {
|
|
579
628
|
constructor(projectRoot) {
|
|
580
629
|
this.projectRoot = projectRoot;
|
|
@@ -598,7 +647,7 @@ var InstallCommand = class {
|
|
|
598
647
|
process.exitCode = 1;
|
|
599
648
|
return;
|
|
600
649
|
}
|
|
601
|
-
if (!options.global && !
|
|
650
|
+
if (!options.global && !existsSync6(join5(this.projectRoot, "package.json"))) {
|
|
602
651
|
console.error("[ctxo] No package.json in the current project. Re-run with --global or create a package.json first.");
|
|
603
652
|
process.exitCode = 1;
|
|
604
653
|
return;
|
|
@@ -608,9 +657,28 @@ var InstallCommand = class {
|
|
|
608
657
|
process.exitCode = 1;
|
|
609
658
|
return;
|
|
610
659
|
}
|
|
611
|
-
const specifiers =
|
|
612
|
-
|
|
613
|
-
|
|
660
|
+
const specifiers = [];
|
|
661
|
+
for (const lang of resolvedLangs) {
|
|
662
|
+
if (lang === "java") {
|
|
663
|
+
const jreAvailable = javaRuntimeAvailable();
|
|
664
|
+
if (!jreAvailable && !options.fullTier && !options.syntaxOnly) {
|
|
665
|
+
console.error('[ctxo] JRE 17+ not found; installing Java syntax tier. Install a JRE then run "ctxo install java --full-tier" for resolved call/use edges.');
|
|
666
|
+
}
|
|
667
|
+
let javaPkgs;
|
|
668
|
+
try {
|
|
669
|
+
javaPkgs = resolveJavaPackages({ jreAvailable, fullTier: options.fullTier, syntaxOnly: options.syntaxOnly });
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.error(`[ctxo] ${err.message}`);
|
|
672
|
+
process.exitCode = 1;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
for (const pkg of javaPkgs) {
|
|
676
|
+
specifiers.push(options.version ? `${pkg}@${options.version}` : pkg);
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
specifiers.push(options.version ? `${officialPluginFor(lang)}@${options.version}` : officialPluginFor(lang));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
614
682
|
const invocation = buildInstallCommand(resolution.manager, specifiers, {
|
|
615
683
|
global: options.global,
|
|
616
684
|
workspaceRoot: !options.global && isWorkspaceRoot(this.projectRoot)
|
|
@@ -702,12 +770,12 @@ function isLockedCI(env) {
|
|
|
702
770
|
}
|
|
703
771
|
|
|
704
772
|
// src/adapters/storage/json-index-writer.ts
|
|
705
|
-
import { mkdirSync, unlinkSync, existsSync as
|
|
706
|
-
import { dirname as dirname4, join as
|
|
773
|
+
import { mkdirSync, unlinkSync, existsSync as existsSync7 } from "fs";
|
|
774
|
+
import { dirname as dirname4, join as join6, resolve as resolve2, sep } from "path";
|
|
707
775
|
var JsonIndexWriter = class {
|
|
708
776
|
indexDir;
|
|
709
777
|
constructor(ctxoRoot) {
|
|
710
|
-
this.indexDir =
|
|
778
|
+
this.indexDir = join6(ctxoRoot, "index");
|
|
711
779
|
}
|
|
712
780
|
write(fileIndex) {
|
|
713
781
|
if (!fileIndex.file) {
|
|
@@ -721,7 +789,7 @@ var JsonIndexWriter = class {
|
|
|
721
789
|
}
|
|
722
790
|
writeCoChanges(matrix) {
|
|
723
791
|
mkdirSync(this.indexDir, { recursive: true });
|
|
724
|
-
const targetPath =
|
|
792
|
+
const targetPath = join6(this.indexDir, "co-changes.json");
|
|
725
793
|
this.atomicWrite(targetPath, JSON.stringify(matrix, null, 2));
|
|
726
794
|
}
|
|
727
795
|
atomicWrite(targetPath, content) {
|
|
@@ -729,7 +797,7 @@ var JsonIndexWriter = class {
|
|
|
729
797
|
}
|
|
730
798
|
delete(relativePath) {
|
|
731
799
|
const targetPath = this.resolveIndexPath(relativePath);
|
|
732
|
-
if (
|
|
800
|
+
if (existsSync7(targetPath)) {
|
|
733
801
|
unlinkSync(targetPath);
|
|
734
802
|
}
|
|
735
803
|
}
|
|
@@ -757,19 +825,19 @@ var JsonIndexWriter = class {
|
|
|
757
825
|
};
|
|
758
826
|
|
|
759
827
|
// src/adapters/storage/schema-manager.ts
|
|
760
|
-
import { readFileSync as readFileSync4, writeFileSync, existsSync as
|
|
761
|
-
import { join as
|
|
828
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2 } from "fs";
|
|
829
|
+
import { join as join7, dirname as dirname5 } from "path";
|
|
762
830
|
var CURRENT_SCHEMA_VERSION = "1.0.0";
|
|
763
831
|
var SchemaManager = class {
|
|
764
832
|
versionFilePath;
|
|
765
833
|
constructor(ctxoRoot) {
|
|
766
|
-
this.versionFilePath =
|
|
834
|
+
this.versionFilePath = join7(ctxoRoot, "index", "schema-version");
|
|
767
835
|
}
|
|
768
836
|
currentVersion() {
|
|
769
837
|
return CURRENT_SCHEMA_VERSION;
|
|
770
838
|
}
|
|
771
839
|
readStoredVersion() {
|
|
772
|
-
if (!
|
|
840
|
+
if (!existsSync8(this.versionFilePath)) {
|
|
773
841
|
return void 0;
|
|
774
842
|
}
|
|
775
843
|
return readFileSync4(this.versionFilePath, "utf-8").trim();
|
|
@@ -786,8 +854,8 @@ var SchemaManager = class {
|
|
|
786
854
|
};
|
|
787
855
|
|
|
788
856
|
// src/adapters/storage/architecture-artifacts-writer.ts
|
|
789
|
-
import { existsSync as
|
|
790
|
-
import { join as
|
|
857
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
858
|
+
import { join as join8, resolve as resolve3 } from "path";
|
|
791
859
|
import { tmpdir } from "os";
|
|
792
860
|
var log3 = createLogger("ctxo:architecture-artifacts");
|
|
793
861
|
var DRIFT_FILE_NAME = "drift-events.json";
|
|
@@ -795,20 +863,20 @@ var VIOLATIONS_FILE_NAME = "boundary-violations.json";
|
|
|
795
863
|
var ArchitectureArtifactsWriter = class {
|
|
796
864
|
indexDir;
|
|
797
865
|
constructor(ctxoRoot, options = {}) {
|
|
798
|
-
this.indexDir =
|
|
866
|
+
this.indexDir = join8(ctxoRoot, "index");
|
|
799
867
|
assertSafeWritePath(ctxoRoot, options.allowProductionPath ?? false);
|
|
800
868
|
}
|
|
801
869
|
writeDriftEvents(artifact) {
|
|
802
870
|
mkdirSync3(this.indexDir, { recursive: true });
|
|
803
|
-
atomicWrite(
|
|
871
|
+
atomicWrite(join8(this.indexDir, DRIFT_FILE_NAME), JSON.stringify(artifact, null, 2));
|
|
804
872
|
}
|
|
805
873
|
writeBoundaryViolations(artifact) {
|
|
806
874
|
mkdirSync3(this.indexDir, { recursive: true });
|
|
807
|
-
atomicWrite(
|
|
875
|
+
atomicWrite(join8(this.indexDir, VIOLATIONS_FILE_NAME), JSON.stringify(artifact, null, 2));
|
|
808
876
|
}
|
|
809
877
|
readDriftEvents() {
|
|
810
|
-
const path =
|
|
811
|
-
if (!
|
|
878
|
+
const path = join8(this.indexDir, DRIFT_FILE_NAME);
|
|
879
|
+
if (!existsSync9(path)) return void 0;
|
|
812
880
|
try {
|
|
813
881
|
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
814
882
|
} catch (err) {
|
|
@@ -817,8 +885,8 @@ var ArchitectureArtifactsWriter = class {
|
|
|
817
885
|
}
|
|
818
886
|
}
|
|
819
887
|
readBoundaryViolations() {
|
|
820
|
-
const path =
|
|
821
|
-
if (!
|
|
888
|
+
const path = join8(this.indexDir, VIOLATIONS_FILE_NAME);
|
|
889
|
+
if (!existsSync9(path)) return void 0;
|
|
822
890
|
try {
|
|
823
891
|
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
824
892
|
} catch (err) {
|
|
@@ -1127,7 +1195,7 @@ var IndexCommand = class {
|
|
|
1127
1195
|
supportedExtensions;
|
|
1128
1196
|
constructor(projectRoot, ctxoRoot) {
|
|
1129
1197
|
this.projectRoot = projectRoot;
|
|
1130
|
-
this.ctxoRoot = ctxoRoot ??
|
|
1198
|
+
this.ctxoRoot = ctxoRoot ?? join9(projectRoot, ".ctxo");
|
|
1131
1199
|
this.supportedExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".go", ".cs"]);
|
|
1132
1200
|
}
|
|
1133
1201
|
loadIgnoreConfig() {
|
|
@@ -1155,7 +1223,7 @@ var IndexCommand = class {
|
|
|
1155
1223
|
}
|
|
1156
1224
|
const ignore = this.loadIgnoreConfig();
|
|
1157
1225
|
const registry = new LanguageAdapterRegistry();
|
|
1158
|
-
const { tsMorphLike, roslynAdapter, csharpTier, goTier } = await this.registerDiscoveredPlugins(registry, ignore.ignoreProjectPatterns);
|
|
1226
|
+
const { tsMorphLike, roslynAdapter, csharpTier, goTier, javaTier } = await this.registerDiscoveredPlugins(registry, ignore.ignoreProjectPatterns);
|
|
1159
1227
|
this.supportedExtensions = registry.getSupportedExtensions();
|
|
1160
1228
|
this.printLanguagePreview();
|
|
1161
1229
|
const writer = new JsonIndexWriter(this.ctxoRoot);
|
|
@@ -1165,7 +1233,7 @@ var IndexCommand = class {
|
|
|
1165
1233
|
const revertDetector = new RevertDetector();
|
|
1166
1234
|
let files;
|
|
1167
1235
|
if (options.file) {
|
|
1168
|
-
const fullPath =
|
|
1236
|
+
const fullPath = join9(this.projectRoot, options.file);
|
|
1169
1237
|
files = [fullPath];
|
|
1170
1238
|
console.error(`[ctxo] Incremental re-index: ${options.file}`);
|
|
1171
1239
|
} else {
|
|
@@ -1178,8 +1246,8 @@ var IndexCommand = class {
|
|
|
1178
1246
|
console.error(`[ctxo] Building codebase index... Found ${files.length} source files`);
|
|
1179
1247
|
}
|
|
1180
1248
|
try {
|
|
1181
|
-
const { sweepStaleTmpFiles } = await import("./atomic-write-
|
|
1182
|
-
const swept = sweepStaleTmpFiles(
|
|
1249
|
+
const { sweepStaleTmpFiles } = await import("./atomic-write-BAFH464Q.js");
|
|
1250
|
+
const swept = sweepStaleTmpFiles(join9(this.ctxoRoot, "index"));
|
|
1183
1251
|
if (swept > 0) {
|
|
1184
1252
|
console.error(`[ctxo] Cleaned ${swept} orphan .tmp file(s) from previous interrupted run`);
|
|
1185
1253
|
}
|
|
@@ -1266,7 +1334,7 @@ var IndexCommand = class {
|
|
|
1266
1334
|
}
|
|
1267
1335
|
let communitySnapshot;
|
|
1268
1336
|
if (options.skipCommunity && pendingIndices.length > 0) {
|
|
1269
|
-
if (
|
|
1337
|
+
if (existsSync10(join9(this.ctxoRoot, "index", "communities.json"))) {
|
|
1270
1338
|
console.error(
|
|
1271
1339
|
`[ctxo] WARN stale communities.json present \u2014 --skip-community preserved existing snapshot. Run \`ctxo index\` (full) to refresh.`
|
|
1272
1340
|
);
|
|
@@ -1279,6 +1347,7 @@ var IndexCommand = class {
|
|
|
1279
1347
|
{
|
|
1280
1348
|
csharpTier,
|
|
1281
1349
|
goTier,
|
|
1350
|
+
javaTier,
|
|
1282
1351
|
tsCount: graphIndices.filter((idx) => /\.(ts|tsx|js|jsx)$/.test(idx.file)).length
|
|
1283
1352
|
},
|
|
1284
1353
|
gitAdapter
|
|
@@ -1316,10 +1385,12 @@ var IndexCommand = class {
|
|
|
1316
1385
|
const csCount = pendingIndices.filter((e) => e.relativePath.endsWith(".cs")).length;
|
|
1317
1386
|
const tsCount = pendingIndices.filter((e) => /\.(ts|tsx|js|jsx)$/.test(e.relativePath)).length;
|
|
1318
1387
|
const goCount = pendingIndices.filter((e) => e.relativePath.endsWith(".go")).length;
|
|
1388
|
+
const javaCount = pendingIndices.filter((e) => e.relativePath.endsWith(".java")).length;
|
|
1319
1389
|
console.error(`[ctxo] Index complete: ${processed} files indexed`);
|
|
1320
1390
|
if (tsCount > 0) console.error(`[ctxo] TypeScript/JS: ${tsCount} files (full tier)`);
|
|
1321
1391
|
if (csCount > 0) console.error(`[ctxo] C#: ${csCount} files (${csharpTier} tier${csharpTier === "syntax" ? " - .NET SDK 8+ for full analysis" : ""})`);
|
|
1322
1392
|
if (goCount > 0) console.error(`[ctxo] Go: ${goCount} files (${goTier} tier${goTier === "syntax" ? " - Go 1.22+ for full analysis" : ""})`);
|
|
1393
|
+
if (javaCount > 0) console.error(`[ctxo] Java: ${javaCount} files (${javaTier} tier${javaTier === "syntax" ? ' - JRE 17+ & "ctxo install java --full-tier" for full analysis' : ""})`);
|
|
1323
1394
|
if (roslynAdapter) await roslynAdapter.dispose();
|
|
1324
1395
|
}
|
|
1325
1396
|
async autoInstallMissingPlugins() {
|
|
@@ -1355,6 +1426,7 @@ var IndexCommand = class {
|
|
|
1355
1426
|
let roslynAdapter = null;
|
|
1356
1427
|
let csharpTier = "unavailable";
|
|
1357
1428
|
let goTier = "unavailable";
|
|
1429
|
+
let javaTier = "unavailable";
|
|
1358
1430
|
for (const { plugin, adapter } of loaded) {
|
|
1359
1431
|
if (typeof adapter.initialize === "function") {
|
|
1360
1432
|
try {
|
|
@@ -1378,11 +1450,15 @@ var IndexCommand = class {
|
|
|
1378
1450
|
if (plugin.id === "go" && typeof goCandidate.getTier === "function") {
|
|
1379
1451
|
goTier = goCandidate.getTier();
|
|
1380
1452
|
}
|
|
1381
|
-
const
|
|
1453
|
+
const javaCandidate = adapter;
|
|
1454
|
+
if (plugin.id === "java" && typeof javaCandidate.getTier === "function") {
|
|
1455
|
+
javaTier = javaCandidate.getTier();
|
|
1456
|
+
}
|
|
1457
|
+
const activeTier = plugin.id === "csharp" ? csharpTier : plugin.id === "go" ? goTier : plugin.id === "java" ? javaTier : plugin.tier;
|
|
1382
1458
|
const tierSuffix = activeTier !== plugin.tier ? ` (active: ${activeTier})` : "";
|
|
1383
1459
|
console.error(`[ctxo] Plugin ${plugin.id}@${plugin.version} (${plugin.tier} tier${tierSuffix}) \u2014 ${plugin.extensions.join(", ")}`);
|
|
1384
1460
|
}
|
|
1385
|
-
return { tsMorphLike, roslynAdapter, csharpTier, goTier };
|
|
1461
|
+
return { tsMorphLike, roslynAdapter, csharpTier, goTier, javaTier };
|
|
1386
1462
|
}
|
|
1387
1463
|
discoverFilesIn(root, ignoreFile = () => false) {
|
|
1388
1464
|
try {
|
|
@@ -1391,18 +1467,18 @@ var IndexCommand = class {
|
|
|
1391
1467
|
encoding: "utf-8",
|
|
1392
1468
|
maxBuffer: 10 * 1024 * 1024
|
|
1393
1469
|
});
|
|
1394
|
-
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) =>
|
|
1470
|
+
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) => join9(root, line)).filter((abs) => {
|
|
1395
1471
|
const rel = relative(this.projectRoot, abs).replace(/\\/g, "/");
|
|
1396
1472
|
return !ignoreFile(rel);
|
|
1397
|
-
}).filter((abs) =>
|
|
1473
|
+
}).filter((abs) => existsSync10(abs));
|
|
1398
1474
|
} catch {
|
|
1399
1475
|
console.error(`[ctxo] git ls-files failed for ${root}`);
|
|
1400
1476
|
return [];
|
|
1401
1477
|
}
|
|
1402
1478
|
}
|
|
1403
1479
|
discoverWorkspaces(ignoreProject = () => false) {
|
|
1404
|
-
const pkgPath =
|
|
1405
|
-
if (!
|
|
1480
|
+
const pkgPath = join9(this.projectRoot, "package.json");
|
|
1481
|
+
if (!existsSync10(pkgPath)) return [this.projectRoot];
|
|
1406
1482
|
try {
|
|
1407
1483
|
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
1408
1484
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
@@ -1410,17 +1486,17 @@ var IndexCommand = class {
|
|
|
1410
1486
|
const resolved = [];
|
|
1411
1487
|
for (const ws of workspaces) {
|
|
1412
1488
|
if (ws.endsWith("/*") || ws.endsWith("\\*")) {
|
|
1413
|
-
const parentDir =
|
|
1414
|
-
if (
|
|
1489
|
+
const parentDir = join9(this.projectRoot, ws.slice(0, -2));
|
|
1490
|
+
if (existsSync10(parentDir)) {
|
|
1415
1491
|
for (const entry of readdirSync2(parentDir, { withFileTypes: true })) {
|
|
1416
1492
|
if (entry.isDirectory()) {
|
|
1417
|
-
resolved.push(
|
|
1493
|
+
resolved.push(join9(parentDir, entry.name));
|
|
1418
1494
|
}
|
|
1419
1495
|
}
|
|
1420
1496
|
}
|
|
1421
1497
|
} else {
|
|
1422
|
-
const wsPath =
|
|
1423
|
-
if (
|
|
1498
|
+
const wsPath = join9(this.projectRoot, ws);
|
|
1499
|
+
if (existsSync10(wsPath)) {
|
|
1424
1500
|
resolved.push(wsPath);
|
|
1425
1501
|
}
|
|
1426
1502
|
}
|
|
@@ -1453,7 +1529,7 @@ var IndexCommand = class {
|
|
|
1453
1529
|
if (_roslyn) await _roslyn.dispose();
|
|
1454
1530
|
const hasher = new ContentHasher();
|
|
1455
1531
|
const files = this.discoverFilesIn(this.projectRoot);
|
|
1456
|
-
const reader = new (await import("./json-index-reader-
|
|
1532
|
+
const reader = new (await import("./json-index-reader-UYS74USB.js")).JsonIndexReader(this.ctxoRoot);
|
|
1457
1533
|
const indices = reader.readAll();
|
|
1458
1534
|
const indexedMap = new Map(indices.map((i) => [i.file, i]));
|
|
1459
1535
|
let staleCount = 0;
|
|
@@ -1466,7 +1542,7 @@ var IndexCommand = class {
|
|
|
1466
1542
|
staleCount++;
|
|
1467
1543
|
continue;
|
|
1468
1544
|
}
|
|
1469
|
-
if (!
|
|
1545
|
+
if (!existsSync10(filePath)) {
|
|
1470
1546
|
console.error(`[ctxo] DELETED: ${relativePath}`);
|
|
1471
1547
|
staleCount++;
|
|
1472
1548
|
continue;
|
|
@@ -1492,13 +1568,13 @@ var IndexCommand = class {
|
|
|
1492
1568
|
console.error("[ctxo] Index is up to date");
|
|
1493
1569
|
}
|
|
1494
1570
|
ensureGitignore() {
|
|
1495
|
-
const gitignorePath =
|
|
1571
|
+
const gitignorePath = join9(this.projectRoot, ".gitignore");
|
|
1496
1572
|
const cachePattern = ".ctxo/.cache/";
|
|
1497
1573
|
const suffix = `
|
|
1498
1574
|
# Ctxo local cache (never committed)
|
|
1499
1575
|
${cachePattern}
|
|
1500
1576
|
`;
|
|
1501
|
-
const existing =
|
|
1577
|
+
const existing = existsSync10(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
|
|
1502
1578
|
if (existing.includes(cachePattern)) return;
|
|
1503
1579
|
writeFileSync2(gitignorePath, existing + suffix, "utf-8");
|
|
1504
1580
|
console.error("[ctxo] Added .ctxo/.cache/ to .gitignore");
|
|
@@ -1617,8 +1693,8 @@ ${cachePattern}
|
|
|
1617
1693
|
}
|
|
1618
1694
|
};
|
|
1619
1695
|
function pickEdgeQuality(tiers) {
|
|
1620
|
-
const hasSyntaxOnly = tiers.csharpTier === "syntax" || tiers.goTier === "syntax";
|
|
1621
|
-
const hasFull = tiers.tsCount > 0 || tiers.csharpTier === "full" || tiers.goTier === "full";
|
|
1696
|
+
const hasSyntaxOnly = tiers.csharpTier === "syntax" || tiers.goTier === "syntax" || tiers.javaTier === "syntax";
|
|
1697
|
+
const hasFull = tiers.tsCount > 0 || tiers.csharpTier === "full" || tiers.goTier === "full" || tiers.javaTier === "full";
|
|
1622
1698
|
if (hasSyntaxOnly && hasFull) return "mixed";
|
|
1623
1699
|
if (hasSyntaxOnly) return "syntax-only";
|
|
1624
1700
|
return "full";
|
|
@@ -1643,11 +1719,11 @@ function countCommunities(snapshot) {
|
|
|
1643
1719
|
}
|
|
1644
1720
|
|
|
1645
1721
|
// src/cli/sync-command.ts
|
|
1646
|
-
import { join as
|
|
1722
|
+
import { join as join10 } from "path";
|
|
1647
1723
|
var SyncCommand = class {
|
|
1648
1724
|
ctxoRoot;
|
|
1649
1725
|
constructor(projectRoot) {
|
|
1650
|
-
this.ctxoRoot =
|
|
1726
|
+
this.ctxoRoot = join10(projectRoot, ".ctxo");
|
|
1651
1727
|
}
|
|
1652
1728
|
async run() {
|
|
1653
1729
|
console.error("[ctxo] Rebuilding SQLite cache from committed JSON index...");
|
|
@@ -1662,19 +1738,19 @@ var SyncCommand = class {
|
|
|
1662
1738
|
};
|
|
1663
1739
|
|
|
1664
1740
|
// src/cli/status-command.ts
|
|
1665
|
-
import { join as
|
|
1666
|
-
import { existsSync as
|
|
1741
|
+
import { join as join11 } from "path";
|
|
1742
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1667
1743
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1668
1744
|
var StatusCommand = class {
|
|
1669
1745
|
projectRoot;
|
|
1670
1746
|
ctxoRoot;
|
|
1671
1747
|
constructor(projectRoot) {
|
|
1672
1748
|
this.projectRoot = projectRoot;
|
|
1673
|
-
this.ctxoRoot =
|
|
1749
|
+
this.ctxoRoot = join11(projectRoot, ".ctxo");
|
|
1674
1750
|
}
|
|
1675
1751
|
run() {
|
|
1676
|
-
const indexDir =
|
|
1677
|
-
if (!
|
|
1752
|
+
const indexDir = join11(this.ctxoRoot, "index");
|
|
1753
|
+
if (!existsSync11(indexDir)) {
|
|
1678
1754
|
console.error('[ctxo] No index found. Run "ctxo index" first.');
|
|
1679
1755
|
return;
|
|
1680
1756
|
}
|
|
@@ -1689,7 +1765,7 @@ var StatusCommand = class {
|
|
|
1689
1765
|
console.error(` Indexed files: ${indices.length}`);
|
|
1690
1766
|
console.error(` Total symbols: ${totalSymbols}`);
|
|
1691
1767
|
console.error(` Total edges: ${totalEdges}`);
|
|
1692
|
-
const cacheExists =
|
|
1768
|
+
const cacheExists = existsSync11(join11(this.ctxoRoot, ".cache", "symbols.db"));
|
|
1693
1769
|
console.error(` SQLite cache: ${cacheExists ? "present" : "missing (run ctxo sync)"}`);
|
|
1694
1770
|
if (indices.length > 0) {
|
|
1695
1771
|
console.error("");
|
|
@@ -1718,7 +1794,7 @@ var StatusCommand = class {
|
|
|
1718
1794
|
|
|
1719
1795
|
// src/cli/verify-command.ts
|
|
1720
1796
|
import { mkdtempSync, rmSync } from "fs";
|
|
1721
|
-
import { join as
|
|
1797
|
+
import { join as join12 } from "path";
|
|
1722
1798
|
import { tmpdir as tmpdir2 } from "os";
|
|
1723
1799
|
var VerifyCommand = class {
|
|
1724
1800
|
projectRoot;
|
|
@@ -1727,12 +1803,12 @@ var VerifyCommand = class {
|
|
|
1727
1803
|
}
|
|
1728
1804
|
async run() {
|
|
1729
1805
|
console.error("[ctxo] Verifying index freshness...");
|
|
1730
|
-
const tempDir = mkdtempSync(
|
|
1806
|
+
const tempDir = mkdtempSync(join12(tmpdir2(), "ctxo-verify-"));
|
|
1731
1807
|
try {
|
|
1732
|
-
const tempCtxo =
|
|
1808
|
+
const tempCtxo = join12(tempDir, ".ctxo");
|
|
1733
1809
|
const indexCmd = new IndexCommand(this.projectRoot, tempCtxo);
|
|
1734
1810
|
await indexCmd.run({ skipSideEffects: true });
|
|
1735
|
-
const committedReader = new JsonIndexReader(
|
|
1811
|
+
const committedReader = new JsonIndexReader(join12(this.projectRoot, ".ctxo"));
|
|
1736
1812
|
const freshReader = new JsonIndexReader(tempCtxo);
|
|
1737
1813
|
const committedIndices = committedReader.readAll();
|
|
1738
1814
|
const freshIndices = freshReader.readAll();
|
|
@@ -1765,13 +1841,41 @@ var VerifyCommand = class {
|
|
|
1765
1841
|
};
|
|
1766
1842
|
|
|
1767
1843
|
// src/cli/init-command.ts
|
|
1768
|
-
import { join as
|
|
1769
|
-
import { existsSync as
|
|
1844
|
+
import { join as join14 } from "path";
|
|
1845
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, chmodSync } from "fs";
|
|
1770
1846
|
import { createRequire as createRequire4 } from "module";
|
|
1771
1847
|
|
|
1772
1848
|
// src/cli/ai-rules.ts
|
|
1773
|
-
import { existsSync as
|
|
1774
|
-
import { join as
|
|
1849
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1850
|
+
import { join as join13, dirname as dirname6 } from "path";
|
|
1851
|
+
var CTXO_HOOK_COMMAND = "ctxo gate-hook";
|
|
1852
|
+
function ensureClaudeHook(projectRoot) {
|
|
1853
|
+
const filePath = join13(projectRoot, ".claude", "settings.json");
|
|
1854
|
+
const dir = dirname6(filePath);
|
|
1855
|
+
if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
|
|
1856
|
+
const ctxoEntry = { matcher: "Edit", hooks: [{ type: "command", command: CTXO_HOOK_COMMAND }] };
|
|
1857
|
+
if (!existsSync12(filePath)) {
|
|
1858
|
+
writeFileSync3(filePath, JSON.stringify({ hooks: { PreToolUse: [ctxoEntry] } }, null, 2) + "\n", "utf-8");
|
|
1859
|
+
return { file: ".claude/settings.json", action: "created" };
|
|
1860
|
+
}
|
|
1861
|
+
let existing;
|
|
1862
|
+
try {
|
|
1863
|
+
existing = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
1864
|
+
} catch {
|
|
1865
|
+
writeFileSync3(filePath, JSON.stringify({ hooks: { PreToolUse: [ctxoEntry] } }, null, 2) + "\n", "utf-8");
|
|
1866
|
+
return { file: ".claude/settings.json", action: "updated" };
|
|
1867
|
+
}
|
|
1868
|
+
const hooks = existing.hooks ?? {};
|
|
1869
|
+
const pre = hooks.PreToolUse ?? [];
|
|
1870
|
+
if (pre.some((e) => e.hooks?.some((h) => h.command === CTXO_HOOK_COMMAND))) {
|
|
1871
|
+
return { file: ".claude/settings.json", action: "skipped" };
|
|
1872
|
+
}
|
|
1873
|
+
pre.push(ctxoEntry);
|
|
1874
|
+
hooks.PreToolUse = pre;
|
|
1875
|
+
existing.hooks = hooks;
|
|
1876
|
+
writeFileSync3(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
1877
|
+
return { file: ".claude/settings.json", action: "updated" };
|
|
1878
|
+
}
|
|
1775
1879
|
var PLATFORMS = [
|
|
1776
1880
|
{ id: "claude-code", name: "Claude Code", file: "CLAUDE.md", mode: "append", detectPaths: ["CLAUDE.md", ".claude"], starred: true },
|
|
1777
1881
|
{ id: "cursor", name: "Cursor", file: ".cursor/rules/ctxo.mdc", mode: "create", detectPaths: [".cursor", ".cursorrules"], starred: false },
|
|
@@ -1786,7 +1890,7 @@ var PLATFORMS = [
|
|
|
1786
1890
|
function detectPlatforms(projectRoot) {
|
|
1787
1891
|
const detected = /* @__PURE__ */ new Set();
|
|
1788
1892
|
for (const p of PLATFORMS) {
|
|
1789
|
-
if (p.detectPaths.some((d) =>
|
|
1893
|
+
if (p.detectPaths.some((d) => existsSync12(join13(projectRoot, d)))) {
|
|
1790
1894
|
detected.add(p.id);
|
|
1791
1895
|
}
|
|
1792
1896
|
}
|
|
@@ -1848,15 +1952,15 @@ function generateRules(platformId) {
|
|
|
1848
1952
|
function installRules(projectRoot, platformId) {
|
|
1849
1953
|
const platform = PLATFORMS.find((p) => p.id === platformId);
|
|
1850
1954
|
if (!platform) throw new Error(`Unknown platform: ${platformId}`);
|
|
1851
|
-
const filePath =
|
|
1955
|
+
const filePath = join13(projectRoot, platform.file);
|
|
1852
1956
|
const content = generateRules(platformId);
|
|
1853
1957
|
const dir = dirname6(filePath);
|
|
1854
|
-
if (!
|
|
1958
|
+
if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
|
|
1855
1959
|
if (platform.mode === "create") {
|
|
1856
1960
|
writeFileSync3(filePath, content, "utf-8");
|
|
1857
|
-
return { file: platform.file, action:
|
|
1961
|
+
return { file: platform.file, action: existsSync12(filePath) ? "updated" : "created" };
|
|
1858
1962
|
}
|
|
1859
|
-
if (!
|
|
1963
|
+
if (!existsSync12(filePath)) {
|
|
1860
1964
|
writeFileSync3(filePath, content, "utf-8");
|
|
1861
1965
|
return { file: platform.file, action: "created" };
|
|
1862
1966
|
}
|
|
@@ -1878,10 +1982,41 @@ ${content}${SECTION_END}
|
|
|
1878
1982
|
function escapeRegExp(s) {
|
|
1879
1983
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1880
1984
|
}
|
|
1985
|
+
var SKILLS = ["ctxo-understand", "ctxo-safe-edit", "ctxo-review-pr"];
|
|
1986
|
+
var SKILL_TARGET_DIR = {
|
|
1987
|
+
"claude-code": (root, skill) => join13(root, ".claude", "skills", skill, "SKILL.md"),
|
|
1988
|
+
cursor: (root, skill) => join13(root, ".cursor", "rules", `${skill}.mdc`)
|
|
1989
|
+
};
|
|
1990
|
+
function skillsSourceDir() {
|
|
1991
|
+
let dir = import.meta.dirname;
|
|
1992
|
+
for (let i = 0; i < 10; i++) {
|
|
1993
|
+
const candidate = join13(dir, "skills");
|
|
1994
|
+
if (existsSync12(join13(candidate, "ctxo-safe-edit", "SKILL.md"))) return candidate;
|
|
1995
|
+
const parent = join13(dir, "..");
|
|
1996
|
+
if (parent === dir) break;
|
|
1997
|
+
dir = parent;
|
|
1998
|
+
}
|
|
1999
|
+
throw new Error("Could not locate ctxo skills/ directory.");
|
|
2000
|
+
}
|
|
2001
|
+
function installSkills(projectRoot, platformId) {
|
|
2002
|
+
const target = SKILL_TARGET_DIR[platformId];
|
|
2003
|
+
if (!target) return [];
|
|
2004
|
+
const src = skillsSourceDir();
|
|
2005
|
+
const out = [];
|
|
2006
|
+
for (const skill of SKILLS) {
|
|
2007
|
+
const from = join13(src, skill, "SKILL.md");
|
|
2008
|
+
const to = target(projectRoot, skill);
|
|
2009
|
+
mkdirSync4(dirname6(to), { recursive: true });
|
|
2010
|
+
const existed = existsSync12(to);
|
|
2011
|
+
writeFileSync3(to, readFileSync7(from, "utf-8"), "utf-8");
|
|
2012
|
+
out.push({ file: to, action: existed ? "updated" : "created" });
|
|
2013
|
+
}
|
|
2014
|
+
return out;
|
|
2015
|
+
}
|
|
1881
2016
|
var GITIGNORE_ENTRY = ".ctxo/.cache/";
|
|
1882
2017
|
function ensureGitignore(projectRoot) {
|
|
1883
|
-
const filePath =
|
|
1884
|
-
if (!
|
|
2018
|
+
const filePath = join13(projectRoot, ".gitignore");
|
|
2019
|
+
if (!existsSync12(filePath)) {
|
|
1885
2020
|
writeFileSync3(filePath, `# Ctxo local cache (auto-rebuilt, never committed)
|
|
1886
2021
|
${GITIGNORE_ENTRY}
|
|
1887
2022
|
`, "utf-8");
|
|
@@ -1916,12 +2051,16 @@ index:
|
|
|
1916
2051
|
ignoreProjects: []
|
|
1917
2052
|
# - "packages/experimental-*"
|
|
1918
2053
|
# - "examples/*"
|
|
2054
|
+
|
|
2055
|
+
gate:
|
|
2056
|
+
enabled: true
|
|
2057
|
+
sensitivity: balanced # strict | balanced | lenient
|
|
1919
2058
|
`;
|
|
1920
2059
|
function ensureConfig(projectRoot) {
|
|
1921
|
-
const filePath =
|
|
2060
|
+
const filePath = join13(projectRoot, ".ctxo", "config.yaml");
|
|
1922
2061
|
const dir = dirname6(filePath);
|
|
1923
|
-
if (!
|
|
1924
|
-
if (
|
|
2062
|
+
if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
|
|
2063
|
+
if (existsSync12(filePath)) {
|
|
1925
2064
|
return { file: ".ctxo/config.yaml", action: "skipped" };
|
|
1926
2065
|
}
|
|
1927
2066
|
writeFileSync3(filePath, DEFAULT_CONFIG, "utf-8");
|
|
@@ -1956,11 +2095,11 @@ function getMcpConfigTargets(selectedPlatformIds) {
|
|
|
1956
2095
|
return [...targets.values()];
|
|
1957
2096
|
}
|
|
1958
2097
|
function ensureMcpConfig(projectRoot, target) {
|
|
1959
|
-
const filePath =
|
|
2098
|
+
const filePath = join13(projectRoot, target.file);
|
|
1960
2099
|
const dir = dirname6(filePath);
|
|
1961
|
-
if (!
|
|
2100
|
+
if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
|
|
1962
2101
|
const entry = target.extraFields ? { ...target.extraFields, ...CTXO_MCP_ENTRY } : { ...CTXO_MCP_ENTRY };
|
|
1963
|
-
if (!
|
|
2102
|
+
if (!existsSync12(filePath)) {
|
|
1964
2103
|
const config = { [target.serverKey]: { ctxo: entry } };
|
|
1965
2104
|
writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1966
2105
|
return { file: target.file, action: "created" };
|
|
@@ -2076,13 +2215,13 @@ var InitCommand = class {
|
|
|
2076
2215
|
this.projectRoot = projectRoot;
|
|
2077
2216
|
}
|
|
2078
2217
|
installHooks() {
|
|
2079
|
-
const hooksDir =
|
|
2218
|
+
const hooksDir = join14(this.projectRoot, ".git", "hooks");
|
|
2080
2219
|
mkdirSync5(hooksDir, { recursive: true });
|
|
2081
2220
|
this.installHook(hooksDir, "post-commit", POST_COMMIT_CONTENT);
|
|
2082
2221
|
this.installHook(hooksDir, "post-merge", POST_MERGE_CONTENT);
|
|
2083
2222
|
}
|
|
2084
2223
|
async run(options = {}) {
|
|
2085
|
-
if (!
|
|
2224
|
+
if (!existsSync13(join14(this.projectRoot, ".git"))) {
|
|
2086
2225
|
console.error('[ctxo] Not a git repository. Run "git init" first.');
|
|
2087
2226
|
process.exit(1);
|
|
2088
2227
|
}
|
|
@@ -2119,8 +2258,8 @@ var InitCommand = class {
|
|
|
2119
2258
|
clack.cancel("Setup cancelled.");
|
|
2120
2259
|
process.exit(0);
|
|
2121
2260
|
}
|
|
2122
|
-
const fullPath =
|
|
2123
|
-
if (!
|
|
2261
|
+
const fullPath = join14(this.projectRoot, indexDir);
|
|
2262
|
+
if (!existsSync13(fullPath)) mkdirSync5(fullPath, { recursive: true });
|
|
2124
2263
|
console.error("");
|
|
2125
2264
|
}
|
|
2126
2265
|
stepNum++;
|
|
@@ -2209,6 +2348,18 @@ var InitCommand = class {
|
|
|
2209
2348
|
}
|
|
2210
2349
|
console.error("");
|
|
2211
2350
|
}
|
|
2351
|
+
let installGuard = false;
|
|
2352
|
+
if (!options.rulesOnly && selectedTools.includes("claude-code")) {
|
|
2353
|
+
const guard = await clack.confirm({
|
|
2354
|
+
message: pc.cyan("Install the safe-edit guard? Blocks edits to high-impact symbols until blast radius is checked."),
|
|
2355
|
+
initialValue: true
|
|
2356
|
+
});
|
|
2357
|
+
if (clack.isCancel(guard)) {
|
|
2358
|
+
clack.cancel("Setup cancelled.");
|
|
2359
|
+
process.exit(0);
|
|
2360
|
+
}
|
|
2361
|
+
installGuard = guard;
|
|
2362
|
+
}
|
|
2212
2363
|
const s = clack.spinner();
|
|
2213
2364
|
s.start(pc.dim("Configuring..."));
|
|
2214
2365
|
const results = [];
|
|
@@ -2226,6 +2377,9 @@ var InitCommand = class {
|
|
|
2226
2377
|
for (const toolId of selectedTools) {
|
|
2227
2378
|
const result = installRules(this.projectRoot, toolId);
|
|
2228
2379
|
results.push(`${pc.green("\u2713")} ${pc.bold(result.file)} ${pc.dim(result.action)}`);
|
|
2380
|
+
for (const sk of installSkills(this.projectRoot, toolId)) {
|
|
2381
|
+
results.push(`${pc.green("\u2713")} ${pc.bold(sk.file)} ${pc.dim(sk.action + " \u2014 skill")}`);
|
|
2382
|
+
}
|
|
2229
2383
|
}
|
|
2230
2384
|
const mcpTargets = getMcpConfigTargets(selectedTools);
|
|
2231
2385
|
for (const target of mcpTargets) {
|
|
@@ -2238,6 +2392,10 @@ var InitCommand = class {
|
|
|
2238
2392
|
this.installHooks();
|
|
2239
2393
|
results.push(`${pc.green("\u2713")} ${pc.bold("post-commit, post-merge")} ${pc.dim("hooks installed")}`);
|
|
2240
2394
|
}
|
|
2395
|
+
if (installGuard) {
|
|
2396
|
+
const r = ensureClaudeHook(this.projectRoot);
|
|
2397
|
+
if (r.action !== "skipped") results.push(`${pc.green("\u2713")} ${pc.bold(r.file)} ${pc.dim(r.action + " \u2014 safe-edit guard")}`);
|
|
2398
|
+
}
|
|
2241
2399
|
if (pluginsToInstall.length > 0) {
|
|
2242
2400
|
s.stop(pc.dim("Installing language plugins..."));
|
|
2243
2401
|
const installer = new InstallCommand(this.projectRoot);
|
|
@@ -2277,8 +2435,8 @@ var InitCommand = class {
|
|
|
2277
2435
|
const gitignoreResult = ensureGitignore(this.projectRoot);
|
|
2278
2436
|
const configResult = ensureConfig(this.projectRoot);
|
|
2279
2437
|
if (!options.rulesOnly) {
|
|
2280
|
-
const indexDir =
|
|
2281
|
-
if (!
|
|
2438
|
+
const indexDir = join14(this.projectRoot, ".ctxo", "index");
|
|
2439
|
+
if (!existsSync13(indexDir)) mkdirSync5(indexDir, { recursive: true });
|
|
2282
2440
|
console.error("[ctxo] \u2713 .ctxo/index/ ready");
|
|
2283
2441
|
}
|
|
2284
2442
|
if (configResult.action !== "skipped") {
|
|
@@ -2290,6 +2448,9 @@ var InitCommand = class {
|
|
|
2290
2448
|
for (const toolId of toolIds) {
|
|
2291
2449
|
const result = installRules(this.projectRoot, toolId);
|
|
2292
2450
|
console.error(`[ctxo] \u2713 ${result.file} \u2014 ${result.action}`);
|
|
2451
|
+
for (const sk of installSkills(this.projectRoot, toolId)) {
|
|
2452
|
+
console.error(`[ctxo] \u2713 ${sk.file} \u2014 ${sk.action} (skill)`);
|
|
2453
|
+
}
|
|
2293
2454
|
}
|
|
2294
2455
|
const mcpTargets = getMcpConfigTargets(toolIds);
|
|
2295
2456
|
for (const target of mcpTargets) {
|
|
@@ -2302,6 +2463,10 @@ var InitCommand = class {
|
|
|
2302
2463
|
this.installHooks();
|
|
2303
2464
|
console.error("[ctxo] \u2713 Git hooks installed");
|
|
2304
2465
|
}
|
|
2466
|
+
if (!options.rulesOnly && toolIds.includes("claude-code")) {
|
|
2467
|
+
const r = ensureClaudeHook(this.projectRoot);
|
|
2468
|
+
if (r.action !== "skipped") console.error(`[ctxo] \u2713 ${r.file} \u2014 ${r.action} (safe-edit guard)`);
|
|
2469
|
+
}
|
|
2305
2470
|
if (!options.rulesOnly && !options.noInstall) {
|
|
2306
2471
|
const detection = detectLanguages(this.projectRoot);
|
|
2307
2472
|
const needed = decideNeededLanguages(detection);
|
|
@@ -2330,7 +2495,7 @@ var InitCommand = class {
|
|
|
2330
2495
|
for (const id of toolIds) {
|
|
2331
2496
|
const platform = PLATFORMS.find((p) => p.id === id);
|
|
2332
2497
|
if (platform) {
|
|
2333
|
-
const exists =
|
|
2498
|
+
const exists = existsSync13(join14(this.projectRoot, platform.file));
|
|
2334
2499
|
const action = platform.mode === "append" && exists ? "append section" : "create";
|
|
2335
2500
|
console.error(` ${platform.file.padEnd(42)} (${action})`);
|
|
2336
2501
|
}
|
|
@@ -2339,27 +2504,31 @@ var InitCommand = class {
|
|
|
2339
2504
|
console.error(" .git/hooks/post-commit (git hook)");
|
|
2340
2505
|
console.error(" .git/hooks/post-merge (git hook)");
|
|
2341
2506
|
}
|
|
2507
|
+
if (!options.rulesOnly && toolIds.includes("claude-code")) {
|
|
2508
|
+
console.error(" .claude/settings.json (safe-edit guard hook)");
|
|
2509
|
+
console.error(" .claude/skills/ctxo-*/SKILL.md (skills)");
|
|
2510
|
+
}
|
|
2342
2511
|
}
|
|
2343
2512
|
/* ---------------------------------------------------------------- */
|
|
2344
2513
|
/* Helpers */
|
|
2345
2514
|
/* ---------------------------------------------------------------- */
|
|
2346
2515
|
async getVersion() {
|
|
2347
2516
|
try {
|
|
2348
|
-
const pkgPath =
|
|
2349
|
-
if (
|
|
2517
|
+
const pkgPath = join14(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
|
|
2518
|
+
if (existsSync13(pkgPath)) {
|
|
2350
2519
|
return JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
|
|
2351
2520
|
}
|
|
2352
|
-
const { createRequire:
|
|
2353
|
-
const
|
|
2354
|
-
return
|
|
2521
|
+
const { createRequire: createRequire9 } = await import("module");
|
|
2522
|
+
const require5 = createRequire9(import.meta.url);
|
|
2523
|
+
return require5("../../package.json").version;
|
|
2355
2524
|
} catch {
|
|
2356
2525
|
return void 0;
|
|
2357
2526
|
}
|
|
2358
2527
|
}
|
|
2359
2528
|
installHook(hooksDir, hookName, hookContent) {
|
|
2360
|
-
const hookPath =
|
|
2529
|
+
const hookPath = join14(hooksDir, hookName);
|
|
2361
2530
|
let existing = "";
|
|
2362
|
-
if (
|
|
2531
|
+
if (existsSync13(hookPath)) {
|
|
2363
2532
|
existing = readFileSync8(hookPath, "utf-8");
|
|
2364
2533
|
if (existing.includes(CTXO_START)) return;
|
|
2365
2534
|
} else {
|
|
@@ -2372,7 +2541,7 @@ var InitCommand = class {
|
|
|
2372
2541
|
};
|
|
2373
2542
|
|
|
2374
2543
|
// src/cli/watch-command.ts
|
|
2375
|
-
import { join as
|
|
2544
|
+
import { join as join15, extname as extname4, relative as relative2 } from "path";
|
|
2376
2545
|
import { readFileSync as readFileSync9 } from "fs";
|
|
2377
2546
|
|
|
2378
2547
|
// src/adapters/watcher/chokidar-watcher-adapter.ts
|
|
@@ -2422,12 +2591,12 @@ var WatchCommand = class {
|
|
|
2422
2591
|
ctxoRoot;
|
|
2423
2592
|
constructor(projectRoot) {
|
|
2424
2593
|
this.projectRoot = projectRoot;
|
|
2425
|
-
this.ctxoRoot =
|
|
2594
|
+
this.ctxoRoot = join15(projectRoot, ".ctxo");
|
|
2426
2595
|
}
|
|
2427
2596
|
async run() {
|
|
2428
2597
|
console.error("[ctxo] Starting file watcher...");
|
|
2429
2598
|
const registry = new LanguageAdapterRegistry();
|
|
2430
|
-
|
|
2599
|
+
const keepAliveByExt = /* @__PURE__ */ new Map();
|
|
2431
2600
|
for (const { plugin, adapter } of await loadPlugins(this.projectRoot)) {
|
|
2432
2601
|
if (typeof adapter.initialize === "function") {
|
|
2433
2602
|
try {
|
|
@@ -2438,15 +2607,12 @@ var WatchCommand = class {
|
|
|
2438
2607
|
}
|
|
2439
2608
|
}
|
|
2440
2609
|
registry.register(plugin.extensions, adapter);
|
|
2441
|
-
const
|
|
2442
|
-
if (typeof
|
|
2443
|
-
const
|
|
2444
|
-
if (
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
roslynAdapter = roslyn;
|
|
2448
|
-
console.error("[ctxo] C# watch: Roslyn keep-alive active (full tier, <100ms per change)");
|
|
2449
|
-
}
|
|
2610
|
+
const capable = adapter;
|
|
2611
|
+
if (typeof capable.getIncrementalReindex === "function") {
|
|
2612
|
+
const cap = capable.getIncrementalReindex();
|
|
2613
|
+
if (cap?.isReady() && await cap.startKeepAlive()) {
|
|
2614
|
+
for (const ext of plugin.extensions) keepAliveByExt.set(ext.toLowerCase(), cap);
|
|
2615
|
+
console.error(`[ctxo] ${plugin.id} watch: keep-alive active (full tier, fast incremental re-index)`);
|
|
2450
2616
|
}
|
|
2451
2617
|
}
|
|
2452
2618
|
console.error(`[ctxo] Plugin ${plugin.id}@${plugin.version} (${plugin.tier} tier)`);
|
|
@@ -2467,8 +2633,9 @@ var WatchCommand = class {
|
|
|
2467
2633
|
if (!adapter) return;
|
|
2468
2634
|
const relativePath = relative2(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
2469
2635
|
try {
|
|
2470
|
-
|
|
2471
|
-
|
|
2636
|
+
const cap = keepAliveByExt.get(extname4(filePath).toLowerCase());
|
|
2637
|
+
if (cap?.isReady()) {
|
|
2638
|
+
const result = await cap.reindexFile(relativePath);
|
|
2472
2639
|
if (result) {
|
|
2473
2640
|
const commits2 = await gitAdapter.getCommitHistory(relativePath);
|
|
2474
2641
|
const intent2 = commits2.map((c) => ({
|
|
@@ -2504,7 +2671,7 @@ var WatchCommand = class {
|
|
|
2504
2671
|
};
|
|
2505
2672
|
writer.write(fileIndex2);
|
|
2506
2673
|
storage.writeSymbolFile(fileIndex2);
|
|
2507
|
-
console.error(`[ctxo] Re-indexed (
|
|
2674
|
+
console.error(`[ctxo] Re-indexed (keep-alive): ${relativePath}`);
|
|
2508
2675
|
return;
|
|
2509
2676
|
}
|
|
2510
2677
|
}
|
|
@@ -2592,8 +2759,9 @@ var WatchCommand = class {
|
|
|
2592
2759
|
clearTimeout(timeout);
|
|
2593
2760
|
}
|
|
2594
2761
|
if (communitySnapshotTimer) clearTimeout(communitySnapshotTimer);
|
|
2595
|
-
const
|
|
2596
|
-
|
|
2762
|
+
const uniques = new Set(keepAliveByExt.values());
|
|
2763
|
+
const shutdowns = Promise.all([...uniques].map((c) => c.dispose()));
|
|
2764
|
+
shutdowns.then(() => {
|
|
2597
2765
|
watcher.stop().then(() => {
|
|
2598
2766
|
storage.close();
|
|
2599
2767
|
console.error("[ctxo] Watcher stopped");
|
|
@@ -2637,21 +2805,21 @@ function readHeadSha() {
|
|
|
2637
2805
|
}
|
|
2638
2806
|
|
|
2639
2807
|
// src/cli/stats-command.ts
|
|
2640
|
-
import { join as
|
|
2641
|
-
import { existsSync as
|
|
2808
|
+
import { join as join16 } from "path";
|
|
2809
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
|
|
2642
2810
|
import initSqlJs from "sql.js";
|
|
2643
2811
|
var StatsCommand = class {
|
|
2644
2812
|
ctxoRoot;
|
|
2645
2813
|
constructor(projectRoot) {
|
|
2646
|
-
this.ctxoRoot =
|
|
2814
|
+
this.ctxoRoot = join16(projectRoot, ".ctxo");
|
|
2647
2815
|
}
|
|
2648
2816
|
async run(options) {
|
|
2649
2817
|
if (!this.isStatsEnabled()) {
|
|
2650
2818
|
console.error("[ctxo] Stats collection is disabled in .ctxo/config.yaml");
|
|
2651
2819
|
return;
|
|
2652
2820
|
}
|
|
2653
|
-
const dbPath =
|
|
2654
|
-
if (!
|
|
2821
|
+
const dbPath = join16(this.ctxoRoot, ".cache", "symbols.db");
|
|
2822
|
+
if (!existsSync14(dbPath)) {
|
|
2655
2823
|
if (options.json) {
|
|
2656
2824
|
process.stdout.write(JSON.stringify(this.emptyReport(options.days)) + "\n");
|
|
2657
2825
|
} else {
|
|
@@ -2667,8 +2835,8 @@ var StatsCommand = class {
|
|
|
2667
2835
|
if (options.clear) {
|
|
2668
2836
|
recorder.clear();
|
|
2669
2837
|
const data = db.export();
|
|
2670
|
-
const { writeFileSync:
|
|
2671
|
-
|
|
2838
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
2839
|
+
writeFileSync7(dbPath, Buffer.from(data));
|
|
2672
2840
|
console.error("[ctxo] Session data cleared.");
|
|
2673
2841
|
return;
|
|
2674
2842
|
}
|
|
@@ -2698,8 +2866,8 @@ var StatsCommand = class {
|
|
|
2698
2866
|
}
|
|
2699
2867
|
}
|
|
2700
2868
|
isStatsEnabled() {
|
|
2701
|
-
const configPath =
|
|
2702
|
-
if (!
|
|
2869
|
+
const configPath = join16(this.ctxoRoot, "config.yaml");
|
|
2870
|
+
if (!existsSync14(configPath)) return true;
|
|
2703
2871
|
try {
|
|
2704
2872
|
const raw = readFileSync10(configPath, "utf-8");
|
|
2705
2873
|
const match = raw.match(/stats:\s*\n\s*enabled:\s*(true|false)/);
|
|
@@ -2790,8 +2958,8 @@ var StatsCommand = class {
|
|
|
2790
2958
|
};
|
|
2791
2959
|
|
|
2792
2960
|
// src/cli/doctor-command.ts
|
|
2793
|
-
import { join as
|
|
2794
|
-
import { existsSync as
|
|
2961
|
+
import { join as join24 } from "path";
|
|
2962
|
+
import { existsSync as existsSync22 } from "fs";
|
|
2795
2963
|
|
|
2796
2964
|
// src/adapters/diagnostics/health-checker.ts
|
|
2797
2965
|
var log4 = createLogger("ctxo:doctor");
|
|
@@ -3126,10 +3294,111 @@ function pluginResolves(packageName) {
|
|
|
3126
3294
|
}
|
|
3127
3295
|
}
|
|
3128
3296
|
|
|
3297
|
+
// src/adapters/diagnostics/checks/java-tier-check.ts
|
|
3298
|
+
import { createRequire as createRequire7 } from "module";
|
|
3299
|
+
import { existsSync as existsSync15, readdirSync as readdirSync3 } from "fs";
|
|
3300
|
+
import { join as join17, dirname as dirname7 } from "path";
|
|
3301
|
+
var require4 = createRequire7(import.meta.url);
|
|
3302
|
+
function evaluateJavaTier(i) {
|
|
3303
|
+
const id = "java_tier";
|
|
3304
|
+
const title = "Java analysis tier";
|
|
3305
|
+
if (!i.hasJava) {
|
|
3306
|
+
return { id, title, status: "pass", message: "no Java sources detected" };
|
|
3307
|
+
}
|
|
3308
|
+
const jreOk = (i.jreMajor ?? 0) >= 17;
|
|
3309
|
+
if (jreOk && i.analyzerInstalled) {
|
|
3310
|
+
return { id, title, status: "pass", message: `full tier (JRE ${i.jreMajor}, analyzer installed)` };
|
|
3311
|
+
}
|
|
3312
|
+
if (jreOk && !i.analyzerInstalled) {
|
|
3313
|
+
return {
|
|
3314
|
+
id,
|
|
3315
|
+
title,
|
|
3316
|
+
status: "warn",
|
|
3317
|
+
message: `syntax tier (JRE ${i.jreMajor} present, analyzer missing)`,
|
|
3318
|
+
fix: 'Run "ctxo install java --full-tier" for resolved call/use edges'
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
if (!jreOk && i.analyzerInstalled) {
|
|
3322
|
+
return {
|
|
3323
|
+
id,
|
|
3324
|
+
title,
|
|
3325
|
+
status: "warn",
|
|
3326
|
+
message: "syntax tier (analyzer installed but no JRE 17+)",
|
|
3327
|
+
fix: "Install a JRE 17+ and ensure it is on PATH (or set JAVA_HOME)"
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
return {
|
|
3331
|
+
id,
|
|
3332
|
+
title,
|
|
3333
|
+
status: "pass",
|
|
3334
|
+
message: 'syntax tier (install JRE 17+ and run "ctxo install java --full-tier" for full tier)'
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
function analyzerJarPresent() {
|
|
3338
|
+
try {
|
|
3339
|
+
const pkgJson = require4.resolve("@ctxo/lang-java-analyzer/package.json");
|
|
3340
|
+
const jarPath = join17(dirname7(pkgJson), "jar", "ctxo-jdt-analyzer.jar");
|
|
3341
|
+
return existsSync15(jarPath);
|
|
3342
|
+
} catch {
|
|
3343
|
+
return false;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3347
|
+
"node_modules",
|
|
3348
|
+
"target",
|
|
3349
|
+
"build",
|
|
3350
|
+
"out",
|
|
3351
|
+
"bin",
|
|
3352
|
+
".git",
|
|
3353
|
+
".ctxo",
|
|
3354
|
+
"dist",
|
|
3355
|
+
"coverage"
|
|
3356
|
+
]);
|
|
3357
|
+
function findJavaFile(root, maxDepth) {
|
|
3358
|
+
function walk(dir, depth) {
|
|
3359
|
+
let entries;
|
|
3360
|
+
try {
|
|
3361
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
3362
|
+
} catch {
|
|
3363
|
+
return false;
|
|
3364
|
+
}
|
|
3365
|
+
for (const entry of entries) {
|
|
3366
|
+
if (entry.isSymbolicLink()) continue;
|
|
3367
|
+
if (entry.isFile() && entry.name.endsWith(".java")) return true;
|
|
3368
|
+
}
|
|
3369
|
+
if (depth >= maxDepth) return false;
|
|
3370
|
+
for (const entry of entries) {
|
|
3371
|
+
if (entry.isSymbolicLink()) continue;
|
|
3372
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
3373
|
+
if (walk(join17(dir, entry.name), depth + 1)) return true;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return false;
|
|
3377
|
+
}
|
|
3378
|
+
return walk(root, 0);
|
|
3379
|
+
}
|
|
3380
|
+
function projectHasJava(root) {
|
|
3381
|
+
for (const f of ["pom.xml", "build.gradle", "build.gradle.kts"]) {
|
|
3382
|
+
if (existsSync15(join17(root, f))) return true;
|
|
3383
|
+
}
|
|
3384
|
+
return findJavaFile(root, 4);
|
|
3385
|
+
}
|
|
3386
|
+
var JavaTierCheck = class {
|
|
3387
|
+
id = "java_tier";
|
|
3388
|
+
title = "Java analysis tier";
|
|
3389
|
+
async run(ctx) {
|
|
3390
|
+
return evaluateJavaTier({
|
|
3391
|
+
hasJava: projectHasJava(ctx.projectRoot),
|
|
3392
|
+
jreMajor: detectJavaMajor(),
|
|
3393
|
+
analyzerInstalled: analyzerJarPresent()
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
};
|
|
3397
|
+
|
|
3129
3398
|
// src/adapters/diagnostics/checks/git-check.ts
|
|
3130
3399
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
3131
|
-
import { existsSync as
|
|
3132
|
-
import { join as
|
|
3400
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3401
|
+
import { join as join18 } from "path";
|
|
3133
3402
|
var GitBinaryCheck = class {
|
|
3134
3403
|
id = "git_binary";
|
|
3135
3404
|
title = "Git available";
|
|
@@ -3146,7 +3415,7 @@ var GitRepoCheck = class {
|
|
|
3146
3415
|
id = "git_repo";
|
|
3147
3416
|
title = "Git repository";
|
|
3148
3417
|
async run(ctx) {
|
|
3149
|
-
if (
|
|
3418
|
+
if (existsSync16(join18(ctx.projectRoot, ".git"))) {
|
|
3150
3419
|
return { id: this.id, title: this.title, status: "pass", message: ctx.projectRoot, value: ctx.projectRoot };
|
|
3151
3420
|
}
|
|
3152
3421
|
return { id: this.id, title: this.title, status: "fail", message: "Not a git repository", fix: 'Run "git init" or clone a repository' };
|
|
@@ -3154,9 +3423,9 @@ var GitRepoCheck = class {
|
|
|
3154
3423
|
};
|
|
3155
3424
|
|
|
3156
3425
|
// src/adapters/diagnostics/checks/index-check.ts
|
|
3157
|
-
import { existsSync as
|
|
3426
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3158
3427
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
3159
|
-
import { join as
|
|
3428
|
+
import { join as join19 } from "path";
|
|
3160
3429
|
function pass2(id, title, message, value) {
|
|
3161
3430
|
return { id, title, status: "pass", message, value };
|
|
3162
3431
|
}
|
|
@@ -3173,8 +3442,8 @@ var IndexDirectoryCheck = class {
|
|
|
3173
3442
|
id = "index_directory";
|
|
3174
3443
|
title = "Index directory";
|
|
3175
3444
|
async run(ctx) {
|
|
3176
|
-
const indexDir =
|
|
3177
|
-
if (!
|
|
3445
|
+
const indexDir = join19(ctx.ctxoRoot, "index");
|
|
3446
|
+
if (!existsSync17(indexDir)) {
|
|
3178
3447
|
return fail(this.id, this.title, "No index directory", 'Run "ctxo index"');
|
|
3179
3448
|
}
|
|
3180
3449
|
const indices = getIndices(ctx);
|
|
@@ -3268,8 +3537,8 @@ var CoChangesCacheCheck = class {
|
|
|
3268
3537
|
id = "co_changes_cache";
|
|
3269
3538
|
title = "Co-changes cache";
|
|
3270
3539
|
async run(ctx) {
|
|
3271
|
-
const coChangesPath =
|
|
3272
|
-
if (
|
|
3540
|
+
const coChangesPath = join19(ctx.ctxoRoot, "index", "co-changes.json");
|
|
3541
|
+
if (existsSync17(coChangesPath)) {
|
|
3273
3542
|
return pass2(this.id, this.title, "present");
|
|
3274
3543
|
}
|
|
3275
3544
|
return warn2(this.id, this.title, "missing (co-change analysis disabled)", 'Run "ctxo index" with git history');
|
|
@@ -3293,14 +3562,14 @@ var SchemaVersionCheck = class {
|
|
|
3293
3562
|
};
|
|
3294
3563
|
|
|
3295
3564
|
// src/adapters/diagnostics/checks/storage-check.ts
|
|
3296
|
-
import { existsSync as
|
|
3297
|
-
import { join as
|
|
3565
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
|
|
3566
|
+
import { join as join20 } from "path";
|
|
3298
3567
|
var SqliteCacheCheck = class {
|
|
3299
3568
|
id = "sqlite_cache";
|
|
3300
3569
|
title = "SQLite cache";
|
|
3301
3570
|
async run(ctx) {
|
|
3302
|
-
const dbPath =
|
|
3303
|
-
if (!
|
|
3571
|
+
const dbPath = join20(ctx.ctxoRoot, ".cache", "symbols.db");
|
|
3572
|
+
if (!existsSync18(dbPath)) {
|
|
3304
3573
|
return { id: this.id, title: this.title, status: "warn", message: "SQLite cache missing", fix: 'Run "ctxo sync" to rebuild' };
|
|
3305
3574
|
}
|
|
3306
3575
|
try {
|
|
@@ -3322,14 +3591,14 @@ var SqliteCacheCheck = class {
|
|
|
3322
3591
|
};
|
|
3323
3592
|
|
|
3324
3593
|
// src/adapters/diagnostics/checks/config-check.ts
|
|
3325
|
-
import { existsSync as
|
|
3326
|
-
import { join as
|
|
3594
|
+
import { existsSync as existsSync19, readFileSync as readFileSync12 } from "fs";
|
|
3595
|
+
import { join as join21 } from "path";
|
|
3327
3596
|
var ConfigFileCheck = class {
|
|
3328
3597
|
id = "config_file";
|
|
3329
3598
|
title = "Config file";
|
|
3330
3599
|
async run(ctx) {
|
|
3331
|
-
const configPath =
|
|
3332
|
-
if (!
|
|
3600
|
+
const configPath = join21(ctx.ctxoRoot, "config.yaml");
|
|
3601
|
+
if (!existsSync19(configPath)) {
|
|
3333
3602
|
return { id: this.id, title: this.title, status: "warn", message: "No config.yaml (using defaults)" };
|
|
3334
3603
|
}
|
|
3335
3604
|
try {
|
|
@@ -3367,8 +3636,8 @@ var ConfigFileCheck = class {
|
|
|
3367
3636
|
};
|
|
3368
3637
|
|
|
3369
3638
|
// src/adapters/diagnostics/checks/disk-check.ts
|
|
3370
|
-
import { existsSync as
|
|
3371
|
-
import { join as
|
|
3639
|
+
import { existsSync as existsSync20, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
3640
|
+
import { join as join22 } from "path";
|
|
3372
3641
|
var DEFAULT_THRESHOLDS = { warnMb: 100, failMb: 500 };
|
|
3373
3642
|
var DiskUsageCheck = class {
|
|
3374
3643
|
id = "disk_usage";
|
|
@@ -3378,7 +3647,7 @@ var DiskUsageCheck = class {
|
|
|
3378
3647
|
this.thresholds = thresholds;
|
|
3379
3648
|
}
|
|
3380
3649
|
async run(ctx) {
|
|
3381
|
-
if (!
|
|
3650
|
+
if (!existsSync20(ctx.ctxoRoot)) {
|
|
3382
3651
|
return { id: this.id, title: this.title, status: "pass", message: "no .ctxo/ directory", value: "0" };
|
|
3383
3652
|
}
|
|
3384
3653
|
const bytes = this.getDirSize(ctx.ctxoRoot);
|
|
@@ -3395,12 +3664,12 @@ var DiskUsageCheck = class {
|
|
|
3395
3664
|
getDirSize(dirPath) {
|
|
3396
3665
|
let total = 0;
|
|
3397
3666
|
try {
|
|
3398
|
-
const entries =
|
|
3667
|
+
const entries = readdirSync4(dirPath, { withFileTypes: true, recursive: true });
|
|
3399
3668
|
for (const entry of entries) {
|
|
3400
3669
|
if (entry.isFile()) {
|
|
3401
3670
|
try {
|
|
3402
3671
|
const parent = entry.parentPath ?? entry.path ?? dirPath;
|
|
3403
|
-
total += statSync2(
|
|
3672
|
+
total += statSync2(join22(parent, entry.name)).size;
|
|
3404
3673
|
} catch {
|
|
3405
3674
|
}
|
|
3406
3675
|
}
|
|
@@ -3413,9 +3682,9 @@ var DiskUsageCheck = class {
|
|
|
3413
3682
|
|
|
3414
3683
|
// src/cli/doctor-fix.ts
|
|
3415
3684
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
3416
|
-
import { appendFileSync, existsSync as
|
|
3417
|
-
import { createRequire as
|
|
3418
|
-
import { dirname as
|
|
3685
|
+
import { appendFileSync, existsSync as existsSync21, mkdirSync as mkdirSync6, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
3686
|
+
import { createRequire as createRequire8 } from "module";
|
|
3687
|
+
import { dirname as dirname8, join as join23 } from "path";
|
|
3419
3688
|
async function applyFixes(report, options) {
|
|
3420
3689
|
if (isCI(process.env) && !options.yes && !options.dryRun) {
|
|
3421
3690
|
return {
|
|
@@ -3550,8 +3819,8 @@ async function fixIndex(projectRoot, dryRun) {
|
|
|
3550
3819
|
}
|
|
3551
3820
|
async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
3552
3821
|
if (dryRun) return;
|
|
3553
|
-
const indexDir =
|
|
3554
|
-
if (!
|
|
3822
|
+
const indexDir = join23(ctxoRoot, "index");
|
|
3823
|
+
if (!existsSync21(indexDir)) return;
|
|
3555
3824
|
const sourceFiles = listGitFiles2(projectRoot);
|
|
3556
3825
|
if (sourceFiles.size === 0) return;
|
|
3557
3826
|
const reader = new JsonIndexReader(ctxoRoot);
|
|
@@ -3559,9 +3828,9 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
|
3559
3828
|
let removed = 0;
|
|
3560
3829
|
for (const idx of indices) {
|
|
3561
3830
|
if (sourceFiles.has(idx.file)) continue;
|
|
3562
|
-
const jsonPath =
|
|
3831
|
+
const jsonPath = join23(indexDir, `${idx.file}.json`);
|
|
3563
3832
|
try {
|
|
3564
|
-
if (
|
|
3833
|
+
if (existsSync21(jsonPath)) {
|
|
3565
3834
|
unlinkSync2(jsonPath);
|
|
3566
3835
|
removed += 1;
|
|
3567
3836
|
pruneEmptyParents(jsonPath, indexDir);
|
|
@@ -3572,14 +3841,14 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
|
3572
3841
|
if (removed === 0) return;
|
|
3573
3842
|
}
|
|
3574
3843
|
function pruneEmptyParents(filePath, stopAt) {
|
|
3575
|
-
let dir =
|
|
3844
|
+
let dir = dirname8(filePath);
|
|
3576
3845
|
while (dir !== stopAt && dir.startsWith(stopAt)) {
|
|
3577
3846
|
try {
|
|
3578
3847
|
rmSync2(dir, { recursive: false });
|
|
3579
3848
|
} catch {
|
|
3580
3849
|
return;
|
|
3581
3850
|
}
|
|
3582
|
-
dir =
|
|
3851
|
+
dir = dirname8(dir);
|
|
3583
3852
|
}
|
|
3584
3853
|
}
|
|
3585
3854
|
function listGitFiles2(projectRoot) {
|
|
@@ -3595,12 +3864,12 @@ function listGitFiles2(projectRoot) {
|
|
|
3595
3864
|
}
|
|
3596
3865
|
}
|
|
3597
3866
|
async function fixSqliteCache(projectRoot, ctxoRoot, dryRun) {
|
|
3598
|
-
const cacheDir =
|
|
3867
|
+
const cacheDir = join23(ctxoRoot, ".cache");
|
|
3599
3868
|
if (dryRun) return;
|
|
3600
|
-
if (
|
|
3869
|
+
if (existsSync21(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
|
|
3601
3870
|
await new SyncCommand(projectRoot).run();
|
|
3602
3871
|
}
|
|
3603
|
-
var doctorRequire =
|
|
3872
|
+
var doctorRequire = createRequire8(import.meta.url);
|
|
3604
3873
|
function canResolvePlugin(lang) {
|
|
3605
3874
|
try {
|
|
3606
3875
|
doctorRequire.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
@@ -3614,8 +3883,8 @@ function isCI(env) {
|
|
|
3614
3883
|
}
|
|
3615
3884
|
function writeLog(ctxoRoot, attempts, halted, reason) {
|
|
3616
3885
|
try {
|
|
3617
|
-
if (!
|
|
3618
|
-
const path =
|
|
3886
|
+
if (!existsSync21(ctxoRoot)) mkdirSync6(ctxoRoot, { recursive: true });
|
|
3887
|
+
const path = join23(ctxoRoot, "doctor-fix.log");
|
|
3619
3888
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3620
3889
|
const lines = [`# ${ts} doctor --fix${halted ? " (halted: " + reason + ")" : ""}`];
|
|
3621
3890
|
for (const a of attempts) {
|
|
@@ -3648,10 +3917,10 @@ var DoctorCommand = class {
|
|
|
3648
3917
|
ctxoRoot;
|
|
3649
3918
|
constructor(projectRoot) {
|
|
3650
3919
|
this.projectRoot = projectRoot;
|
|
3651
|
-
this.ctxoRoot =
|
|
3920
|
+
this.ctxoRoot = join24(projectRoot, ".ctxo");
|
|
3652
3921
|
}
|
|
3653
3922
|
async run(options = {}) {
|
|
3654
|
-
const { getVersion: getVersion2 } = await import("./cli-router-
|
|
3923
|
+
const { getVersion: getVersion2 } = await import("./cli-router-NFAGLJ46.js");
|
|
3655
3924
|
console.error(`ctxo v${getVersion2()} \u2014 health check
|
|
3656
3925
|
`);
|
|
3657
3926
|
const checks = [
|
|
@@ -3666,6 +3935,7 @@ var DoctorCommand = class {
|
|
|
3666
3935
|
new TsMorphCheck(),
|
|
3667
3936
|
new TreeSitterCheck(),
|
|
3668
3937
|
new LanguageCoverageCheck(),
|
|
3938
|
+
new JavaTierCheck(),
|
|
3669
3939
|
new DiskUsageCheck(),
|
|
3670
3940
|
new SymbolCountCheck(),
|
|
3671
3941
|
new EdgeCountCheck(),
|
|
@@ -3673,8 +3943,8 @@ var DoctorCommand = class {
|
|
|
3673
3943
|
new CoChangesCacheCheck(),
|
|
3674
3944
|
new SchemaVersionCheck()
|
|
3675
3945
|
];
|
|
3676
|
-
const indexDir =
|
|
3677
|
-
const indices =
|
|
3946
|
+
const indexDir = join24(this.ctxoRoot, "index");
|
|
3947
|
+
const indices = existsSync22(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
|
|
3678
3948
|
const checker = new HealthChecker(checks, { quiet: Boolean(options.quiet) });
|
|
3679
3949
|
const report = await checker.runAll({
|
|
3680
3950
|
projectRoot: this.projectRoot,
|
|
@@ -3715,36 +3985,35 @@ var DoctorCommand = class {
|
|
|
3715
3985
|
};
|
|
3716
3986
|
|
|
3717
3987
|
// src/cli/visualize-command.ts
|
|
3718
|
-
import { readFileSync as readFileSync13, writeFileSync as writeFileSync5, existsSync as
|
|
3719
|
-
import { join as
|
|
3988
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync5, existsSync as existsSync23 } from "fs";
|
|
3989
|
+
import { join as join25, basename } from "path";
|
|
3720
3990
|
import { execFile } from "child_process";
|
|
3991
|
+
|
|
3992
|
+
// src/cli/graph-loader.ts
|
|
3993
|
+
function loadGraph(ctxoRoot) {
|
|
3994
|
+
const indices = new JsonIndexReader(ctxoRoot).readAll();
|
|
3995
|
+
const graph = new SymbolGraph();
|
|
3996
|
+
for (const fi of indices) for (const sym of fi.symbols) graph.addNode(sym);
|
|
3997
|
+
for (const fi of indices) for (const edge of fi.edges) graph.addEdge(edge);
|
|
3998
|
+
return { graph, indices };
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// src/cli/visualize-command.ts
|
|
3721
4002
|
var VisualizeCommand = class {
|
|
3722
4003
|
constructor(projectRoot) {
|
|
3723
4004
|
this.projectRoot = projectRoot;
|
|
3724
|
-
this.ctxoRoot =
|
|
4005
|
+
this.ctxoRoot = join25(projectRoot, ".ctxo");
|
|
3725
4006
|
}
|
|
3726
4007
|
projectRoot;
|
|
3727
4008
|
ctxoRoot;
|
|
3728
4009
|
async run(options = {}) {
|
|
3729
|
-
const
|
|
3730
|
-
const indices = reader.readAll();
|
|
4010
|
+
const { graph, indices } = loadGraph(this.ctxoRoot);
|
|
3731
4011
|
if (indices.length === 0) {
|
|
3732
4012
|
console.error('[ctxo] No index found. Run "ctxo index" first.');
|
|
3733
4013
|
process.exit(1);
|
|
3734
4014
|
return;
|
|
3735
4015
|
}
|
|
3736
4016
|
console.error(`[ctxo] Building graph from ${indices.length} files...`);
|
|
3737
|
-
const graph = new SymbolGraph();
|
|
3738
|
-
for (const fileIndex of indices) {
|
|
3739
|
-
for (const sym of fileIndex.symbols) {
|
|
3740
|
-
graph.addNode(sym);
|
|
3741
|
-
}
|
|
3742
|
-
}
|
|
3743
|
-
for (const fileIndex of indices) {
|
|
3744
|
-
for (const edge of fileIndex.edges) {
|
|
3745
|
-
graph.addEdge(edge);
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
3748
4017
|
console.error(`[ctxo] Graph: ${graph.nodeCount} symbols, ${graph.edgeCount} edges`);
|
|
3749
4018
|
const pageRank = new PageRankCalculator();
|
|
3750
4019
|
const prResult = pageRank.calculate(graph, { limit: 1e5 });
|
|
@@ -3876,7 +4145,7 @@ var VisualizeCommand = class {
|
|
|
3876
4145
|
const templatePath = this.getTemplatePath();
|
|
3877
4146
|
const template = readFileSync13(templatePath, "utf-8");
|
|
3878
4147
|
const html = template.replace("/*__CTXO_DATA__*/null", JSON.stringify(payload));
|
|
3879
|
-
const outputPath = options.output ??
|
|
4148
|
+
const outputPath = options.output ?? join25(this.ctxoRoot, "visualize.html");
|
|
3880
4149
|
writeFileSync5(outputPath, html, "utf-8");
|
|
3881
4150
|
console.error(`[ctxo] Wrote visualization to ${outputPath}`);
|
|
3882
4151
|
console.error(`[ctxo] ${nodes.length} symbols, ${edges.length} edges, ${deadResult.deadSymbols.length} dead`);
|
|
@@ -3887,9 +4156,9 @@ var VisualizeCommand = class {
|
|
|
3887
4156
|
getTemplatePath() {
|
|
3888
4157
|
let dir = import.meta.dirname;
|
|
3889
4158
|
for (let i = 0; i < 10; i++) {
|
|
3890
|
-
const candidate =
|
|
3891
|
-
if (
|
|
3892
|
-
const parent =
|
|
4159
|
+
const candidate = join25(dir, "pages", "visualize-template.html");
|
|
4160
|
+
if (existsSync23(candidate)) return candidate;
|
|
4161
|
+
const parent = join25(dir, "..");
|
|
3893
4162
|
if (parent === dir) break;
|
|
3894
4163
|
dir = parent;
|
|
3895
4164
|
}
|
|
@@ -3913,8 +4182,8 @@ var VisualizeCommand = class {
|
|
|
3913
4182
|
};
|
|
3914
4183
|
|
|
3915
4184
|
// src/cli/update-command.ts
|
|
3916
|
-
import { existsSync as
|
|
3917
|
-
import { join as
|
|
4185
|
+
import { existsSync as existsSync24, readFileSync as readFileSync14 } from "fs";
|
|
4186
|
+
import { join as join26 } from "path";
|
|
3918
4187
|
|
|
3919
4188
|
// src/core/update/registry-client.ts
|
|
3920
4189
|
import { request } from "https";
|
|
@@ -4232,8 +4501,8 @@ var UpdateCommand = class {
|
|
|
4232
4501
|
return this.projectHasCtxoDep() ? "execute" : "print";
|
|
4233
4502
|
}
|
|
4234
4503
|
projectHasCtxoDep() {
|
|
4235
|
-
const pkg =
|
|
4236
|
-
if (!
|
|
4504
|
+
const pkg = join26(this.projectRoot, "package.json");
|
|
4505
|
+
if (!existsSync24(pkg)) return false;
|
|
4237
4506
|
try {
|
|
4238
4507
|
const json = JSON.parse(readFileSync14(pkg, "utf-8"));
|
|
4239
4508
|
const merged = { ...json.dependencies ?? {}, ...json.devDependencies ?? {} };
|
|
@@ -4269,20 +4538,326 @@ function isLockedCI2(env) {
|
|
|
4269
4538
|
return env["CI"] === "true" || env["CI"] === "1";
|
|
4270
4539
|
}
|
|
4271
4540
|
|
|
4541
|
+
// src/cli/blast-radius-command.ts
|
|
4542
|
+
import { join as join27 } from "path";
|
|
4543
|
+
var BlastRadiusCommand = class {
|
|
4544
|
+
ctxoRoot;
|
|
4545
|
+
constructor(projectRoot) {
|
|
4546
|
+
this.ctxoRoot = join27(projectRoot, ".ctxo");
|
|
4547
|
+
}
|
|
4548
|
+
run(options) {
|
|
4549
|
+
const { graph } = loadGraph(this.ctxoRoot);
|
|
4550
|
+
if (!graph.hasNode(options.symbolId)) {
|
|
4551
|
+
process.stdout.write(JSON.stringify({ found: false, symbolId: options.symbolId }) + "\n");
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
const result = new BlastRadiusCalculator().calculate(graph, options.symbolId);
|
|
4555
|
+
const payload = {
|
|
4556
|
+
found: true,
|
|
4557
|
+
symbolId: options.symbolId,
|
|
4558
|
+
directDependentsCount: result.directDependentsCount,
|
|
4559
|
+
confirmedCount: result.confirmedCount,
|
|
4560
|
+
likelyCount: result.likelyCount,
|
|
4561
|
+
potentialCount: result.potentialCount,
|
|
4562
|
+
overallRiskScore: result.overallRiskScore,
|
|
4563
|
+
impactedSymbols: result.impactedSymbols.map((s) => ({
|
|
4564
|
+
symbolId: s.symbolId,
|
|
4565
|
+
confidence: s.confidence,
|
|
4566
|
+
depth: s.depth
|
|
4567
|
+
}))
|
|
4568
|
+
};
|
|
4569
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
4570
|
+
}
|
|
4571
|
+
};
|
|
4572
|
+
|
|
4573
|
+
// src/cli/gate-hook-command.ts
|
|
4574
|
+
import { join as join29, relative as relative3, sep as sep2 } from "path";
|
|
4575
|
+
import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
|
|
4576
|
+
|
|
4577
|
+
// src/core/gate/gate-config.ts
|
|
4578
|
+
var DIAL = {
|
|
4579
|
+
strict: { percentile: 30, minDependents: 2 },
|
|
4580
|
+
balanced: { percentile: 15, minDependents: 3 },
|
|
4581
|
+
lenient: { percentile: 5, minDependents: 5 }
|
|
4582
|
+
};
|
|
4583
|
+
function resolveGateThresholds(gate) {
|
|
4584
|
+
const dial = DIAL[gate?.sensitivity ?? "balanced"];
|
|
4585
|
+
return {
|
|
4586
|
+
enabled: gate?.enabled ?? true,
|
|
4587
|
+
percentile: gate?.percentile ?? dial.percentile,
|
|
4588
|
+
minDependents: gate?.minDependents ?? dial.minDependents
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
// src/core/gate/symbol-locator.ts
|
|
4593
|
+
function lineOfSnippet(source, snippet) {
|
|
4594
|
+
const idx = source.indexOf(snippet);
|
|
4595
|
+
if (idx === -1) return null;
|
|
4596
|
+
let line = 0;
|
|
4597
|
+
for (let i = 0; i < idx; i++) {
|
|
4598
|
+
if (source.codePointAt(i) === 10) line++;
|
|
4599
|
+
}
|
|
4600
|
+
return line;
|
|
4601
|
+
}
|
|
4602
|
+
function locateSymbolAtLine(fileIndex, line) {
|
|
4603
|
+
let best = null;
|
|
4604
|
+
for (const s of fileIndex.symbols) {
|
|
4605
|
+
if (line >= s.startLine && line <= s.endLine) {
|
|
4606
|
+
const span = s.endLine - s.startLine;
|
|
4607
|
+
if (best === null || span < best.span) best = { id: s.symbolId, span };
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
return best?.id ?? null;
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4613
|
+
// src/core/gate/transcript-scan.ts
|
|
4614
|
+
function fileAndName(symbolId) {
|
|
4615
|
+
const [file, name] = symbolId.split("::");
|
|
4616
|
+
return `${file}::${name}`;
|
|
4617
|
+
}
|
|
4618
|
+
function transcriptHasBlastCheck(lines, symbolId) {
|
|
4619
|
+
const target = fileAndName(symbolId);
|
|
4620
|
+
for (const line of lines) {
|
|
4621
|
+
if (!line.includes("get_blast_radius")) continue;
|
|
4622
|
+
let entry;
|
|
4623
|
+
try {
|
|
4624
|
+
entry = JSON.parse(line);
|
|
4625
|
+
} catch {
|
|
4626
|
+
continue;
|
|
4627
|
+
}
|
|
4628
|
+
const content = entry?.message?.content;
|
|
4629
|
+
if (!Array.isArray(content)) continue;
|
|
4630
|
+
for (const block of content) {
|
|
4631
|
+
const b = block;
|
|
4632
|
+
if (b?.type === "tool_use" && typeof b.name === "string" && b.name.includes("get_blast_radius")) {
|
|
4633
|
+
const id = b.input?.symbolId;
|
|
4634
|
+
if (typeof id === "string" && fileAndName(id) === target) return true;
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
return false;
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
// src/adapters/gate/gate-session-store.ts
|
|
4642
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
|
|
4643
|
+
import { join as join28 } from "path";
|
|
4644
|
+
var log7 = createLogger("ctxo:gate-store");
|
|
4645
|
+
var GateSessionStore = class {
|
|
4646
|
+
constructor(cacheDir) {
|
|
4647
|
+
this.cacheDir = cacheDir;
|
|
4648
|
+
}
|
|
4649
|
+
cacheDir;
|
|
4650
|
+
file(sessionId) {
|
|
4651
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4652
|
+
return join28(this.cacheDir, "gate", `${safe}.json`);
|
|
4653
|
+
}
|
|
4654
|
+
read(sessionId) {
|
|
4655
|
+
const f = this.file(sessionId);
|
|
4656
|
+
if (!existsSync25(f)) return /* @__PURE__ */ new Set();
|
|
4657
|
+
try {
|
|
4658
|
+
return new Set(JSON.parse(readFileSync15(f, "utf-8")));
|
|
4659
|
+
} catch (err) {
|
|
4660
|
+
log7.error(`${err.message}`);
|
|
4661
|
+
return /* @__PURE__ */ new Set();
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
hasWarned(sessionId, symbolId) {
|
|
4665
|
+
return this.read(sessionId).has(symbolId);
|
|
4666
|
+
}
|
|
4667
|
+
recordWarned(sessionId, symbolId) {
|
|
4668
|
+
try {
|
|
4669
|
+
const set = this.read(sessionId);
|
|
4670
|
+
set.add(symbolId);
|
|
4671
|
+
const f = this.file(sessionId);
|
|
4672
|
+
mkdirSync7(join28(this.cacheDir, "gate"), { recursive: true });
|
|
4673
|
+
writeFileSync6(f, JSON.stringify([...set]), "utf-8");
|
|
4674
|
+
} catch (err) {
|
|
4675
|
+
log7.error(`${err.message}`);
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
|
|
4680
|
+
// src/core/gate/percentile.ts
|
|
4681
|
+
function percentileCutoff(scores, topPercent) {
|
|
4682
|
+
if (scores.length === 0) return Infinity;
|
|
4683
|
+
const sorted = [...scores].sort((a, b) => a - b);
|
|
4684
|
+
const rank = Math.ceil((100 - topPercent) / 100 * sorted.length);
|
|
4685
|
+
const idx = Math.min(Math.max(rank, 0), sorted.length - 1);
|
|
4686
|
+
return sorted[idx];
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
// src/core/gate/gate-decision.ts
|
|
4690
|
+
var MAX_TOP_DEPENDENTS = 8;
|
|
4691
|
+
function shouldBlock(input, t) {
|
|
4692
|
+
if (!t.enabled) return false;
|
|
4693
|
+
return input.riskCount >= t.minDependents && input.importanceScore >= input.importanceCutoff;
|
|
4694
|
+
}
|
|
4695
|
+
function formatBlockReason(symbolName, riskCount, topDependents) {
|
|
4696
|
+
const list = topDependents.slice(0, MAX_TOP_DEPENDENTS).map((d) => ` - ${d}`).join("\n");
|
|
4697
|
+
return [
|
|
4698
|
+
`\u26A0 ctxo guard: "${symbolName}" is a high-impact symbol - ${riskCount} confirmed/likely dependents may break.`,
|
|
4699
|
+
topDependents.length > 0 ? `Top dependents:
|
|
4700
|
+
${list}` : "",
|
|
4701
|
+
`Call get_blast_radius / get_why_context before editing. This guard fires once per symbol per session - re-issue the edit to proceed.`
|
|
4702
|
+
].filter(Boolean).join("\n");
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
// src/cli/gate-hook-command.ts
|
|
4706
|
+
var log8 = createLogger("ctxo:gate-hook");
|
|
4707
|
+
var RANK_ALL = 1e5;
|
|
4708
|
+
var GateHookCommand = class {
|
|
4709
|
+
constructor(projectRoot) {
|
|
4710
|
+
this.projectRoot = projectRoot;
|
|
4711
|
+
this.ctxoRoot = join29(projectRoot, ".ctxo");
|
|
4712
|
+
this.cacheDir = join29(this.ctxoRoot, ".cache");
|
|
4713
|
+
}
|
|
4714
|
+
projectRoot;
|
|
4715
|
+
ctxoRoot;
|
|
4716
|
+
cacheDir;
|
|
4717
|
+
/** Pure-ish decision: never throws (fail-open). */
|
|
4718
|
+
evaluate(payload) {
|
|
4719
|
+
try {
|
|
4720
|
+
if (payload.tool_name !== "Edit") return { block: false };
|
|
4721
|
+
const filePath = payload.tool_input?.file_path;
|
|
4722
|
+
const oldString = payload.tool_input?.old_string;
|
|
4723
|
+
if (!filePath || !oldString) return { block: false };
|
|
4724
|
+
const loaded = loadConfig(this.ctxoRoot);
|
|
4725
|
+
const thresholds = resolveGateThresholds(loaded.config.gate);
|
|
4726
|
+
if (!thresholds.enabled) return { block: false };
|
|
4727
|
+
const relPath = relative3(this.projectRoot, filePath).split(sep2).join("/");
|
|
4728
|
+
const indexFile = join29(this.ctxoRoot, "index", `${relPath}.json`);
|
|
4729
|
+
if (!existsSync26(indexFile)) return { block: false };
|
|
4730
|
+
const fileIndex = new JsonIndexReader(this.ctxoRoot).readSingle(indexFile);
|
|
4731
|
+
if (!fileIndex) return { block: false };
|
|
4732
|
+
if (!existsSync26(filePath)) return { block: false };
|
|
4733
|
+
const source = readFileSync16(filePath, "utf-8");
|
|
4734
|
+
const line = lineOfSnippet(source, oldString);
|
|
4735
|
+
if (line === null) return { block: false };
|
|
4736
|
+
const symbolId = locateSymbolAtLine(fileIndex, line);
|
|
4737
|
+
if (!symbolId) return { block: false };
|
|
4738
|
+
const store = new GateSessionStore(this.cacheDir);
|
|
4739
|
+
if (store.hasWarned(payload.session_id, symbolId)) return { block: false };
|
|
4740
|
+
if (payload.transcript_path && existsSync26(payload.transcript_path)) {
|
|
4741
|
+
const lines = readFileSync16(payload.transcript_path, "utf-8").split("\n").filter(Boolean);
|
|
4742
|
+
if (transcriptHasBlastCheck(lines, symbolId)) return { block: false };
|
|
4743
|
+
}
|
|
4744
|
+
const { graph } = loadGraph(this.ctxoRoot);
|
|
4745
|
+
if (!graph.hasNode(symbolId)) return { block: false };
|
|
4746
|
+
const blast = new BlastRadiusCalculator().calculate(graph, symbolId);
|
|
4747
|
+
const riskCount = blast.confirmedCount + blast.likelyCount;
|
|
4748
|
+
const pr = new PageRankCalculator().calculate(graph, { limit: RANK_ALL });
|
|
4749
|
+
const prMap = new Map(pr.rankings.map((r) => [r.symbolId, r.score]));
|
|
4750
|
+
const cutoff = percentileCutoff([...prMap.values()], thresholds.percentile);
|
|
4751
|
+
const importanceScore = prMap.get(symbolId) ?? 0;
|
|
4752
|
+
const block = shouldBlock({ riskCount, importanceScore, importanceCutoff: cutoff }, thresholds);
|
|
4753
|
+
if (!block) return { block: false };
|
|
4754
|
+
store.recordWarned(payload.session_id, symbolId);
|
|
4755
|
+
const name = symbolId.split("::")[1] ?? symbolId;
|
|
4756
|
+
const top = blast.impactedSymbols.filter((s) => s.confidence !== "potential").map((s) => s.symbolId);
|
|
4757
|
+
return { block: true, reason: formatBlockReason(name, riskCount, top) };
|
|
4758
|
+
} catch (err) {
|
|
4759
|
+
log8.error(`${err.message}`);
|
|
4760
|
+
return { block: false };
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
async run() {
|
|
4764
|
+
try {
|
|
4765
|
+
const payload = await this.readStdin();
|
|
4766
|
+
if (payload) {
|
|
4767
|
+
const decision = this.evaluate(payload);
|
|
4768
|
+
if (decision.block) this.emitBlock(decision.reason ?? "ctxo guard");
|
|
4769
|
+
}
|
|
4770
|
+
} catch (err) {
|
|
4771
|
+
log8.error(`${err.message}`);
|
|
4772
|
+
}
|
|
4773
|
+
process.exit(0);
|
|
4774
|
+
}
|
|
4775
|
+
/** PreToolUse block contract (verified): exit 0 + stdout JSON deny. */
|
|
4776
|
+
emitBlock(reason) {
|
|
4777
|
+
process.stdout.write(JSON.stringify({
|
|
4778
|
+
hookSpecificOutput: {
|
|
4779
|
+
hookEventName: "PreToolUse",
|
|
4780
|
+
permissionDecision: "deny",
|
|
4781
|
+
permissionDecisionReason: reason
|
|
4782
|
+
}
|
|
4783
|
+
}) + "\n");
|
|
4784
|
+
process.exit(0);
|
|
4785
|
+
}
|
|
4786
|
+
readStdin() {
|
|
4787
|
+
return new Promise((resolve4) => {
|
|
4788
|
+
let data = "";
|
|
4789
|
+
process.stdin.setEncoding("utf-8");
|
|
4790
|
+
process.stdin.on("data", (c) => {
|
|
4791
|
+
data += c;
|
|
4792
|
+
});
|
|
4793
|
+
process.stdin.on("end", () => {
|
|
4794
|
+
try {
|
|
4795
|
+
resolve4(JSON.parse(data));
|
|
4796
|
+
} catch {
|
|
4797
|
+
resolve4(null);
|
|
4798
|
+
}
|
|
4799
|
+
});
|
|
4800
|
+
process.stdin.on("error", () => resolve4(null));
|
|
4801
|
+
});
|
|
4802
|
+
}
|
|
4803
|
+
};
|
|
4804
|
+
|
|
4805
|
+
// src/cli/gate-command.ts
|
|
4806
|
+
import { join as join30 } from "path";
|
|
4807
|
+
var RANK_ALL2 = 1e5;
|
|
4808
|
+
var GateCommand = class {
|
|
4809
|
+
ctxoRoot;
|
|
4810
|
+
constructor(projectRoot) {
|
|
4811
|
+
this.ctxoRoot = join30(projectRoot, ".ctxo");
|
|
4812
|
+
}
|
|
4813
|
+
preview(options = {}) {
|
|
4814
|
+
const gate = loadConfig(this.ctxoRoot).config.gate;
|
|
4815
|
+
const thresholds = resolveGateThresholds(gate);
|
|
4816
|
+
const { graph } = loadGraph(this.ctxoRoot);
|
|
4817
|
+
const pr = new PageRankCalculator().calculate(graph, { limit: RANK_ALL2 });
|
|
4818
|
+
const prMap = new Map(pr.rankings.map((r) => [r.symbolId, r.score]));
|
|
4819
|
+
const cutoff = percentileCutoff([...prMap.values()], thresholds.percentile);
|
|
4820
|
+
const calc = new BlastRadiusCalculator();
|
|
4821
|
+
const firing = [];
|
|
4822
|
+
for (const [symbolId, importance] of prMap) {
|
|
4823
|
+
if (importance < cutoff) continue;
|
|
4824
|
+
const blast = calc.calculate(graph, symbolId);
|
|
4825
|
+
const riskCount = blast.confirmedCount + blast.likelyCount;
|
|
4826
|
+
if (shouldBlock({ riskCount, importanceScore: importance, importanceCutoff: cutoff }, thresholds)) {
|
|
4827
|
+
firing.push({ symbolId, riskCount, importance: Math.round(importance * 1e4) / 1e4 });
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
firing.sort((a, b) => b.riskCount - a.riskCount);
|
|
4831
|
+
const payload = {
|
|
4832
|
+
sensitivity: gate?.sensitivity ?? "balanced",
|
|
4833
|
+
thresholds,
|
|
4834
|
+
totalSymbols: graph.nodeCount,
|
|
4835
|
+
wouldFireCount: firing.length,
|
|
4836
|
+
symbols: firing.slice(0, 50)
|
|
4837
|
+
};
|
|
4838
|
+
if (options.json) {
|
|
4839
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
console.error(`[ctxo] gate preview (${payload.sensitivity}): ${firing.length}/${graph.nodeCount} symbols would gate.`);
|
|
4843
|
+
for (const f of firing.slice(0, 25)) console.error(` ${f.riskCount} dep ${f.symbolId}`);
|
|
4844
|
+
}
|
|
4845
|
+
};
|
|
4846
|
+
|
|
4272
4847
|
// src/cli/cli-router.ts
|
|
4273
4848
|
function getVersion() {
|
|
4274
4849
|
let dir = import.meta.dirname;
|
|
4275
4850
|
for (let i = 0; i < 10; i++) {
|
|
4276
|
-
const pkg =
|
|
4277
|
-
if (
|
|
4851
|
+
const pkg = join31(dir, "package.json");
|
|
4852
|
+
if (existsSync27(pkg)) {
|
|
4278
4853
|
try {
|
|
4279
|
-
const json = JSON.parse(
|
|
4854
|
+
const json = JSON.parse(readFileSync17(pkg, "utf-8"));
|
|
4280
4855
|
return json.version ?? "unknown";
|
|
4281
4856
|
} catch {
|
|
4282
4857
|
break;
|
|
4283
4858
|
}
|
|
4284
4859
|
}
|
|
4285
|
-
const parent =
|
|
4860
|
+
const parent = join31(dir, "..");
|
|
4286
4861
|
if (parent === dir) break;
|
|
4287
4862
|
dir = parent;
|
|
4288
4863
|
}
|
|
@@ -4291,8 +4866,8 @@ function getVersion() {
|
|
|
4291
4866
|
function locateGitRoot(startDir) {
|
|
4292
4867
|
let dir = startDir;
|
|
4293
4868
|
for (let i = 0; i < 12; i++) {
|
|
4294
|
-
if (
|
|
4295
|
-
const parent =
|
|
4869
|
+
if (existsSync27(join31(dir, ".git"))) return dir;
|
|
4870
|
+
const parent = dirname9(dir);
|
|
4296
4871
|
if (parent === dir) break;
|
|
4297
4872
|
dir = parent;
|
|
4298
4873
|
}
|
|
@@ -4409,6 +4984,8 @@ var CliRouter = class {
|
|
|
4409
4984
|
else if (a === "--global" || a === "-g") flagValues["global"] = true;
|
|
4410
4985
|
else if (a === "--dry-run") flagValues["dryRun"] = true;
|
|
4411
4986
|
else if (a === "--force") flagValues["force"] = true;
|
|
4987
|
+
else if (a === "--full-tier") flagValues["fullTier"] = true;
|
|
4988
|
+
else if (a === "--syntax-only") flagValues["syntaxOnly"] = true;
|
|
4412
4989
|
else if (a === "--pm") {
|
|
4413
4990
|
const next = args[++i];
|
|
4414
4991
|
if (!next) {
|
|
@@ -4439,6 +5016,8 @@ var CliRouter = class {
|
|
|
4439
5016
|
global: flagValues["global"] === true,
|
|
4440
5017
|
dryRun: flagValues["dryRun"] === true,
|
|
4441
5018
|
force: flagValues["force"] === true,
|
|
5019
|
+
fullTier: flagValues["fullTier"] === true,
|
|
5020
|
+
syntaxOnly: flagValues["syntaxOnly"] === true,
|
|
4442
5021
|
pm: typeof flagValues["pm"] === "string" ? flagValues["pm"] : void 0,
|
|
4443
5022
|
version: typeof flagValues["version"] === "string" ? flagValues["version"] : void 0
|
|
4444
5023
|
});
|
|
@@ -4481,6 +5060,28 @@ var CliRouter = class {
|
|
|
4481
5060
|
});
|
|
4482
5061
|
break;
|
|
4483
5062
|
}
|
|
5063
|
+
case "gate": {
|
|
5064
|
+
if (args.includes("--preview")) {
|
|
5065
|
+
new GateCommand(this.projectRoot).preview({ json: args.includes("--json") });
|
|
5066
|
+
} else {
|
|
5067
|
+
console.error("[ctxo] usage: ctxo gate --preview [--json]");
|
|
5068
|
+
process.exit(1);
|
|
5069
|
+
}
|
|
5070
|
+
break;
|
|
5071
|
+
}
|
|
5072
|
+
case "gate-hook":
|
|
5073
|
+
await new GateHookCommand(this.projectRoot).run();
|
|
5074
|
+
break;
|
|
5075
|
+
case "blast-radius": {
|
|
5076
|
+
const symbolId = args[1];
|
|
5077
|
+
if (!symbolId || symbolId.startsWith("--")) {
|
|
5078
|
+
console.error("[ctxo] usage: ctxo blast-radius <symbolId> [--json]");
|
|
5079
|
+
process.exit(1);
|
|
5080
|
+
return;
|
|
5081
|
+
}
|
|
5082
|
+
new BlastRadiusCommand(this.projectRoot).run({ symbolId, json: args.includes("--json") });
|
|
5083
|
+
break;
|
|
5084
|
+
}
|
|
4484
5085
|
case "visualize": {
|
|
4485
5086
|
const outputIdx = args.indexOf("--output");
|
|
4486
5087
|
const outputArg = outputIdx !== -1 ? args[outputIdx + 1] : void 0;
|
|
@@ -4535,6 +5136,9 @@ Usage:
|
|
|
4535
5136
|
ctxo update --json Machine-readable output
|
|
4536
5137
|
ctxo update --pm <pm> Override package manager (npm|pnpm|yarn|bun)
|
|
4537
5138
|
ctxo update --force Bypass CI lockfile guard
|
|
5139
|
+
ctxo blast-radius <symbolId> Blast radius for one symbol (--json for machine output)
|
|
5140
|
+
ctxo gate --preview Preview which symbols the safe-edit guard would block
|
|
5141
|
+
ctxo gate --preview --json Same, machine-readable output
|
|
4538
5142
|
ctxo --version Print core version
|
|
4539
5143
|
ctxo --version --verbose Print core + plugins + runtime
|
|
4540
5144
|
ctxo --version --json Machine-readable version payload
|
|
@@ -4548,4 +5152,4 @@ export {
|
|
|
4548
5152
|
getVersion,
|
|
4549
5153
|
locateGitRoot
|
|
4550
5154
|
};
|
|
4551
|
-
//# sourceMappingURL=cli-router-
|
|
5155
|
+
//# sourceMappingURL=cli-router-NFAGLJ46.js.map
|