@ctxo/cli 0.0.1 → 0.7.0-alpha.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/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/chunk-7OCVIDC7.js +12 -0
- package/dist/chunk-7OCVIDC7.js.map +1 -0
- package/dist/chunk-I4TUA7CD.js +148 -0
- package/dist/chunk-I4TUA7CD.js.map +1 -0
- package/dist/chunk-O3L56NY6.js +1175 -0
- package/dist/chunk-O3L56NY6.js.map +1 -0
- package/dist/chunk-PSKDDLSF.js +40 -0
- package/dist/chunk-PSKDDLSF.js.map +1 -0
- package/dist/cli-router-RA4EBMVV.js +3330 -0
- package/dist/cli-router-RA4EBMVV.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2723 -0
- package/dist/index.js.map +1 -0
- package/dist/json-index-reader-NNKOVCOG.js +9 -0
- package/dist/json-index-reader-NNKOVCOG.js.map +1 -0
- package/dist/staleness-detector-HJH52U5W.js +9 -0
- package/dist/staleness-detector-HJH52U5W.js.map +1 -0
- package/llms-full.txt +284 -0
- package/llms.txt +72 -0
- package/package.json +65 -13
- package/pages/visualize-template.html +787 -0
|
@@ -0,0 +1,3330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ArchitecturalOverlay,
|
|
4
|
+
DeadCodeDetector,
|
|
5
|
+
PageRankCalculator,
|
|
6
|
+
RevertDetector,
|
|
7
|
+
SessionRecorderAdapter,
|
|
8
|
+
SimpleGitAdapter,
|
|
9
|
+
SqliteStorageAdapter,
|
|
10
|
+
SymbolGraph,
|
|
11
|
+
aggregateCoChanges,
|
|
12
|
+
createLogger,
|
|
13
|
+
detectWorkspace
|
|
14
|
+
} from "./chunk-O3L56NY6.js";
|
|
15
|
+
import {
|
|
16
|
+
JsonIndexReader
|
|
17
|
+
} from "./chunk-I4TUA7CD.js";
|
|
18
|
+
import {
|
|
19
|
+
StalenessDetector
|
|
20
|
+
} from "./chunk-PSKDDLSF.js";
|
|
21
|
+
import {
|
|
22
|
+
__require
|
|
23
|
+
} from "./chunk-7OCVIDC7.js";
|
|
24
|
+
|
|
25
|
+
// src/cli/cli-router.ts
|
|
26
|
+
import { readFileSync as readFileSync13, existsSync as existsSync21 } from "fs";
|
|
27
|
+
import { dirname as dirname8, join as join23 } from "path";
|
|
28
|
+
|
|
29
|
+
// src/cli/index-command.ts
|
|
30
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
31
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync8, statSync, readdirSync as readdirSync2 } from "fs";
|
|
32
|
+
import { join as join7, relative, extname as extname3 } from "path";
|
|
33
|
+
import { createRequire } from "module";
|
|
34
|
+
|
|
35
|
+
// src/core/staleness/content-hasher.ts
|
|
36
|
+
import { createHash } from "crypto";
|
|
37
|
+
var ContentHasher = class {
|
|
38
|
+
hash(content) {
|
|
39
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/adapters/language/language-adapter-registry.ts
|
|
44
|
+
import { extname } from "path";
|
|
45
|
+
var LanguageAdapterRegistry = class {
|
|
46
|
+
adaptersByExtension = /* @__PURE__ */ new Map();
|
|
47
|
+
register(extensions, adapter) {
|
|
48
|
+
for (const ext of extensions) {
|
|
49
|
+
this.adaptersByExtension.set(ext.toLowerCase(), adapter);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getSupportedExtensions() {
|
|
53
|
+
return new Set(this.adaptersByExtension.keys());
|
|
54
|
+
}
|
|
55
|
+
getAdapter(filePath) {
|
|
56
|
+
if (!filePath) return void 0;
|
|
57
|
+
const ext = extname(filePath).toLowerCase();
|
|
58
|
+
if (!ext) return void 0;
|
|
59
|
+
return this.adaptersByExtension.get(ext);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/cli/plugin-loader.ts
|
|
64
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
65
|
+
import { dirname as dirname2, join } from "path";
|
|
66
|
+
|
|
67
|
+
// src/adapters/language/plugin-discovery.ts
|
|
68
|
+
import { existsSync, readFileSync } from "fs";
|
|
69
|
+
import { dirname, isAbsolute, resolve } from "path";
|
|
70
|
+
import { pathToFileURL } from "url";
|
|
71
|
+
import { SUPPORTED_API_VERSION } from "@ctxo/plugin-api";
|
|
72
|
+
var log = createLogger("ctxo:plugin-discovery");
|
|
73
|
+
var OFFICIAL_PREFIX = "@ctxo/lang-";
|
|
74
|
+
var COMMUNITY_PREFIX = "ctxo-lang-";
|
|
75
|
+
function readManifestDeps(manifestPath) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
78
|
+
const manifest = JSON.parse(raw);
|
|
79
|
+
const all = {
|
|
80
|
+
...manifest.dependencies,
|
|
81
|
+
...manifest.devDependencies,
|
|
82
|
+
...manifest.peerDependencies
|
|
83
|
+
};
|
|
84
|
+
return Object.keys(all).filter(
|
|
85
|
+
(name) => name.startsWith(OFFICIAL_PREFIX) || name.startsWith(COMMUNITY_PREFIX)
|
|
86
|
+
);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
log.warn(`Failed to read manifest ${manifestPath}: ${err.message}`);
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isLocalSpecifier(spec) {
|
|
93
|
+
return spec.startsWith(".") || spec.startsWith("/") || isAbsolute(spec);
|
|
94
|
+
}
|
|
95
|
+
async function tryLoad(specifier, baseDir) {
|
|
96
|
+
try {
|
|
97
|
+
const target = isLocalSpecifier(specifier) ? pathToFileURL(resolve(baseDir, specifier)).href : specifier;
|
|
98
|
+
const mod = await import(target);
|
|
99
|
+
const candidate = mod.default ?? mod.plugin ?? mod;
|
|
100
|
+
if (!candidate || typeof candidate !== "object") {
|
|
101
|
+
return { specifier, reason: "module export is not an object" };
|
|
102
|
+
}
|
|
103
|
+
if (candidate.apiVersion !== SUPPORTED_API_VERSION) {
|
|
104
|
+
return {
|
|
105
|
+
specifier,
|
|
106
|
+
reason: `unsupported apiVersion ${String(candidate.apiVersion)} (want '${SUPPORTED_API_VERSION}')`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (typeof candidate.createAdapter !== "function") {
|
|
110
|
+
return { specifier, reason: "missing createAdapter()" };
|
|
111
|
+
}
|
|
112
|
+
if (typeof candidate.id !== "string" || candidate.id.length === 0) {
|
|
113
|
+
return { specifier, reason: "missing or empty id" };
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(candidate.extensions)) {
|
|
116
|
+
return { specifier, reason: "extensions must be an array" };
|
|
117
|
+
}
|
|
118
|
+
if (candidate.tier !== "syntax" && candidate.tier !== "full") {
|
|
119
|
+
return { specifier, reason: `invalid tier ${String(candidate.tier)}` };
|
|
120
|
+
}
|
|
121
|
+
return candidate;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return { specifier, reason: err.message };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function discoverPlugins(options) {
|
|
127
|
+
const { manifestPath } = options;
|
|
128
|
+
if (!existsSync(manifestPath)) {
|
|
129
|
+
log.info(`Manifest not found at ${manifestPath} \u2014 no plugins discovered`);
|
|
130
|
+
return { plugins: [], failures: [] };
|
|
131
|
+
}
|
|
132
|
+
const baseDir = dirname(manifestPath);
|
|
133
|
+
const specifiers = options.explicit ?? readManifestDeps(manifestPath);
|
|
134
|
+
const plugins = [];
|
|
135
|
+
const failures = [];
|
|
136
|
+
for (const spec of specifiers) {
|
|
137
|
+
const result = await tryLoad(spec, baseDir);
|
|
138
|
+
if ("apiVersion" in result) {
|
|
139
|
+
plugins.push({ plugin: result, specifier: spec });
|
|
140
|
+
} else {
|
|
141
|
+
failures.push(result);
|
|
142
|
+
log.warn(`Skipped plugin ${spec}: ${result.reason}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { plugins, failures };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/cli/plugin-loader.ts
|
|
149
|
+
var log2 = createLogger("ctxo:plugin-loader");
|
|
150
|
+
function manifestHasPlugins(path) {
|
|
151
|
+
try {
|
|
152
|
+
const raw = JSON.parse(readFileSync2(path, "utf-8"));
|
|
153
|
+
const all = { ...raw.dependencies, ...raw.devDependencies, ...raw.peerDependencies };
|
|
154
|
+
return Object.keys(all).some((n) => n.startsWith("@ctxo/lang-") || n.startsWith("ctxo-lang-"));
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function loadManifestPath(projectRoot) {
|
|
160
|
+
return resolveManifestPath(projectRoot);
|
|
161
|
+
}
|
|
162
|
+
function resolveManifestPath(projectRoot) {
|
|
163
|
+
const projectManifest = join(projectRoot, "package.json");
|
|
164
|
+
if (existsSync2(projectManifest) && manifestHasPlugins(projectManifest)) {
|
|
165
|
+
return projectManifest;
|
|
166
|
+
}
|
|
167
|
+
let dir = import.meta.dirname;
|
|
168
|
+
for (let i = 0; i < 12; i++) {
|
|
169
|
+
const candidate = join(dir, "package.json");
|
|
170
|
+
if (existsSync2(candidate) && manifestHasPlugins(candidate)) return candidate;
|
|
171
|
+
const parent = dirname2(dir);
|
|
172
|
+
if (parent === dir) break;
|
|
173
|
+
dir = parent;
|
|
174
|
+
}
|
|
175
|
+
return existsSync2(projectManifest) ? projectManifest : null;
|
|
176
|
+
}
|
|
177
|
+
function buildContext(projectRoot, workspace, pluginId) {
|
|
178
|
+
const pluginLog = createLogger(`ctxo:plugin:${pluginId}`);
|
|
179
|
+
return {
|
|
180
|
+
logger: {
|
|
181
|
+
debug: (msg, ...args) => pluginLog.info(String(msg), ...args),
|
|
182
|
+
info: (msg, ...args) => pluginLog.info(String(msg), ...args),
|
|
183
|
+
warn: (msg, ...args) => pluginLog.warn(String(msg), ...args),
|
|
184
|
+
error: (msg, ...args) => pluginLog.error(String(msg), ...args)
|
|
185
|
+
},
|
|
186
|
+
projectRoot,
|
|
187
|
+
workspace,
|
|
188
|
+
config: {}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function loadPlugins(projectRoot) {
|
|
192
|
+
const manifestPath = resolveManifestPath(projectRoot);
|
|
193
|
+
if (!manifestPath) {
|
|
194
|
+
log2.info("No package.json found for plugin discovery");
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const workspace = detectWorkspace(projectRoot);
|
|
198
|
+
const result = await discoverPlugins({ manifestPath });
|
|
199
|
+
const loaded = [];
|
|
200
|
+
for (const { plugin, specifier } of result.plugins) {
|
|
201
|
+
try {
|
|
202
|
+
const ctx = buildContext(projectRoot, workspace, plugin.id);
|
|
203
|
+
const adapter = plugin.createAdapter(ctx);
|
|
204
|
+
loaded.push({ plugin, adapter, specifier });
|
|
205
|
+
} catch (err) {
|
|
206
|
+
log2.warn(`Plugin ${plugin.id} createAdapter failed: ${err.message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return loaded;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/cli/install-command.ts
|
|
213
|
+
import { existsSync as existsSync5 } from "fs";
|
|
214
|
+
import { spawn } from "child_process";
|
|
215
|
+
import { join as join4 } from "path";
|
|
216
|
+
|
|
217
|
+
// src/core/detection/detect-languages.ts
|
|
218
|
+
import { execFileSync } from "child_process";
|
|
219
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
220
|
+
import { extname as extname2, join as join2 } from "path";
|
|
221
|
+
var KNOWN_LANGUAGES = [
|
|
222
|
+
"typescript",
|
|
223
|
+
"javascript",
|
|
224
|
+
"go",
|
|
225
|
+
"csharp",
|
|
226
|
+
"python",
|
|
227
|
+
"java",
|
|
228
|
+
"rust",
|
|
229
|
+
"ruby",
|
|
230
|
+
"kotlin"
|
|
231
|
+
];
|
|
232
|
+
var EXTENSION_LANGUAGE = {
|
|
233
|
+
".ts": "typescript",
|
|
234
|
+
".tsx": "typescript",
|
|
235
|
+
".js": "javascript",
|
|
236
|
+
".jsx": "javascript",
|
|
237
|
+
".go": "go",
|
|
238
|
+
".cs": "csharp",
|
|
239
|
+
".py": "python",
|
|
240
|
+
".pyi": "python",
|
|
241
|
+
".java": "java",
|
|
242
|
+
".kt": "kotlin",
|
|
243
|
+
".kts": "kotlin",
|
|
244
|
+
".rs": "rust",
|
|
245
|
+
".rb": "ruby"
|
|
246
|
+
};
|
|
247
|
+
var MANIFEST_PROBES = [
|
|
248
|
+
{
|
|
249
|
+
id: "typescript",
|
|
250
|
+
check: (root) => existsSync3(join2(root, "tsconfig.json")) ? "tsconfig.json" : null
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: "python",
|
|
254
|
+
check: (root) => {
|
|
255
|
+
if (existsSync3(join2(root, "pyproject.toml"))) return "pyproject.toml";
|
|
256
|
+
if (existsSync3(join2(root, "setup.py"))) return "setup.py";
|
|
257
|
+
if (existsSync3(join2(root, "requirements.txt"))) return "requirements.txt";
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "java",
|
|
263
|
+
check: (root) => {
|
|
264
|
+
if (existsSync3(join2(root, "pom.xml"))) return "pom.xml";
|
|
265
|
+
if (existsSync3(join2(root, "build.gradle"))) return "build.gradle";
|
|
266
|
+
if (existsSync3(join2(root, "build.gradle.kts"))) return "build.gradle.kts";
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "go",
|
|
272
|
+
check: (root) => existsSync3(join2(root, "go.mod")) ? "go.mod" : null
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "csharp",
|
|
276
|
+
check: (root) => {
|
|
277
|
+
try {
|
|
278
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
279
|
+
if (entry.isFile() && (entry.name.endsWith(".csproj") || entry.name.endsWith(".sln"))) {
|
|
280
|
+
return entry.name;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "rust",
|
|
290
|
+
check: (root) => existsSync3(join2(root, "Cargo.toml")) ? "Cargo.toml" : null
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: "ruby",
|
|
294
|
+
check: (root) => existsSync3(join2(root, "Gemfile")) ? "Gemfile" : null
|
|
295
|
+
}
|
|
296
|
+
];
|
|
297
|
+
function listGitFiles(root) {
|
|
298
|
+
try {
|
|
299
|
+
const out = execFileSync(
|
|
300
|
+
"git",
|
|
301
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
302
|
+
{ cwd: root, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
303
|
+
);
|
|
304
|
+
return out.split("\n").filter(Boolean);
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function detectLanguages(root) {
|
|
310
|
+
const byManifest = {};
|
|
311
|
+
for (const probe of MANIFEST_PROBES) {
|
|
312
|
+
const manifest = probe.check(root);
|
|
313
|
+
if (manifest) byManifest[probe.id] = manifest;
|
|
314
|
+
}
|
|
315
|
+
const byExtension = {};
|
|
316
|
+
const unknownExtensions = {};
|
|
317
|
+
let totalFiles = 0;
|
|
318
|
+
for (const file of listGitFiles(root)) {
|
|
319
|
+
const ext = extname2(file).toLowerCase();
|
|
320
|
+
if (!ext) continue;
|
|
321
|
+
totalFiles += 1;
|
|
322
|
+
const lang = EXTENSION_LANGUAGE[ext];
|
|
323
|
+
if (lang) {
|
|
324
|
+
byExtension[lang] = (byExtension[lang] ?? 0) + 1;
|
|
325
|
+
} else {
|
|
326
|
+
unknownExtensions[ext] = (unknownExtensions[ext] ?? 0) + 1;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { byManifest, byExtension, totalFiles, unknownExtensions };
|
|
330
|
+
}
|
|
331
|
+
function decideNeededLanguages(result, options = {}) {
|
|
332
|
+
const minFiles = options.minFiles ?? 5;
|
|
333
|
+
const minRatio = options.minRatio ?? 0.02;
|
|
334
|
+
const needed = /* @__PURE__ */ new Set();
|
|
335
|
+
for (const id of Object.keys(result.byManifest)) {
|
|
336
|
+
needed.add(id);
|
|
337
|
+
}
|
|
338
|
+
for (const [id, count] of Object.entries(result.byExtension)) {
|
|
339
|
+
if (result.byManifest[id]) continue;
|
|
340
|
+
const ratio = result.totalFiles > 0 ? count / result.totalFiles : 0;
|
|
341
|
+
if (count >= minFiles && ratio >= minRatio) {
|
|
342
|
+
needed.add(id);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return [...needed];
|
|
346
|
+
}
|
|
347
|
+
function officialPluginFor(language) {
|
|
348
|
+
return `@ctxo/lang-${language}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/core/install/package-manager.ts
|
|
352
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
353
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
354
|
+
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
355
|
+
function isPackageManager(value) {
|
|
356
|
+
return PACKAGE_MANAGERS.includes(value);
|
|
357
|
+
}
|
|
358
|
+
function resolvePackageManager(options) {
|
|
359
|
+
const env = options.env ?? process.env;
|
|
360
|
+
if (options.flag) {
|
|
361
|
+
if (!isPackageManager(options.flag)) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Unknown --pm value "${options.flag}". Supported: ${PACKAGE_MANAGERS.join(", ")}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return { manager: options.flag, source: "flag" };
|
|
367
|
+
}
|
|
368
|
+
const envValue = env["CTXO_PM"];
|
|
369
|
+
if (envValue) {
|
|
370
|
+
if (!isPackageManager(envValue)) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`CTXO_PM="${envValue}" is not a supported package manager. Expected one of: ${PACKAGE_MANAGERS.join(", ")}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return { manager: envValue, source: "env" };
|
|
376
|
+
}
|
|
377
|
+
const configured = readConfigPackageManager(options.projectRoot);
|
|
378
|
+
if (configured) return { manager: configured, source: "config" };
|
|
379
|
+
const corepack = readPackageManagerField(options.projectRoot);
|
|
380
|
+
if (corepack) return { manager: corepack.manager, source: "config", detail: corepack.detail };
|
|
381
|
+
const lockHit = detectFromLockfileWalkUp(options.projectRoot);
|
|
382
|
+
if (lockHit) return { manager: lockHit.manager, source: "lockfile", detail: lockHit.detail };
|
|
383
|
+
return { manager: "npm", source: "default" };
|
|
384
|
+
}
|
|
385
|
+
function readPackageManagerField(startDir) {
|
|
386
|
+
let dir = startDir;
|
|
387
|
+
for (let i = 0; i < 12; i++) {
|
|
388
|
+
const pkg = join3(dir, "package.json");
|
|
389
|
+
if (existsSync4(pkg)) {
|
|
390
|
+
try {
|
|
391
|
+
const parsed = JSON.parse(readFileSync3(pkg, "utf-8"));
|
|
392
|
+
const value = parsed.packageManager;
|
|
393
|
+
if (typeof value === "string") {
|
|
394
|
+
const [nameRaw] = value.split("@");
|
|
395
|
+
const name = (nameRaw ?? "").trim();
|
|
396
|
+
if (isPackageManager(name)) {
|
|
397
|
+
return { manager: name, detail: `packageManager: ${value}` };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const parent = dirname3(dir);
|
|
404
|
+
if (parent === dir) break;
|
|
405
|
+
dir = parent;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
function readConfigPackageManager(projectRoot) {
|
|
410
|
+
const path = join3(projectRoot, ".ctxo", "config.yaml");
|
|
411
|
+
if (!existsSync4(path)) return null;
|
|
412
|
+
try {
|
|
413
|
+
const raw = readFileSync3(path, "utf-8");
|
|
414
|
+
const match = raw.match(/^packageManager:\s*(\S+)\s*$/m);
|
|
415
|
+
if (!match) return null;
|
|
416
|
+
const value = match[1];
|
|
417
|
+
return isPackageManager(value) ? value : null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function detectFromLockfileWalkUp(startDir) {
|
|
423
|
+
const probes = [
|
|
424
|
+
{ file: "bun.lockb", manager: "bun" },
|
|
425
|
+
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
426
|
+
{ file: "pnpm-workspace.yaml", manager: "pnpm" },
|
|
427
|
+
{ file: "yarn.lock", manager: "yarn" },
|
|
428
|
+
{ file: "package-lock.json", manager: "npm" }
|
|
429
|
+
];
|
|
430
|
+
let dir = startDir;
|
|
431
|
+
for (let i = 0; i < 12; i++) {
|
|
432
|
+
for (const probe of probes) {
|
|
433
|
+
const path = join3(dir, probe.file);
|
|
434
|
+
if (existsSync4(path)) {
|
|
435
|
+
return { manager: probe.manager, detail: probe.file };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const parent = dirname3(dir);
|
|
439
|
+
if (parent === dir) break;
|
|
440
|
+
dir = parent;
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
function buildInstallCommand(manager, packages, options = {}) {
|
|
445
|
+
if (packages.length === 0) {
|
|
446
|
+
throw new Error("buildInstallCommand requires at least one package");
|
|
447
|
+
}
|
|
448
|
+
switch (manager) {
|
|
449
|
+
case "npm":
|
|
450
|
+
return {
|
|
451
|
+
command: "npm",
|
|
452
|
+
args: options.global ? ["install", "-g", ...packages] : ["install", "-D", ...packages]
|
|
453
|
+
};
|
|
454
|
+
case "pnpm":
|
|
455
|
+
return {
|
|
456
|
+
command: "pnpm",
|
|
457
|
+
args: options.global ? ["add", "-g", ...packages] : ["add", "-D", ...packages]
|
|
458
|
+
};
|
|
459
|
+
case "yarn":
|
|
460
|
+
return {
|
|
461
|
+
command: "yarn",
|
|
462
|
+
args: options.global ? ["global", "add", ...packages] : ["add", "--dev", ...packages]
|
|
463
|
+
};
|
|
464
|
+
case "bun":
|
|
465
|
+
return {
|
|
466
|
+
command: "bun",
|
|
467
|
+
args: options.global ? ["add", "-g", ...packages] : ["add", "-d", ...packages]
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/cli/install-command.ts
|
|
473
|
+
var InstallCommand = class {
|
|
474
|
+
constructor(projectRoot) {
|
|
475
|
+
this.projectRoot = projectRoot;
|
|
476
|
+
}
|
|
477
|
+
projectRoot;
|
|
478
|
+
async run(options = {}) {
|
|
479
|
+
const resolvedLangs = this.resolveLanguages(options);
|
|
480
|
+
if (resolvedLangs.length === 0) {
|
|
481
|
+
console.error('[ctxo] Nothing to install. Specify a language (e.g. "ctxo install python") or add one to your project.');
|
|
482
|
+
process.exitCode = 0;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
let resolution;
|
|
486
|
+
try {
|
|
487
|
+
resolution = resolvePackageManager({
|
|
488
|
+
flag: options.pm,
|
|
489
|
+
projectRoot: this.projectRoot
|
|
490
|
+
});
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.error(`[ctxo] ${err.message}`);
|
|
493
|
+
process.exitCode = 1;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (!options.global && !existsSync5(join4(this.projectRoot, "package.json"))) {
|
|
497
|
+
console.error("[ctxo] No package.json in the current project. Re-run with --global or create a package.json first.");
|
|
498
|
+
process.exitCode = 1;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!options.force && !options.global && isLockedCI(process.env)) {
|
|
502
|
+
console.error("[ctxo] CI environment detected with a frozen lockfile. Refusing to mutate dependencies. Use --force or switch to --global.");
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const specifiers = resolvedLangs.map(
|
|
507
|
+
(lang) => options.version ? `${officialPluginFor(lang)}@${options.version}` : officialPluginFor(lang)
|
|
508
|
+
);
|
|
509
|
+
const invocation = buildInstallCommand(resolution.manager, specifiers, {
|
|
510
|
+
global: options.global
|
|
511
|
+
});
|
|
512
|
+
const plan = {
|
|
513
|
+
languages: resolvedLangs,
|
|
514
|
+
packages: specifiers,
|
|
515
|
+
manager: resolution.manager,
|
|
516
|
+
managerSource: resolution.source,
|
|
517
|
+
global: options.global ?? false,
|
|
518
|
+
command: invocation.command,
|
|
519
|
+
args: [...invocation.args]
|
|
520
|
+
};
|
|
521
|
+
console.error(`[ctxo] Plan: install ${plan.packages.join(", ")}`);
|
|
522
|
+
console.error(`[ctxo] Using ${plan.manager} (${plan.managerSource}${resolution.detail ? `: ${resolution.detail}` : ""})${plan.global ? ", global" : ""}`);
|
|
523
|
+
console.error(`[ctxo] Command: ${plan.command} ${plan.args.join(" ")}`);
|
|
524
|
+
if (options.dryRun) {
|
|
525
|
+
console.error("[ctxo] --dry-run set; not executing.");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const code = await spawnInstall(plan.command, plan.args, this.projectRoot);
|
|
529
|
+
if (code !== 0) {
|
|
530
|
+
console.error(`[ctxo] ${plan.command} exited with code ${code}`);
|
|
531
|
+
process.exitCode = code;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
console.error(`[ctxo] Installed ${plan.packages.join(", ")}`);
|
|
535
|
+
}
|
|
536
|
+
resolveLanguages(options) {
|
|
537
|
+
const requested = options.languages ?? [];
|
|
538
|
+
if (requested.length > 0) {
|
|
539
|
+
const out = [];
|
|
540
|
+
for (const raw of requested) {
|
|
541
|
+
const lang = normalizeLanguage(raw);
|
|
542
|
+
if (!lang) {
|
|
543
|
+
console.error(
|
|
544
|
+
`[ctxo] Unknown language "${raw}". Known: ${KNOWN_LANGUAGES.join(", ")}. Community plugins: see the ctxo-lang-template scaffolding guide.`
|
|
545
|
+
);
|
|
546
|
+
process.exitCode = 1;
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
if (!out.includes(lang)) out.push(lang);
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
const detection = detectLanguages(this.projectRoot);
|
|
554
|
+
const needed = decideNeededLanguages(detection);
|
|
555
|
+
if (needed.length === 0) return [];
|
|
556
|
+
const missing = needed.filter((lang) => !isPluginInstalled(lang));
|
|
557
|
+
if (missing.length === 0) {
|
|
558
|
+
console.error("[ctxo] All detected languages already have plugins installed.");
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
console.error(`[ctxo] Detected missing plugins for: ${missing.join(", ")}`);
|
|
562
|
+
return missing;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
function normalizeLanguage(input) {
|
|
566
|
+
const lowered = input.toLowerCase();
|
|
567
|
+
if (KNOWN_LANGUAGES.includes(lowered)) {
|
|
568
|
+
return lowered;
|
|
569
|
+
}
|
|
570
|
+
const aliases = {
|
|
571
|
+
ts: "typescript",
|
|
572
|
+
js: "javascript",
|
|
573
|
+
py: "python",
|
|
574
|
+
"c#": "csharp",
|
|
575
|
+
"cs": "csharp",
|
|
576
|
+
"net": "csharp",
|
|
577
|
+
golang: "go",
|
|
578
|
+
kt: "kotlin",
|
|
579
|
+
rb: "ruby",
|
|
580
|
+
rs: "rust"
|
|
581
|
+
};
|
|
582
|
+
return aliases[lowered] ?? null;
|
|
583
|
+
}
|
|
584
|
+
function isPluginInstalled(lang) {
|
|
585
|
+
try {
|
|
586
|
+
const { createRequire: createRequire5 } = __require("module");
|
|
587
|
+
const r = createRequire5(import.meta.url);
|
|
588
|
+
r.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
589
|
+
return true;
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function isLockedCI(env) {
|
|
595
|
+
if (env["CI"] !== "true" && env["CI"] !== "1") return false;
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
function spawnInstall(command, args, cwd) {
|
|
599
|
+
return new Promise((resolve3, reject) => {
|
|
600
|
+
const child = spawn(command, [...args], { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
601
|
+
child.on("exit", (code) => resolve3(code ?? 0));
|
|
602
|
+
child.on("error", (err) => reject(err));
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/adapters/storage/json-index-writer.ts
|
|
607
|
+
import { writeFileSync, renameSync, mkdirSync, unlinkSync, existsSync as existsSync6 } from "fs";
|
|
608
|
+
import { dirname as dirname4, join as join5, resolve as resolve2, sep } from "path";
|
|
609
|
+
var JsonIndexWriter = class {
|
|
610
|
+
indexDir;
|
|
611
|
+
constructor(ctxoRoot) {
|
|
612
|
+
this.indexDir = join5(ctxoRoot, "index");
|
|
613
|
+
}
|
|
614
|
+
write(fileIndex) {
|
|
615
|
+
if (!fileIndex.file) {
|
|
616
|
+
throw new Error("FileIndex.file must not be empty");
|
|
617
|
+
}
|
|
618
|
+
const targetPath = this.resolveIndexPath(fileIndex.file);
|
|
619
|
+
mkdirSync(dirname4(targetPath), { recursive: true });
|
|
620
|
+
const sorted = this.sortKeys(fileIndex);
|
|
621
|
+
const json = JSON.stringify(sorted, null, 2);
|
|
622
|
+
this.atomicWrite(targetPath, json);
|
|
623
|
+
}
|
|
624
|
+
writeCoChanges(matrix) {
|
|
625
|
+
mkdirSync(this.indexDir, { recursive: true });
|
|
626
|
+
const targetPath = join5(this.indexDir, "co-changes.json");
|
|
627
|
+
this.atomicWrite(targetPath, JSON.stringify(matrix, null, 2));
|
|
628
|
+
}
|
|
629
|
+
atomicWrite(targetPath, content) {
|
|
630
|
+
const tmpPath = `${targetPath}.${process.pid}.tmp`;
|
|
631
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
632
|
+
renameSync(tmpPath, targetPath);
|
|
633
|
+
}
|
|
634
|
+
delete(relativePath) {
|
|
635
|
+
const targetPath = this.resolveIndexPath(relativePath);
|
|
636
|
+
if (existsSync6(targetPath)) {
|
|
637
|
+
unlinkSync(targetPath);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
resolveIndexPath(relativePath) {
|
|
641
|
+
const resolved = resolve2(this.indexDir, `${relativePath}.json`);
|
|
642
|
+
const normalizedDir = resolve2(this.indexDir) + sep;
|
|
643
|
+
if (!resolved.startsWith(normalizedDir)) {
|
|
644
|
+
throw new Error(`Path traversal detected: ${relativePath}`);
|
|
645
|
+
}
|
|
646
|
+
return resolved;
|
|
647
|
+
}
|
|
648
|
+
sortKeys(obj) {
|
|
649
|
+
if (Array.isArray(obj)) {
|
|
650
|
+
return obj.map((item) => this.sortKeys(item));
|
|
651
|
+
}
|
|
652
|
+
if (obj !== null && typeof obj === "object") {
|
|
653
|
+
const sorted = {};
|
|
654
|
+
for (const key of Object.keys(obj).sort()) {
|
|
655
|
+
sorted[key] = this.sortKeys(obj[key]);
|
|
656
|
+
}
|
|
657
|
+
return sorted;
|
|
658
|
+
}
|
|
659
|
+
return obj;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// src/adapters/storage/schema-manager.ts
|
|
664
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
665
|
+
import { join as join6, dirname as dirname5 } from "path";
|
|
666
|
+
var CURRENT_SCHEMA_VERSION = "1.0.0";
|
|
667
|
+
var SchemaManager = class {
|
|
668
|
+
versionFilePath;
|
|
669
|
+
constructor(ctxoRoot) {
|
|
670
|
+
this.versionFilePath = join6(ctxoRoot, "index", "schema-version");
|
|
671
|
+
}
|
|
672
|
+
currentVersion() {
|
|
673
|
+
return CURRENT_SCHEMA_VERSION;
|
|
674
|
+
}
|
|
675
|
+
readStoredVersion() {
|
|
676
|
+
if (!existsSync7(this.versionFilePath)) {
|
|
677
|
+
return void 0;
|
|
678
|
+
}
|
|
679
|
+
return readFileSync4(this.versionFilePath, "utf-8").trim();
|
|
680
|
+
}
|
|
681
|
+
writeVersion() {
|
|
682
|
+
mkdirSync2(dirname5(this.versionFilePath), { recursive: true });
|
|
683
|
+
writeFileSync2(this.versionFilePath, CURRENT_SCHEMA_VERSION, "utf-8");
|
|
684
|
+
}
|
|
685
|
+
isCompatible() {
|
|
686
|
+
const stored = this.readStoredVersion();
|
|
687
|
+
if (!stored) return false;
|
|
688
|
+
return stored === CURRENT_SCHEMA_VERSION;
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/cli/index-command.ts
|
|
693
|
+
var requireCjs = createRequire(import.meta.url);
|
|
694
|
+
var IndexCommand = class {
|
|
695
|
+
projectRoot;
|
|
696
|
+
ctxoRoot;
|
|
697
|
+
supportedExtensions;
|
|
698
|
+
constructor(projectRoot, ctxoRoot) {
|
|
699
|
+
this.projectRoot = projectRoot;
|
|
700
|
+
this.ctxoRoot = ctxoRoot ?? join7(projectRoot, ".ctxo");
|
|
701
|
+
this.supportedExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".go", ".cs"]);
|
|
702
|
+
}
|
|
703
|
+
async run(options = {}) {
|
|
704
|
+
if (options.check) {
|
|
705
|
+
return this.runCheck();
|
|
706
|
+
}
|
|
707
|
+
if (options.installMissing) {
|
|
708
|
+
await this.autoInstallMissingPlugins();
|
|
709
|
+
}
|
|
710
|
+
const registry = new LanguageAdapterRegistry();
|
|
711
|
+
const { tsMorphLike, roslynAdapter, csharpTier } = await this.registerDiscoveredPlugins(registry);
|
|
712
|
+
this.supportedExtensions = registry.getSupportedExtensions();
|
|
713
|
+
this.printLanguagePreview();
|
|
714
|
+
const writer = new JsonIndexWriter(this.ctxoRoot);
|
|
715
|
+
const schemaManager = new SchemaManager(this.ctxoRoot);
|
|
716
|
+
const hasher = new ContentHasher();
|
|
717
|
+
const gitAdapter = new SimpleGitAdapter(this.projectRoot);
|
|
718
|
+
const revertDetector = new RevertDetector();
|
|
719
|
+
let files;
|
|
720
|
+
if (options.file) {
|
|
721
|
+
const fullPath = join7(this.projectRoot, options.file);
|
|
722
|
+
files = [fullPath];
|
|
723
|
+
console.error(`[ctxo] Incremental re-index: ${options.file}`);
|
|
724
|
+
} else {
|
|
725
|
+
const workspaces = this.discoverWorkspaces();
|
|
726
|
+
files = [];
|
|
727
|
+
for (const ws of workspaces) {
|
|
728
|
+
const wsFiles = this.discoverFilesIn(ws);
|
|
729
|
+
files.push(...wsFiles);
|
|
730
|
+
}
|
|
731
|
+
console.error(`[ctxo] Building codebase index... Found ${files.length} source files`);
|
|
732
|
+
}
|
|
733
|
+
if (roslynAdapter?.isReady()) {
|
|
734
|
+
const hasCsFiles = files.some((f) => f.endsWith(".cs"));
|
|
735
|
+
if (hasCsFiles) {
|
|
736
|
+
await roslynAdapter.batchIndex();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const symbolRegistry = /* @__PURE__ */ new Map();
|
|
740
|
+
const pendingIndices = [];
|
|
741
|
+
let processed = 0;
|
|
742
|
+
for (const filePath of files) {
|
|
743
|
+
const adapter = registry.getAdapter(filePath);
|
|
744
|
+
if (!adapter) continue;
|
|
745
|
+
const relativePath = relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
746
|
+
try {
|
|
747
|
+
const source = readFileSync5(filePath, "utf-8");
|
|
748
|
+
const lastModified = Math.floor(Date.now() / 1e3);
|
|
749
|
+
const symbols = await adapter.extractSymbols(relativePath, source);
|
|
750
|
+
const complexity = await adapter.extractComplexity(relativePath, source);
|
|
751
|
+
for (const sym of symbols) {
|
|
752
|
+
symbolRegistry.set(sym.symbolId, sym.kind);
|
|
753
|
+
}
|
|
754
|
+
pendingIndices.push({
|
|
755
|
+
relativePath,
|
|
756
|
+
source,
|
|
757
|
+
fileIndex: {
|
|
758
|
+
file: relativePath,
|
|
759
|
+
lastModified,
|
|
760
|
+
contentHash: hasher.hash(source),
|
|
761
|
+
symbols,
|
|
762
|
+
edges: [],
|
|
763
|
+
complexity,
|
|
764
|
+
intent: [],
|
|
765
|
+
antiPatterns: []
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
processed++;
|
|
769
|
+
if (processed % 50 === 0) {
|
|
770
|
+
console.error(`[ctxo] Processed ${processed}/${files.length} files (symbols)`);
|
|
771
|
+
}
|
|
772
|
+
} catch (err) {
|
|
773
|
+
console.error(`[ctxo] Skipped ${relativePath}: ${err.message}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const allSources = /* @__PURE__ */ new Map();
|
|
777
|
+
for (const entry of pendingIndices) {
|
|
778
|
+
allSources.set(entry.relativePath, entry.source);
|
|
779
|
+
}
|
|
780
|
+
tsMorphLike?.loadProjectSources(allSources);
|
|
781
|
+
for (const entry of pendingIndices) {
|
|
782
|
+
const adapter = registry.getAdapter(entry.relativePath);
|
|
783
|
+
if (!adapter) continue;
|
|
784
|
+
try {
|
|
785
|
+
adapter.setSymbolRegistry?.(symbolRegistry);
|
|
786
|
+
entry.fileIndex.edges = await adapter.extractEdges(entry.relativePath, entry.source);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
console.error(`[ctxo] Edge extraction failed for ${entry.relativePath}: ${err.message}`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
tsMorphLike?.clearProjectSources();
|
|
792
|
+
if (!options.skipHistory && pendingIndices.length > 0) {
|
|
793
|
+
const maxHistory = options.maxHistory ?? 20;
|
|
794
|
+
const batchHistory = await gitAdapter.getBatchHistory?.(maxHistory) ?? /* @__PURE__ */ new Map();
|
|
795
|
+
for (const { relativePath, fileIndex } of pendingIndices) {
|
|
796
|
+
const commits = batchHistory.get(relativePath) ?? [];
|
|
797
|
+
fileIndex.intent = commits.map((c) => ({
|
|
798
|
+
hash: c.hash,
|
|
799
|
+
message: c.message,
|
|
800
|
+
date: c.date,
|
|
801
|
+
kind: "commit"
|
|
802
|
+
}));
|
|
803
|
+
fileIndex.antiPatterns = revertDetector.detect(commits);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (!options.skipHistory && pendingIndices.length > 0) {
|
|
807
|
+
const fileIndices = pendingIndices.map((e) => e.fileIndex);
|
|
808
|
+
const coChangeMatrix = aggregateCoChanges(fileIndices);
|
|
809
|
+
writer.writeCoChanges(coChangeMatrix);
|
|
810
|
+
console.error(`[ctxo] Co-change analysis: ${coChangeMatrix.entries.length} file pairs detected`);
|
|
811
|
+
}
|
|
812
|
+
const indices = [];
|
|
813
|
+
for (const { fileIndex } of pendingIndices) {
|
|
814
|
+
writer.write(fileIndex);
|
|
815
|
+
indices.push(fileIndex);
|
|
816
|
+
}
|
|
817
|
+
schemaManager.writeVersion();
|
|
818
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
819
|
+
try {
|
|
820
|
+
await storage.initEmpty();
|
|
821
|
+
storage.bulkWrite(indices);
|
|
822
|
+
} finally {
|
|
823
|
+
storage.close();
|
|
824
|
+
}
|
|
825
|
+
if (!options.skipSideEffects) {
|
|
826
|
+
this.ensureGitignore();
|
|
827
|
+
}
|
|
828
|
+
const csCount = pendingIndices.filter((e) => e.relativePath.endsWith(".cs")).length;
|
|
829
|
+
const tsCount = pendingIndices.filter((e) => /\.(ts|tsx|js|jsx)$/.test(e.relativePath)).length;
|
|
830
|
+
const goCount = pendingIndices.filter((e) => e.relativePath.endsWith(".go")).length;
|
|
831
|
+
console.error(`[ctxo] Index complete: ${processed} files indexed`);
|
|
832
|
+
if (tsCount > 0) console.error(`[ctxo] TypeScript/JS: ${tsCount} files (full tier)`);
|
|
833
|
+
if (csCount > 0) console.error(`[ctxo] C#: ${csCount} files (${csharpTier} tier${csharpTier === "syntax" ? " - .NET SDK 8+ for full analysis" : ""})`);
|
|
834
|
+
if (goCount > 0) console.error(`[ctxo] Go: ${goCount} files (syntax tier)`);
|
|
835
|
+
if (roslynAdapter) await roslynAdapter.dispose();
|
|
836
|
+
}
|
|
837
|
+
async autoInstallMissingPlugins() {
|
|
838
|
+
const detection = detectLanguages(this.projectRoot);
|
|
839
|
+
const needed = decideNeededLanguages(detection);
|
|
840
|
+
const missing = [];
|
|
841
|
+
for (const lang of needed) {
|
|
842
|
+
try {
|
|
843
|
+
requireCjs.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
844
|
+
} catch {
|
|
845
|
+
missing.push(lang);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (missing.length === 0) return;
|
|
849
|
+
console.error(`[ctxo] --install-missing: installing ${missing.join(", ")}`);
|
|
850
|
+
await new InstallCommand(this.projectRoot).run({ languages: missing, yes: true });
|
|
851
|
+
}
|
|
852
|
+
printLanguagePreview() {
|
|
853
|
+
const detection = detectLanguages(this.projectRoot);
|
|
854
|
+
const lines = [];
|
|
855
|
+
for (const [lang, count] of Object.entries(detection.byExtension)) {
|
|
856
|
+
const hasPlugin = this.supportedExtensions.size > 0 && Object.entries(EXTENSION_LANGUAGE).some(([ext, id]) => id === lang && this.supportedExtensions.has(ext));
|
|
857
|
+
const status = hasPlugin ? "plugin available" : "no plugin \u2014 skipped";
|
|
858
|
+
lines.push(` ${lang.padEnd(10)} ${String(count).padStart(5)} files (${status})`);
|
|
859
|
+
}
|
|
860
|
+
if (lines.length === 0) return;
|
|
861
|
+
console.error("[ctxo] Language scan:");
|
|
862
|
+
for (const line of lines) console.error(line);
|
|
863
|
+
}
|
|
864
|
+
async registerDiscoveredPlugins(registry) {
|
|
865
|
+
const loaded = await loadPlugins(this.projectRoot);
|
|
866
|
+
let tsMorphLike;
|
|
867
|
+
let roslynAdapter = null;
|
|
868
|
+
let csharpTier = "unavailable";
|
|
869
|
+
for (const { plugin, adapter } of loaded) {
|
|
870
|
+
if (typeof adapter.initialize === "function") {
|
|
871
|
+
try {
|
|
872
|
+
await adapter.initialize(this.projectRoot);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
console.error(`[ctxo] Plugin ${plugin.id} initialize failed: ${err.message}`);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
registry.register(plugin.extensions, adapter);
|
|
879
|
+
const tsCandidate = adapter;
|
|
880
|
+
if (typeof tsCandidate.loadProjectSources === "function" && typeof tsCandidate.clearProjectSources === "function") {
|
|
881
|
+
tsMorphLike = tsCandidate;
|
|
882
|
+
}
|
|
883
|
+
const csCandidate = adapter;
|
|
884
|
+
if (typeof csCandidate.getRoslynDelegate === "function" && typeof csCandidate.getTier === "function") {
|
|
885
|
+
csharpTier = csCandidate.getTier();
|
|
886
|
+
roslynAdapter = csCandidate.getRoslynDelegate() ?? null;
|
|
887
|
+
}
|
|
888
|
+
const tierSuffix = plugin.id === "csharp" && csharpTier !== "full" ? ` (active: ${csharpTier})` : "";
|
|
889
|
+
console.error(`[ctxo] Plugin ${plugin.id}@${plugin.version} (${plugin.tier} tier${tierSuffix}) \u2014 ${plugin.extensions.join(", ")}`);
|
|
890
|
+
}
|
|
891
|
+
return { tsMorphLike, roslynAdapter, csharpTier };
|
|
892
|
+
}
|
|
893
|
+
discoverFilesIn(root) {
|
|
894
|
+
try {
|
|
895
|
+
const output = execFileSync2("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
896
|
+
cwd: root,
|
|
897
|
+
encoding: "utf-8",
|
|
898
|
+
maxBuffer: 10 * 1024 * 1024
|
|
899
|
+
});
|
|
900
|
+
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) => join7(root, line));
|
|
901
|
+
} catch {
|
|
902
|
+
console.error(`[ctxo] git ls-files failed for ${root}`);
|
|
903
|
+
return [];
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
discoverWorkspaces() {
|
|
907
|
+
const pkgPath = join7(this.projectRoot, "package.json");
|
|
908
|
+
if (!existsSync8(pkgPath)) return [this.projectRoot];
|
|
909
|
+
try {
|
|
910
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
911
|
+
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
912
|
+
if (!workspaces || workspaces.length === 0) return [this.projectRoot];
|
|
913
|
+
const resolved = [];
|
|
914
|
+
for (const ws of workspaces) {
|
|
915
|
+
if (ws.endsWith("/*") || ws.endsWith("\\*")) {
|
|
916
|
+
const parentDir = join7(this.projectRoot, ws.slice(0, -2));
|
|
917
|
+
if (existsSync8(parentDir)) {
|
|
918
|
+
for (const entry of readdirSync2(parentDir, { withFileTypes: true })) {
|
|
919
|
+
if (entry.isDirectory()) {
|
|
920
|
+
resolved.push(join7(parentDir, entry.name));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
const wsPath = join7(this.projectRoot, ws);
|
|
926
|
+
if (existsSync8(wsPath)) {
|
|
927
|
+
resolved.push(wsPath);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (resolved.length === 0) return [this.projectRoot];
|
|
932
|
+
console.error(`[ctxo] Monorepo detected: ${resolved.length} workspace(s)`);
|
|
933
|
+
return resolved;
|
|
934
|
+
} catch {
|
|
935
|
+
return [this.projectRoot];
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
isSupportedExtension(filePath) {
|
|
939
|
+
const ext = extname3(filePath).toLowerCase();
|
|
940
|
+
return this.supportedExtensions.has(ext);
|
|
941
|
+
}
|
|
942
|
+
async runCheck() {
|
|
943
|
+
console.error("[ctxo] Checking index freshness...");
|
|
944
|
+
const registry = new LanguageAdapterRegistry();
|
|
945
|
+
const { roslynAdapter: _roslyn } = await this.registerDiscoveredPlugins(registry);
|
|
946
|
+
this.supportedExtensions = registry.getSupportedExtensions();
|
|
947
|
+
if (_roslyn) await _roslyn.dispose();
|
|
948
|
+
const hasher = new ContentHasher();
|
|
949
|
+
const files = this.discoverFilesIn(this.projectRoot);
|
|
950
|
+
const reader = new (await import("./json-index-reader-NNKOVCOG.js")).JsonIndexReader(this.ctxoRoot);
|
|
951
|
+
const indices = reader.readAll();
|
|
952
|
+
const indexedMap = new Map(indices.map((i) => [i.file, i]));
|
|
953
|
+
let staleCount = 0;
|
|
954
|
+
for (const filePath of files) {
|
|
955
|
+
if (!this.isSupportedExtension(filePath)) continue;
|
|
956
|
+
const relativePath = relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
957
|
+
const indexed = indexedMap.get(relativePath);
|
|
958
|
+
if (!indexed) {
|
|
959
|
+
console.error(`[ctxo] NOT INDEXED: ${relativePath}`);
|
|
960
|
+
staleCount++;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (!existsSync8(filePath)) {
|
|
964
|
+
console.error(`[ctxo] DELETED: ${relativePath}`);
|
|
965
|
+
staleCount++;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const mtime = Math.floor(statSync(filePath).mtimeMs / 1e3);
|
|
969
|
+
if (mtime <= indexed.lastModified) continue;
|
|
970
|
+
if (indexed.contentHash) {
|
|
971
|
+
const source = readFileSync5(filePath, "utf-8");
|
|
972
|
+
const currentHash = hasher.hash(source);
|
|
973
|
+
if (currentHash === indexed.contentHash) continue;
|
|
974
|
+
}
|
|
975
|
+
console.error(`[ctxo] STALE: ${relativePath}`);
|
|
976
|
+
staleCount++;
|
|
977
|
+
}
|
|
978
|
+
if (staleCount > 0) {
|
|
979
|
+
console.error(`[ctxo] ${staleCount} file(s) need re-indexing. Run "ctxo index"`);
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
console.error("[ctxo] Index is up to date");
|
|
983
|
+
}
|
|
984
|
+
ensureGitignore() {
|
|
985
|
+
const gitignorePath = join7(this.projectRoot, ".gitignore");
|
|
986
|
+
const cachePattern = ".ctxo/.cache/";
|
|
987
|
+
const suffix = `
|
|
988
|
+
# Ctxo local cache (never committed)
|
|
989
|
+
${cachePattern}
|
|
990
|
+
`;
|
|
991
|
+
const existing = existsSync8(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : "";
|
|
992
|
+
if (existing.includes(cachePattern)) return;
|
|
993
|
+
writeFileSync3(gitignorePath, existing + suffix, "utf-8");
|
|
994
|
+
console.error("[ctxo] Added .ctxo/.cache/ to .gitignore");
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// src/cli/sync-command.ts
|
|
999
|
+
import { join as join8 } from "path";
|
|
1000
|
+
var SyncCommand = class {
|
|
1001
|
+
ctxoRoot;
|
|
1002
|
+
constructor(projectRoot) {
|
|
1003
|
+
this.ctxoRoot = join8(projectRoot, ".ctxo");
|
|
1004
|
+
}
|
|
1005
|
+
async run() {
|
|
1006
|
+
console.error("[ctxo] Rebuilding SQLite cache from committed JSON index...");
|
|
1007
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
1008
|
+
try {
|
|
1009
|
+
await storage.init();
|
|
1010
|
+
} finally {
|
|
1011
|
+
storage.close();
|
|
1012
|
+
}
|
|
1013
|
+
console.error("[ctxo] Sync complete");
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// src/cli/status-command.ts
|
|
1018
|
+
import { join as join9 } from "path";
|
|
1019
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1020
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1021
|
+
var StatusCommand = class {
|
|
1022
|
+
projectRoot;
|
|
1023
|
+
ctxoRoot;
|
|
1024
|
+
constructor(projectRoot) {
|
|
1025
|
+
this.projectRoot = projectRoot;
|
|
1026
|
+
this.ctxoRoot = join9(projectRoot, ".ctxo");
|
|
1027
|
+
}
|
|
1028
|
+
run() {
|
|
1029
|
+
const indexDir = join9(this.ctxoRoot, "index");
|
|
1030
|
+
if (!existsSync9(indexDir)) {
|
|
1031
|
+
console.error('[ctxo] No index found. Run "ctxo index" first.');
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const schemaManager = new SchemaManager(this.ctxoRoot);
|
|
1035
|
+
const version = schemaManager.readStoredVersion() ?? "unknown";
|
|
1036
|
+
const reader = new JsonIndexReader(this.ctxoRoot);
|
|
1037
|
+
const indices = reader.readAll();
|
|
1038
|
+
const totalSymbols = indices.reduce((sum, idx) => sum + idx.symbols.length, 0);
|
|
1039
|
+
const totalEdges = indices.reduce((sum, idx) => sum + idx.edges.length, 0);
|
|
1040
|
+
console.error(`[ctxo] Index Status`);
|
|
1041
|
+
console.error(` Schema version: ${version}`);
|
|
1042
|
+
console.error(` Indexed files: ${indices.length}`);
|
|
1043
|
+
console.error(` Total symbols: ${totalSymbols}`);
|
|
1044
|
+
console.error(` Total edges: ${totalEdges}`);
|
|
1045
|
+
const cacheExists = existsSync9(join9(this.ctxoRoot, ".cache", "symbols.db"));
|
|
1046
|
+
console.error(` SQLite cache: ${cacheExists ? "present" : "missing (run ctxo sync)"}`);
|
|
1047
|
+
if (indices.length > 0) {
|
|
1048
|
+
console.error("");
|
|
1049
|
+
console.error(" Files:");
|
|
1050
|
+
const sourceFiles = this.getSourceFiles();
|
|
1051
|
+
for (const idx of indices.sort((a, b) => a.file.localeCompare(b.file))) {
|
|
1052
|
+
const ts = new Date(idx.lastModified * 1e3).toISOString();
|
|
1053
|
+
const isOrphaned = sourceFiles.size > 0 && !sourceFiles.has(idx.file);
|
|
1054
|
+
const badge = isOrphaned ? " [orphaned]" : "";
|
|
1055
|
+
console.error(` ${idx.file} ${ts} (${idx.symbols.length} symbols, ${idx.edges.length} edges)${badge}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
getSourceFiles() {
|
|
1060
|
+
try {
|
|
1061
|
+
const output = execFileSync3("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
1062
|
+
cwd: this.projectRoot,
|
|
1063
|
+
encoding: "utf-8"
|
|
1064
|
+
});
|
|
1065
|
+
return new Set(output.split("\n").map((l) => l.trim()).filter((l) => l.length > 0));
|
|
1066
|
+
} catch {
|
|
1067
|
+
return /* @__PURE__ */ new Set();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// src/cli/verify-command.ts
|
|
1073
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
1074
|
+
import { join as join10 } from "path";
|
|
1075
|
+
import { tmpdir } from "os";
|
|
1076
|
+
var VerifyCommand = class {
|
|
1077
|
+
projectRoot;
|
|
1078
|
+
constructor(projectRoot) {
|
|
1079
|
+
this.projectRoot = projectRoot;
|
|
1080
|
+
}
|
|
1081
|
+
async run() {
|
|
1082
|
+
console.error("[ctxo] Verifying index freshness...");
|
|
1083
|
+
const tempDir = mkdtempSync(join10(tmpdir(), "ctxo-verify-"));
|
|
1084
|
+
try {
|
|
1085
|
+
const tempCtxo = join10(tempDir, ".ctxo");
|
|
1086
|
+
const indexCmd = new IndexCommand(this.projectRoot, tempCtxo);
|
|
1087
|
+
await indexCmd.run({ skipSideEffects: true });
|
|
1088
|
+
const committedReader = new JsonIndexReader(join10(this.projectRoot, ".ctxo"));
|
|
1089
|
+
const freshReader = new JsonIndexReader(tempCtxo);
|
|
1090
|
+
const committedIndices = committedReader.readAll();
|
|
1091
|
+
const freshIndices = freshReader.readAll();
|
|
1092
|
+
const serialize = (i) => JSON.stringify({ symbols: i.symbols, edges: i.edges, intent: i.intent, antiPatterns: i.antiPatterns });
|
|
1093
|
+
const committedMap = new Map(committedIndices.map((i) => [i.file, serialize(i)]));
|
|
1094
|
+
const freshMap = new Map(freshIndices.map((i) => [i.file, serialize(i)]));
|
|
1095
|
+
let stale = false;
|
|
1096
|
+
for (const [file, freshData] of freshMap) {
|
|
1097
|
+
const committed = committedMap.get(file);
|
|
1098
|
+
if (committed !== freshData) {
|
|
1099
|
+
console.error(`[ctxo] STALE: ${file}`);
|
|
1100
|
+
stale = true;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
for (const file of committedMap.keys()) {
|
|
1104
|
+
if (!freshMap.has(file)) {
|
|
1105
|
+
console.error(`[ctxo] REMOVED: ${file}`);
|
|
1106
|
+
stale = true;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (stale) {
|
|
1110
|
+
console.error('[ctxo] Index is STALE \u2014 run "ctxo index" and commit .ctxo/index/');
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
console.error("[ctxo] Index is up to date");
|
|
1114
|
+
} finally {
|
|
1115
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// src/cli/init-command.ts
|
|
1121
|
+
import { join as join12 } from "path";
|
|
1122
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, chmodSync } from "fs";
|
|
1123
|
+
import { createRequire as createRequire2 } from "module";
|
|
1124
|
+
|
|
1125
|
+
// src/cli/ai-rules.ts
|
|
1126
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
1127
|
+
import { join as join11, dirname as dirname6 } from "path";
|
|
1128
|
+
var PLATFORMS = [
|
|
1129
|
+
{ id: "claude-code", name: "Claude Code", file: "CLAUDE.md", mode: "append", detectPaths: ["CLAUDE.md", ".claude"], starred: true },
|
|
1130
|
+
{ id: "cursor", name: "Cursor", file: ".cursor/rules/ctxo.mdc", mode: "create", detectPaths: [".cursor", ".cursorrules"], starred: false },
|
|
1131
|
+
{ id: "github-copilot", name: "GitHub Copilot", file: ".github/copilot-instructions.md", mode: "create", detectPaths: [".github", ".vscode"], starred: false },
|
|
1132
|
+
{ id: "windsurf", name: "Windsurf", file: ".windsurfrules", mode: "create", detectPaths: [".windsurfrules", ".windsurf"], starred: false },
|
|
1133
|
+
{ id: "antigravity", name: "Google Antigravity", file: "AGENTS.md", mode: "create", detectPaths: ["AGENTS.md", "GEMINI.md", ".gemini"], starred: false },
|
|
1134
|
+
{ id: "augment", name: "Augment Code", file: "augment-guidelines.md", mode: "create", detectPaths: ["augment-guidelines.md", ".augment"], starred: false },
|
|
1135
|
+
{ id: "amazonq", name: "Amazon Q", file: ".amazonq/rules/ctxo.md", mode: "create", detectPaths: [".amazonq"], starred: false }
|
|
1136
|
+
];
|
|
1137
|
+
function detectPlatforms(projectRoot) {
|
|
1138
|
+
const detected = /* @__PURE__ */ new Set();
|
|
1139
|
+
for (const p of PLATFORMS) {
|
|
1140
|
+
if (p.detectPaths.some((d) => existsSync10(join11(projectRoot, d)))) {
|
|
1141
|
+
detected.add(p.id);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return detected;
|
|
1145
|
+
}
|
|
1146
|
+
var SECTION_START = "<!-- ctxo-rules-start -->";
|
|
1147
|
+
var SECTION_END = "<!-- ctxo-rules-end -->";
|
|
1148
|
+
function ruleBody() {
|
|
1149
|
+
return `## ctxo MCP Tool Usage (MANDATORY)
|
|
1150
|
+
|
|
1151
|
+
**ALWAYS use ctxo MCP tools before reading source files or making code changes.** The ctxo index contains dependency graphs, git intent, anti-patterns, and change health that cannot be derived from reading files alone. Skipping these tools leads to blind edits and broken dependencies.
|
|
1152
|
+
|
|
1153
|
+
### Before ANY Code Modification
|
|
1154
|
+
1. Call \`get_blast_radius\` for the symbol you are about to change \u2014 understand what breaks
|
|
1155
|
+
2. Call \`get_why_context\` for the same symbol \u2014 check for revert history or anti-patterns
|
|
1156
|
+
3. Only then read and edit source files
|
|
1157
|
+
|
|
1158
|
+
### Before Starting a Task
|
|
1159
|
+
| Task Type | REQUIRED First Call |
|
|
1160
|
+
|---|---|
|
|
1161
|
+
| Fixing a bug | \`get_context_for_task(taskType: "fix")\` |
|
|
1162
|
+
| Adding/extending a feature | \`get_context_for_task(taskType: "extend")\` |
|
|
1163
|
+
| Refactoring | \`get_context_for_task(taskType: "refactor")\` |
|
|
1164
|
+
| Understanding code | \`get_context_for_task(taskType: "understand")\` |
|
|
1165
|
+
|
|
1166
|
+
### Before Reviewing a PR or Diff
|
|
1167
|
+
- Call \`get_pr_impact\` \u2014 single call gives full risk assessment with co-change analysis
|
|
1168
|
+
|
|
1169
|
+
### When Exploring or Searching Code
|
|
1170
|
+
- Use \`search_symbols\` for name/regex lookup \u2014 DO NOT grep source files for symbol discovery
|
|
1171
|
+
- Use \`get_ranked_context\` for natural language queries \u2014 DO NOT manually browse directories
|
|
1172
|
+
|
|
1173
|
+
### Orientation in Unfamiliar Areas
|
|
1174
|
+
- Call \`get_architectural_overlay\` to understand layer boundaries
|
|
1175
|
+
- Call \`get_symbol_importance\` to identify critical symbols
|
|
1176
|
+
|
|
1177
|
+
### NEVER Do These
|
|
1178
|
+
- NEVER edit a function without first calling \`get_blast_radius\` on it
|
|
1179
|
+
- NEVER skip \`get_why_context\` \u2014 reverted code and anti-patterns are invisible without it
|
|
1180
|
+
- NEVER grep source files to find symbols when \`search_symbols\` exists
|
|
1181
|
+
- NEVER manually trace imports when \`find_importers\` gives the full reverse dependency graph`;
|
|
1182
|
+
}
|
|
1183
|
+
function cursorFrontmatter() {
|
|
1184
|
+
return `---
|
|
1185
|
+
description: ctxo MCP tool usage rules \u2014 always applied
|
|
1186
|
+
globs:
|
|
1187
|
+
alwaysApply: true
|
|
1188
|
+
---
|
|
1189
|
+
|
|
1190
|
+
`;
|
|
1191
|
+
}
|
|
1192
|
+
function generateRules(platformId) {
|
|
1193
|
+
const body = ruleBody();
|
|
1194
|
+
if (platformId === "cursor") {
|
|
1195
|
+
return cursorFrontmatter() + body + "\n";
|
|
1196
|
+
}
|
|
1197
|
+
return body + "\n";
|
|
1198
|
+
}
|
|
1199
|
+
function installRules(projectRoot, platformId) {
|
|
1200
|
+
const platform = PLATFORMS.find((p) => p.id === platformId);
|
|
1201
|
+
if (!platform) throw new Error(`Unknown platform: ${platformId}`);
|
|
1202
|
+
const filePath = join11(projectRoot, platform.file);
|
|
1203
|
+
const content = generateRules(platformId);
|
|
1204
|
+
const dir = dirname6(filePath);
|
|
1205
|
+
if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
|
|
1206
|
+
if (platform.mode === "create") {
|
|
1207
|
+
writeFileSync4(filePath, content, "utf-8");
|
|
1208
|
+
return { file: platform.file, action: existsSync10(filePath) ? "updated" : "created" };
|
|
1209
|
+
}
|
|
1210
|
+
if (!existsSync10(filePath)) {
|
|
1211
|
+
writeFileSync4(filePath, content, "utf-8");
|
|
1212
|
+
return { file: platform.file, action: "created" };
|
|
1213
|
+
}
|
|
1214
|
+
const existing = readFileSync6(filePath, "utf-8");
|
|
1215
|
+
if (existing.includes(SECTION_START)) {
|
|
1216
|
+
const re = new RegExp(`${escapeRegExp(SECTION_START)}[\\s\\S]*?${escapeRegExp(SECTION_END)}`, "m");
|
|
1217
|
+
const updated2 = existing.replace(re, `${SECTION_START}
|
|
1218
|
+
${content}${SECTION_END}`);
|
|
1219
|
+
writeFileSync4(filePath, updated2, "utf-8");
|
|
1220
|
+
return { file: platform.file, action: "updated" };
|
|
1221
|
+
}
|
|
1222
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1223
|
+
const updated = existing + separator + `${SECTION_START}
|
|
1224
|
+
${content}${SECTION_END}
|
|
1225
|
+
`;
|
|
1226
|
+
writeFileSync4(filePath, updated, "utf-8");
|
|
1227
|
+
return { file: platform.file, action: "updated" };
|
|
1228
|
+
}
|
|
1229
|
+
function escapeRegExp(s) {
|
|
1230
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1231
|
+
}
|
|
1232
|
+
var GITIGNORE_ENTRY = ".ctxo/.cache/";
|
|
1233
|
+
function ensureGitignore(projectRoot) {
|
|
1234
|
+
const filePath = join11(projectRoot, ".gitignore");
|
|
1235
|
+
if (!existsSync10(filePath)) {
|
|
1236
|
+
writeFileSync4(filePath, `# Ctxo local cache (auto-rebuilt, never committed)
|
|
1237
|
+
${GITIGNORE_ENTRY}
|
|
1238
|
+
`, "utf-8");
|
|
1239
|
+
return { file: ".gitignore", action: "created" };
|
|
1240
|
+
}
|
|
1241
|
+
const existing = readFileSync6(filePath, "utf-8");
|
|
1242
|
+
if (existing.includes(GITIGNORE_ENTRY)) {
|
|
1243
|
+
return { file: ".gitignore", action: "skipped" };
|
|
1244
|
+
}
|
|
1245
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1246
|
+
const updated = existing + `${separator}# Ctxo local cache (auto-rebuilt, never committed)
|
|
1247
|
+
${GITIGNORE_ENTRY}
|
|
1248
|
+
`;
|
|
1249
|
+
writeFileSync4(filePath, updated, "utf-8");
|
|
1250
|
+
return { file: ".gitignore", action: "updated" };
|
|
1251
|
+
}
|
|
1252
|
+
var DEFAULT_CONFIG = `# ctxo project configuration
|
|
1253
|
+
# Docs: https://github.com/alperhankendi/Ctxo
|
|
1254
|
+
version: "1.0"
|
|
1255
|
+
`;
|
|
1256
|
+
function ensureConfig(projectRoot) {
|
|
1257
|
+
const filePath = join11(projectRoot, ".ctxo", "config.yaml");
|
|
1258
|
+
const dir = dirname6(filePath);
|
|
1259
|
+
if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
|
|
1260
|
+
if (existsSync10(filePath)) {
|
|
1261
|
+
return { file: ".ctxo/config.yaml", action: "skipped" };
|
|
1262
|
+
}
|
|
1263
|
+
writeFileSync4(filePath, DEFAULT_CONFIG, "utf-8");
|
|
1264
|
+
return { file: ".ctxo/config.yaml", action: "created" };
|
|
1265
|
+
}
|
|
1266
|
+
var CTXO_MCP_ENTRY = { command: "npx", args: ["-y", "@ctxo/cli"] };
|
|
1267
|
+
function getMcpConfigTargets(selectedPlatformIds) {
|
|
1268
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1269
|
+
for (const id of selectedPlatformIds) {
|
|
1270
|
+
switch (id) {
|
|
1271
|
+
case "claude-code":
|
|
1272
|
+
case "cursor":
|
|
1273
|
+
case "windsurf":
|
|
1274
|
+
case "augment":
|
|
1275
|
+
case "antigravity":
|
|
1276
|
+
targets.set(".mcp.json", { file: ".mcp.json", serverKey: "mcpServers" });
|
|
1277
|
+
break;
|
|
1278
|
+
case "github-copilot":
|
|
1279
|
+
targets.set(".vscode/mcp.json", { file: ".vscode/mcp.json", serverKey: "servers", extraFields: { type: "stdio" } });
|
|
1280
|
+
break;
|
|
1281
|
+
case "amazonq":
|
|
1282
|
+
targets.set(".amazonq/mcp.json", { file: ".amazonq/mcp.json", serverKey: "mcpServers" });
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return [...targets.values()];
|
|
1287
|
+
}
|
|
1288
|
+
function ensureMcpConfig(projectRoot, target) {
|
|
1289
|
+
const filePath = join11(projectRoot, target.file);
|
|
1290
|
+
const dir = dirname6(filePath);
|
|
1291
|
+
if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
|
|
1292
|
+
const entry = target.extraFields ? { ...target.extraFields, ...CTXO_MCP_ENTRY } : { ...CTXO_MCP_ENTRY };
|
|
1293
|
+
if (!existsSync10(filePath)) {
|
|
1294
|
+
const config = { [target.serverKey]: { ctxo: entry } };
|
|
1295
|
+
writeFileSync4(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1296
|
+
return { file: target.file, action: "created" };
|
|
1297
|
+
}
|
|
1298
|
+
let existing;
|
|
1299
|
+
try {
|
|
1300
|
+
existing = JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
1301
|
+
} catch {
|
|
1302
|
+
const config = { [target.serverKey]: { ctxo: entry } };
|
|
1303
|
+
writeFileSync4(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1304
|
+
return { file: target.file, action: "updated" };
|
|
1305
|
+
}
|
|
1306
|
+
const servers = existing[target.serverKey] ?? {};
|
|
1307
|
+
if (servers["ctxo"]) {
|
|
1308
|
+
return { file: target.file, action: "skipped" };
|
|
1309
|
+
}
|
|
1310
|
+
servers["ctxo"] = entry;
|
|
1311
|
+
existing[target.serverKey] = servers;
|
|
1312
|
+
writeFileSync4(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
1313
|
+
return { file: target.file, action: "updated" };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// src/cli/init-command.ts
|
|
1317
|
+
var requireCjs2 = createRequire2(import.meta.url);
|
|
1318
|
+
function resolveInstalledPlugins(needed) {
|
|
1319
|
+
const installed = [];
|
|
1320
|
+
const missing = [];
|
|
1321
|
+
for (const lang of needed) {
|
|
1322
|
+
try {
|
|
1323
|
+
requireCjs2.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
1324
|
+
installed.push(lang);
|
|
1325
|
+
} catch {
|
|
1326
|
+
missing.push(lang);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return { installed, missing };
|
|
1330
|
+
}
|
|
1331
|
+
var CTXO_START = "# ctxo-start";
|
|
1332
|
+
var CTXO_END = "# ctxo-end";
|
|
1333
|
+
var POST_COMMIT_CONTENT = `
|
|
1334
|
+
${CTXO_START}
|
|
1335
|
+
# Incremental re-index on commit (only changed files)
|
|
1336
|
+
if command -v ctxo >/dev/null 2>&1; then
|
|
1337
|
+
for file in $(git diff --name-only HEAD~1 HEAD 2>/dev/null); do
|
|
1338
|
+
ctxo index --file "$file" 2>/dev/null || true
|
|
1339
|
+
done
|
|
1340
|
+
fi
|
|
1341
|
+
${CTXO_END}
|
|
1342
|
+
`.trim();
|
|
1343
|
+
var POST_MERGE_CONTENT = `
|
|
1344
|
+
${CTXO_START}
|
|
1345
|
+
# Rebuild SQLite cache after merge (index updated via git pull)
|
|
1346
|
+
if command -v ctxo >/dev/null 2>&1; then
|
|
1347
|
+
ctxo sync 2>/dev/null || true
|
|
1348
|
+
fi
|
|
1349
|
+
${CTXO_END}
|
|
1350
|
+
`.trim();
|
|
1351
|
+
var STRIP_ANSI = /\u001b\[[0-9;]*m/g;
|
|
1352
|
+
var strip = (s) => s.replace(STRIP_ANSI, "");
|
|
1353
|
+
function box(lines, opts, pc) {
|
|
1354
|
+
const border = opts.border ?? pc.dim;
|
|
1355
|
+
const w = opts.width ?? Math.max(...lines.map((l) => strip(l).length), strip(opts.title ?? "").length + 4, 48);
|
|
1356
|
+
const pad = (s) => s + " ".repeat(Math.max(0, w - strip(s).length));
|
|
1357
|
+
const hr = "\u2500".repeat(w + 2);
|
|
1358
|
+
let top;
|
|
1359
|
+
if (opts.title) {
|
|
1360
|
+
const tLen = strip(opts.title).length;
|
|
1361
|
+
top = border("\u256D\u2500 ") + opts.title + border(` ${"\u2500".repeat(Math.max(0, w - tLen - 2))}\u256E`);
|
|
1362
|
+
} else {
|
|
1363
|
+
top = border(`\u256D${hr}\u256E`);
|
|
1364
|
+
}
|
|
1365
|
+
const bot = border(`\u2570${hr}\u256F`);
|
|
1366
|
+
const body = lines.map((l) => `${border("\u2502")} ${pad(l)} ${border("\u2502")}`).join("\n");
|
|
1367
|
+
return `${top}
|
|
1368
|
+
${body}
|
|
1369
|
+
${bot}`;
|
|
1370
|
+
}
|
|
1371
|
+
function stepHeader(step, total, title, desc, pc) {
|
|
1372
|
+
const stepLabel = pc.bold(pc.cyan(`${step}/${total}`));
|
|
1373
|
+
const titleStr = pc.bold(title);
|
|
1374
|
+
return box(
|
|
1375
|
+
[`${pc.dim(desc)}`],
|
|
1376
|
+
{ title: `${stepLabel} ${titleStr}`, border: pc.cyan },
|
|
1377
|
+
pc
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
function renderBanner(_version, pc) {
|
|
1381
|
+
const art = [
|
|
1382
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
1383
|
+
" \u2588\u2588\u2554\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557",
|
|
1384
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
1385
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
1386
|
+
" \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
1387
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D "
|
|
1388
|
+
];
|
|
1389
|
+
const colors = [pc.cyan, pc.cyan, pc.blueBright, pc.blueBright, pc.magentaBright, pc.magentaBright];
|
|
1390
|
+
const coloredArt = art.map((line, i) => colors[i](line));
|
|
1391
|
+
const tagline1 = pc.bold(pc.white(" Code intelligence for AI agents."));
|
|
1392
|
+
const tagline2 = pc.dim(" One call instead of hundreds.");
|
|
1393
|
+
const inner = [
|
|
1394
|
+
"",
|
|
1395
|
+
...coloredArt,
|
|
1396
|
+
"",
|
|
1397
|
+
tagline1,
|
|
1398
|
+
tagline2,
|
|
1399
|
+
""
|
|
1400
|
+
];
|
|
1401
|
+
return box(inner, { border: pc.dim }, pc);
|
|
1402
|
+
}
|
|
1403
|
+
var InitCommand = class {
|
|
1404
|
+
projectRoot;
|
|
1405
|
+
constructor(projectRoot) {
|
|
1406
|
+
this.projectRoot = projectRoot;
|
|
1407
|
+
}
|
|
1408
|
+
installHooks() {
|
|
1409
|
+
const hooksDir = join12(this.projectRoot, ".git", "hooks");
|
|
1410
|
+
mkdirSync4(hooksDir, { recursive: true });
|
|
1411
|
+
this.installHook(hooksDir, "post-commit", POST_COMMIT_CONTENT);
|
|
1412
|
+
this.installHook(hooksDir, "post-merge", POST_MERGE_CONTENT);
|
|
1413
|
+
}
|
|
1414
|
+
async run(options = {}) {
|
|
1415
|
+
if (!existsSync11(join12(this.projectRoot, ".git"))) {
|
|
1416
|
+
console.error('[ctxo] Not a git repository. Run "git init" first.');
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
if (options.dryRun) return this.dryRun(options);
|
|
1420
|
+
if (options.yes || options.tools) return await this.runNonInteractive(options);
|
|
1421
|
+
return this.runInteractive(options);
|
|
1422
|
+
}
|
|
1423
|
+
/* ---------------------------------------------------------------- */
|
|
1424
|
+
/* Interactive flow */
|
|
1425
|
+
/* ---------------------------------------------------------------- */
|
|
1426
|
+
async runInteractive(options) {
|
|
1427
|
+
const clack = await import("@clack/prompts");
|
|
1428
|
+
const pc = (await import("picocolors")).default;
|
|
1429
|
+
const version = await this.getVersion();
|
|
1430
|
+
const totalSteps = options.rulesOnly ? 1 : 3;
|
|
1431
|
+
console.error("");
|
|
1432
|
+
console.error(renderBanner(version, pc));
|
|
1433
|
+
console.error("");
|
|
1434
|
+
let stepNum = 0;
|
|
1435
|
+
if (!options.rulesOnly) {
|
|
1436
|
+
stepNum++;
|
|
1437
|
+
console.error(stepHeader(stepNum, totalSteps, "Index Directory", "Where ctxo stores the symbol graph and metadata.", pc));
|
|
1438
|
+
console.error("");
|
|
1439
|
+
const indexDir = await clack.text({
|
|
1440
|
+
message: pc.cyan("Directory path"),
|
|
1441
|
+
placeholder: ".ctxo/index",
|
|
1442
|
+
initialValue: ".ctxo/index",
|
|
1443
|
+
validate: (val) => {
|
|
1444
|
+
if (!val?.trim()) return "Directory path is required";
|
|
1445
|
+
return void 0;
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
if (clack.isCancel(indexDir)) {
|
|
1449
|
+
clack.cancel("Setup cancelled.");
|
|
1450
|
+
process.exit(0);
|
|
1451
|
+
}
|
|
1452
|
+
const fullPath = join12(this.projectRoot, indexDir);
|
|
1453
|
+
if (!existsSync11(fullPath)) mkdirSync4(fullPath, { recursive: true });
|
|
1454
|
+
console.error("");
|
|
1455
|
+
}
|
|
1456
|
+
stepNum++;
|
|
1457
|
+
const detected = detectPlatforms(this.projectRoot);
|
|
1458
|
+
const detectedCount = detected.size;
|
|
1459
|
+
const detectedHint = detectedCount > 0 ? pc.green(` ${detectedCount} detected`) : "";
|
|
1460
|
+
console.error(stepHeader(stepNum, totalSteps, `AI Tools${detectedHint}`, "Select your tools. Generates MCP usage rules for each.", pc));
|
|
1461
|
+
console.error("");
|
|
1462
|
+
const toolChoices = PLATFORMS.map((p) => {
|
|
1463
|
+
const isDetected = detected.has(p.id);
|
|
1464
|
+
const star = p.starred ? pc.yellow(" \u2605") : "";
|
|
1465
|
+
const detect = isDetected ? pc.green(" (detected)") : "";
|
|
1466
|
+
return {
|
|
1467
|
+
value: p.id,
|
|
1468
|
+
label: `${p.name}${star}${detect}`,
|
|
1469
|
+
hint: isDetected ? void 0 : pc.dim(p.file)
|
|
1470
|
+
};
|
|
1471
|
+
});
|
|
1472
|
+
const selectedTools = await clack.multiselect({
|
|
1473
|
+
message: pc.cyan("Select tools"),
|
|
1474
|
+
options: toolChoices,
|
|
1475
|
+
initialValues: [...detected],
|
|
1476
|
+
required: false
|
|
1477
|
+
});
|
|
1478
|
+
if (clack.isCancel(selectedTools)) {
|
|
1479
|
+
clack.cancel("Setup cancelled.");
|
|
1480
|
+
process.exit(0);
|
|
1481
|
+
}
|
|
1482
|
+
console.error("");
|
|
1483
|
+
let pluginsToInstall = [];
|
|
1484
|
+
if (!options.rulesOnly && !options.noInstall) {
|
|
1485
|
+
const detection = detectLanguages(this.projectRoot);
|
|
1486
|
+
const needed = decideNeededLanguages(detection);
|
|
1487
|
+
const { missing } = resolveInstalledPlugins(needed);
|
|
1488
|
+
if (missing.length > 0) {
|
|
1489
|
+
console.error(stepHeader(
|
|
1490
|
+
stepNum + 1,
|
|
1491
|
+
totalSteps + 1,
|
|
1492
|
+
`Language Plugins ${pc.yellow("missing")}`,
|
|
1493
|
+
`Detected: ${missing.join(", ")} \u2014 ctxo needs a plugin per language to index it.`,
|
|
1494
|
+
pc
|
|
1495
|
+
));
|
|
1496
|
+
console.error("");
|
|
1497
|
+
const confirm = await clack.confirm({
|
|
1498
|
+
message: pc.cyan(`Install ${missing.map(officialPluginFor).join(" ")}?`),
|
|
1499
|
+
initialValue: true
|
|
1500
|
+
});
|
|
1501
|
+
if (clack.isCancel(confirm)) {
|
|
1502
|
+
clack.cancel("Setup cancelled.");
|
|
1503
|
+
process.exit(0);
|
|
1504
|
+
}
|
|
1505
|
+
if (confirm) {
|
|
1506
|
+
pluginsToInstall = missing;
|
|
1507
|
+
stepNum++;
|
|
1508
|
+
}
|
|
1509
|
+
console.error("");
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
let installGitHooks = false;
|
|
1513
|
+
if (!options.rulesOnly) {
|
|
1514
|
+
stepNum++;
|
|
1515
|
+
console.error(stepHeader(stepNum, totalSteps, `Git Hooks ${pc.yellow("recommended")}`, "Auto-updates index on every commit and pull.", pc));
|
|
1516
|
+
console.error("");
|
|
1517
|
+
const hooks = await clack.confirm({
|
|
1518
|
+
message: pc.cyan("Install hooks?"),
|
|
1519
|
+
initialValue: true
|
|
1520
|
+
});
|
|
1521
|
+
if (clack.isCancel(hooks)) {
|
|
1522
|
+
clack.cancel("Setup cancelled.");
|
|
1523
|
+
process.exit(0);
|
|
1524
|
+
}
|
|
1525
|
+
installGitHooks = hooks;
|
|
1526
|
+
console.error("");
|
|
1527
|
+
}
|
|
1528
|
+
const s = clack.spinner();
|
|
1529
|
+
s.start(pc.dim("Configuring..."));
|
|
1530
|
+
const results = [];
|
|
1531
|
+
const gitignoreResult = ensureGitignore(this.projectRoot);
|
|
1532
|
+
const configResult = ensureConfig(this.projectRoot);
|
|
1533
|
+
if (!options.rulesOnly) {
|
|
1534
|
+
results.push(`${pc.green("\u2713")} ${pc.bold(".ctxo/index/")} ${pc.dim("index directory")}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (configResult.action !== "skipped") {
|
|
1537
|
+
results.push(`${pc.green("\u2713")} ${pc.bold(configResult.file)} ${pc.dim(configResult.action)}`);
|
|
1538
|
+
}
|
|
1539
|
+
if (gitignoreResult.action !== "skipped") {
|
|
1540
|
+
results.push(`${pc.green("\u2713")} ${pc.bold(gitignoreResult.file)} ${pc.dim(gitignoreResult.action)}`);
|
|
1541
|
+
}
|
|
1542
|
+
for (const toolId of selectedTools) {
|
|
1543
|
+
const result = installRules(this.projectRoot, toolId);
|
|
1544
|
+
results.push(`${pc.green("\u2713")} ${pc.bold(result.file)} ${pc.dim(result.action)}`);
|
|
1545
|
+
}
|
|
1546
|
+
const mcpTargets = getMcpConfigTargets(selectedTools);
|
|
1547
|
+
for (const target of mcpTargets) {
|
|
1548
|
+
const result = ensureMcpConfig(this.projectRoot, target);
|
|
1549
|
+
if (result.action !== "skipped") {
|
|
1550
|
+
results.push(`${pc.green("\u2713")} ${pc.bold(result.file)} ${pc.dim(result.action + " \u2014 MCP server registered")}`);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (installGitHooks) {
|
|
1554
|
+
this.installHooks();
|
|
1555
|
+
results.push(`${pc.green("\u2713")} ${pc.bold("post-commit, post-merge")} ${pc.dim("hooks installed")}`);
|
|
1556
|
+
}
|
|
1557
|
+
if (pluginsToInstall.length > 0) {
|
|
1558
|
+
s.stop(pc.dim("Installing language plugins..."));
|
|
1559
|
+
const installer = new InstallCommand(this.projectRoot);
|
|
1560
|
+
await installer.run({ languages: pluginsToInstall, yes: true });
|
|
1561
|
+
results.push(`${pc.green("\u2713")} ${pc.bold(pluginsToInstall.map(officialPluginFor).join(", "))} ${pc.dim("language plugins installed")}`);
|
|
1562
|
+
s.start(pc.dim("Finalizing..."));
|
|
1563
|
+
}
|
|
1564
|
+
s.stop(pc.green("Done!"));
|
|
1565
|
+
if (results.length > 0) {
|
|
1566
|
+
console.error("");
|
|
1567
|
+
console.error(box(results, { title: pc.green("\u2713 Created"), border: pc.green }, pc));
|
|
1568
|
+
}
|
|
1569
|
+
const hasWork = selectedTools.length > 0 || installGitHooks;
|
|
1570
|
+
if (hasWork) {
|
|
1571
|
+
const steps = [
|
|
1572
|
+
`${pc.cyan("\u25B6")} npx ctxo index ${pc.dim("\u2500 build codebase index")}`,
|
|
1573
|
+
`${pc.cyan("\u25B6")} npx ctxo doctor ${pc.dim("\u2500 verify everything works")}`
|
|
1574
|
+
];
|
|
1575
|
+
console.error("");
|
|
1576
|
+
console.error(box(steps, { title: pc.cyan("\u2192 Next steps"), border: pc.cyan }, pc));
|
|
1577
|
+
}
|
|
1578
|
+
console.error("");
|
|
1579
|
+
console.error(` ${pc.dim("Happy coding!")} ${pc.magentaBright("\u2764")}`);
|
|
1580
|
+
console.error("");
|
|
1581
|
+
}
|
|
1582
|
+
/* ---------------------------------------------------------------- */
|
|
1583
|
+
/* Non-interactive flow */
|
|
1584
|
+
/* ---------------------------------------------------------------- */
|
|
1585
|
+
async runNonInteractive(options) {
|
|
1586
|
+
const toolIds = options.tools ?? [];
|
|
1587
|
+
for (const id of toolIds) {
|
|
1588
|
+
if (!PLATFORMS.find((p) => p.id === id)) {
|
|
1589
|
+
console.error(`[ctxo] Unknown tool: "${id}". Valid: ${PLATFORMS.map((p) => p.id).join(", ")}`);
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const gitignoreResult = ensureGitignore(this.projectRoot);
|
|
1594
|
+
const configResult = ensureConfig(this.projectRoot);
|
|
1595
|
+
if (!options.rulesOnly) {
|
|
1596
|
+
const indexDir = join12(this.projectRoot, ".ctxo", "index");
|
|
1597
|
+
if (!existsSync11(indexDir)) mkdirSync4(indexDir, { recursive: true });
|
|
1598
|
+
console.error("[ctxo] \u2713 .ctxo/index/ ready");
|
|
1599
|
+
}
|
|
1600
|
+
if (configResult.action !== "skipped") {
|
|
1601
|
+
console.error(`[ctxo] \u2713 ${configResult.file} \u2014 ${configResult.action}`);
|
|
1602
|
+
}
|
|
1603
|
+
if (gitignoreResult.action !== "skipped") {
|
|
1604
|
+
console.error(`[ctxo] \u2713 ${gitignoreResult.file} \u2014 ${gitignoreResult.action}`);
|
|
1605
|
+
}
|
|
1606
|
+
for (const toolId of toolIds) {
|
|
1607
|
+
const result = installRules(this.projectRoot, toolId);
|
|
1608
|
+
console.error(`[ctxo] \u2713 ${result.file} \u2014 ${result.action}`);
|
|
1609
|
+
}
|
|
1610
|
+
const mcpTargets = getMcpConfigTargets(toolIds);
|
|
1611
|
+
for (const target of mcpTargets) {
|
|
1612
|
+
const result = ensureMcpConfig(this.projectRoot, target);
|
|
1613
|
+
if (result.action !== "skipped") {
|
|
1614
|
+
console.error(`[ctxo] \u2713 ${result.file} \u2014 ${result.action} (MCP server registered)`);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (!options.rulesOnly) {
|
|
1618
|
+
this.installHooks();
|
|
1619
|
+
console.error("[ctxo] \u2713 Git hooks installed");
|
|
1620
|
+
}
|
|
1621
|
+
if (!options.rulesOnly && !options.noInstall) {
|
|
1622
|
+
const detection = detectLanguages(this.projectRoot);
|
|
1623
|
+
const needed = decideNeededLanguages(detection);
|
|
1624
|
+
const { missing } = resolveInstalledPlugins(needed);
|
|
1625
|
+
if (missing.length > 0) {
|
|
1626
|
+
if (options.yes) {
|
|
1627
|
+
console.error(`[ctxo] Installing missing plugins: ${missing.join(", ")}`);
|
|
1628
|
+
await new InstallCommand(this.projectRoot).run({ languages: missing, yes: true });
|
|
1629
|
+
} else {
|
|
1630
|
+
console.error(
|
|
1631
|
+
`[ctxo] Detected missing plugins: ${missing.join(", ")}. Run "ctxo install ${missing.join(" ")}" to install.`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
/* ---------------------------------------------------------------- */
|
|
1638
|
+
/* Dry run */
|
|
1639
|
+
/* ---------------------------------------------------------------- */
|
|
1640
|
+
dryRun(options) {
|
|
1641
|
+
const toolIds = options.tools ?? PLATFORMS.map((p) => p.id);
|
|
1642
|
+
console.error("[ctxo] Dry run \u2014 the following files would be created/updated:\n");
|
|
1643
|
+
if (!options.rulesOnly) {
|
|
1644
|
+
console.error(" .ctxo/index/ (index directory)");
|
|
1645
|
+
}
|
|
1646
|
+
for (const id of toolIds) {
|
|
1647
|
+
const platform = PLATFORMS.find((p) => p.id === id);
|
|
1648
|
+
if (platform) {
|
|
1649
|
+
const exists = existsSync11(join12(this.projectRoot, platform.file));
|
|
1650
|
+
const action = platform.mode === "append" && exists ? "append section" : "create";
|
|
1651
|
+
console.error(` ${platform.file.padEnd(42)} (${action})`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (!options.rulesOnly) {
|
|
1655
|
+
console.error(" .git/hooks/post-commit (git hook)");
|
|
1656
|
+
console.error(" .git/hooks/post-merge (git hook)");
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
/* ---------------------------------------------------------------- */
|
|
1660
|
+
/* Helpers */
|
|
1661
|
+
/* ---------------------------------------------------------------- */
|
|
1662
|
+
async getVersion() {
|
|
1663
|
+
try {
|
|
1664
|
+
const pkgPath = join12(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
|
|
1665
|
+
if (existsSync11(pkgPath)) {
|
|
1666
|
+
return JSON.parse(readFileSync7(pkgPath, "utf-8")).version;
|
|
1667
|
+
}
|
|
1668
|
+
const { createRequire: createRequire5 } = await import("module");
|
|
1669
|
+
const require4 = createRequire5(import.meta.url);
|
|
1670
|
+
return require4("../../package.json").version;
|
|
1671
|
+
} catch {
|
|
1672
|
+
return void 0;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
installHook(hooksDir, hookName, hookContent) {
|
|
1676
|
+
const hookPath = join12(hooksDir, hookName);
|
|
1677
|
+
let existing = "";
|
|
1678
|
+
if (existsSync11(hookPath)) {
|
|
1679
|
+
existing = readFileSync7(hookPath, "utf-8");
|
|
1680
|
+
if (existing.includes(CTXO_START)) return;
|
|
1681
|
+
} else {
|
|
1682
|
+
existing = "#!/bin/sh\n";
|
|
1683
|
+
}
|
|
1684
|
+
const updated = existing.endsWith("\n") ? existing + "\n" + hookContent + "\n" : existing + "\n\n" + hookContent + "\n";
|
|
1685
|
+
writeFileSync5(hookPath, updated, "utf-8");
|
|
1686
|
+
chmodSync(hookPath, 493);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
// src/cli/watch-command.ts
|
|
1691
|
+
import { join as join13, extname as extname4, relative as relative2 } from "path";
|
|
1692
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1693
|
+
|
|
1694
|
+
// src/adapters/watcher/chokidar-watcher-adapter.ts
|
|
1695
|
+
import { watch } from "chokidar";
|
|
1696
|
+
var DEFAULT_IGNORED = [
|
|
1697
|
+
"**/node_modules/**",
|
|
1698
|
+
"**/.git/**",
|
|
1699
|
+
"**/.ctxo/**",
|
|
1700
|
+
"**/dist/**",
|
|
1701
|
+
"**/coverage/**"
|
|
1702
|
+
];
|
|
1703
|
+
var ChokidarWatcherAdapter = class {
|
|
1704
|
+
projectRoot;
|
|
1705
|
+
ignored;
|
|
1706
|
+
watcher;
|
|
1707
|
+
constructor(projectRoot, ignored) {
|
|
1708
|
+
this.projectRoot = projectRoot;
|
|
1709
|
+
this.ignored = ignored ?? DEFAULT_IGNORED;
|
|
1710
|
+
}
|
|
1711
|
+
start(handler) {
|
|
1712
|
+
if (this.watcher) {
|
|
1713
|
+
throw new Error("Watcher already started. Call stop() before starting again.");
|
|
1714
|
+
}
|
|
1715
|
+
this.watcher = watch(this.projectRoot, {
|
|
1716
|
+
ignored: this.ignored,
|
|
1717
|
+
persistent: true,
|
|
1718
|
+
ignoreInitial: true
|
|
1719
|
+
});
|
|
1720
|
+
this.watcher.on("add", (path) => handler("add", path));
|
|
1721
|
+
this.watcher.on("change", (path) => handler("change", path));
|
|
1722
|
+
this.watcher.on("unlink", (path) => handler("unlink", path));
|
|
1723
|
+
}
|
|
1724
|
+
async stop() {
|
|
1725
|
+
if (this.watcher) {
|
|
1726
|
+
await this.watcher.close();
|
|
1727
|
+
this.watcher = void 0;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
// src/cli/watch-command.ts
|
|
1733
|
+
var DEBOUNCE_MS = 300;
|
|
1734
|
+
var WatchCommand = class {
|
|
1735
|
+
projectRoot;
|
|
1736
|
+
ctxoRoot;
|
|
1737
|
+
constructor(projectRoot) {
|
|
1738
|
+
this.projectRoot = projectRoot;
|
|
1739
|
+
this.ctxoRoot = join13(projectRoot, ".ctxo");
|
|
1740
|
+
}
|
|
1741
|
+
async run() {
|
|
1742
|
+
console.error("[ctxo] Starting file watcher...");
|
|
1743
|
+
const registry = new LanguageAdapterRegistry();
|
|
1744
|
+
let roslynAdapter = null;
|
|
1745
|
+
for (const { plugin, adapter } of await loadPlugins(this.projectRoot)) {
|
|
1746
|
+
if (typeof adapter.initialize === "function") {
|
|
1747
|
+
try {
|
|
1748
|
+
await adapter.initialize(this.projectRoot);
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
console.error(`[ctxo] Plugin ${plugin.id} initialize failed: ${err.message}`);
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
registry.register(plugin.extensions, adapter);
|
|
1755
|
+
const csCandidate = adapter;
|
|
1756
|
+
if (typeof csCandidate.getRoslynDelegate === "function") {
|
|
1757
|
+
const roslyn = csCandidate.getRoslynDelegate();
|
|
1758
|
+
if (roslyn?.isReady()) {
|
|
1759
|
+
const started = await roslyn.startKeepAlive();
|
|
1760
|
+
if (started) {
|
|
1761
|
+
roslynAdapter = roslyn;
|
|
1762
|
+
console.error("[ctxo] C# watch: Roslyn keep-alive active (full tier, <100ms per change)");
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
console.error(`[ctxo] Plugin ${plugin.id}@${plugin.version} (${plugin.tier} tier)`);
|
|
1767
|
+
}
|
|
1768
|
+
const supportedExtensions = registry.getSupportedExtensions();
|
|
1769
|
+
const writer = new JsonIndexWriter(this.ctxoRoot);
|
|
1770
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
1771
|
+
await storage.init();
|
|
1772
|
+
const hasher = new ContentHasher();
|
|
1773
|
+
const gitAdapter = new SimpleGitAdapter(this.projectRoot);
|
|
1774
|
+
const revertDetector = new RevertDetector();
|
|
1775
|
+
const watcher = new ChokidarWatcherAdapter(this.projectRoot);
|
|
1776
|
+
const pendingFiles = /* @__PURE__ */ new Map();
|
|
1777
|
+
const reindexFile = async (filePath) => {
|
|
1778
|
+
const adapter = registry.getAdapter(filePath);
|
|
1779
|
+
if (!adapter) return;
|
|
1780
|
+
const relativePath = relative2(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
1781
|
+
try {
|
|
1782
|
+
if (roslynAdapter?.isReady() && filePath.endsWith(".cs")) {
|
|
1783
|
+
const result = await roslynAdapter.reindexFile(relativePath);
|
|
1784
|
+
if (result) {
|
|
1785
|
+
const commits2 = await gitAdapter.getCommitHistory(relativePath);
|
|
1786
|
+
const intent2 = commits2.map((c) => ({
|
|
1787
|
+
hash: c.hash,
|
|
1788
|
+
message: c.message,
|
|
1789
|
+
date: c.date,
|
|
1790
|
+
kind: "commit"
|
|
1791
|
+
}));
|
|
1792
|
+
const antiPatterns2 = revertDetector.detect(commits2);
|
|
1793
|
+
const source2 = readFileSync8(filePath, "utf-8");
|
|
1794
|
+
const fileIndex2 = {
|
|
1795
|
+
file: relativePath,
|
|
1796
|
+
lastModified: Math.floor(Date.now() / 1e3),
|
|
1797
|
+
contentHash: hasher.hash(source2),
|
|
1798
|
+
symbols: result.symbols.map((s) => ({
|
|
1799
|
+
symbolId: s.symbolId,
|
|
1800
|
+
name: s.name,
|
|
1801
|
+
kind: s.kind,
|
|
1802
|
+
startLine: s.startLine,
|
|
1803
|
+
endLine: s.endLine
|
|
1804
|
+
})),
|
|
1805
|
+
edges: result.edges.filter((e) => !e.to.startsWith("ns:")).map((e) => ({
|
|
1806
|
+
from: e.from,
|
|
1807
|
+
to: e.to,
|
|
1808
|
+
kind: e.kind
|
|
1809
|
+
})),
|
|
1810
|
+
complexity: result.complexity.map((c) => ({
|
|
1811
|
+
symbolId: c.symbolId,
|
|
1812
|
+
cyclomatic: c.cyclomatic
|
|
1813
|
+
})),
|
|
1814
|
+
intent: intent2,
|
|
1815
|
+
antiPatterns: antiPatterns2
|
|
1816
|
+
};
|
|
1817
|
+
writer.write(fileIndex2);
|
|
1818
|
+
storage.writeSymbolFile(fileIndex2);
|
|
1819
|
+
console.error(`[ctxo] Re-indexed (Roslyn): ${relativePath}`);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const source = readFileSync8(filePath, "utf-8");
|
|
1824
|
+
const lastModified = Math.floor(Date.now() / 1e3);
|
|
1825
|
+
const symbols = await adapter.extractSymbols(relativePath, source);
|
|
1826
|
+
const edges = await adapter.extractEdges(relativePath, source);
|
|
1827
|
+
const complexity = await adapter.extractComplexity(relativePath, source);
|
|
1828
|
+
const commits = await gitAdapter.getCommitHistory(relativePath);
|
|
1829
|
+
const intent = commits.map((c) => ({
|
|
1830
|
+
hash: c.hash,
|
|
1831
|
+
message: c.message,
|
|
1832
|
+
date: c.date,
|
|
1833
|
+
kind: "commit"
|
|
1834
|
+
}));
|
|
1835
|
+
const antiPatterns = revertDetector.detect(commits);
|
|
1836
|
+
const fileIndex = {
|
|
1837
|
+
file: relativePath,
|
|
1838
|
+
lastModified,
|
|
1839
|
+
contentHash: hasher.hash(source),
|
|
1840
|
+
symbols,
|
|
1841
|
+
edges,
|
|
1842
|
+
complexity,
|
|
1843
|
+
intent,
|
|
1844
|
+
antiPatterns
|
|
1845
|
+
};
|
|
1846
|
+
writer.write(fileIndex);
|
|
1847
|
+
storage.writeSymbolFile(fileIndex);
|
|
1848
|
+
console.error(`[ctxo] Re-indexed: ${relativePath}`);
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
console.error(`[ctxo] Failed to re-index ${relativePath}: ${err.message}`);
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
const debouncedReindex = (filePath) => {
|
|
1854
|
+
const existing = pendingFiles.get(filePath);
|
|
1855
|
+
if (existing) clearTimeout(existing);
|
|
1856
|
+
pendingFiles.set(
|
|
1857
|
+
filePath,
|
|
1858
|
+
setTimeout(() => {
|
|
1859
|
+
pendingFiles.delete(filePath);
|
|
1860
|
+
reindexFile(filePath);
|
|
1861
|
+
}, DEBOUNCE_MS)
|
|
1862
|
+
);
|
|
1863
|
+
};
|
|
1864
|
+
watcher.start((event, filePath) => {
|
|
1865
|
+
if (event === "unlink") {
|
|
1866
|
+
const ext = extname4(filePath).toLowerCase();
|
|
1867
|
+
if (!supportedExtensions.has(ext)) return;
|
|
1868
|
+
const relativePath = relative2(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
1869
|
+
writer.delete(relativePath);
|
|
1870
|
+
storage.deleteSymbolFile(relativePath);
|
|
1871
|
+
console.error(`[ctxo] Removed from index: ${relativePath}`);
|
|
1872
|
+
} else {
|
|
1873
|
+
debouncedReindex(filePath);
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
console.error("[ctxo] Watching for file changes... (Ctrl+C to stop)");
|
|
1877
|
+
const cleanup = () => {
|
|
1878
|
+
console.error("\n[ctxo] Stopping watcher...");
|
|
1879
|
+
for (const timeout of pendingFiles.values()) {
|
|
1880
|
+
clearTimeout(timeout);
|
|
1881
|
+
}
|
|
1882
|
+
const roslynShutdown = roslynAdapter ? roslynAdapter.dispose() : Promise.resolve();
|
|
1883
|
+
roslynShutdown.then(() => {
|
|
1884
|
+
watcher.stop().then(() => {
|
|
1885
|
+
storage.close();
|
|
1886
|
+
console.error("[ctxo] Watcher stopped");
|
|
1887
|
+
process.exit(0);
|
|
1888
|
+
});
|
|
1889
|
+
});
|
|
1890
|
+
};
|
|
1891
|
+
process.on("SIGINT", cleanup);
|
|
1892
|
+
process.on("SIGTERM", cleanup);
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
// src/cli/stats-command.ts
|
|
1897
|
+
import { join as join14 } from "path";
|
|
1898
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
1899
|
+
import initSqlJs from "sql.js";
|
|
1900
|
+
var StatsCommand = class {
|
|
1901
|
+
ctxoRoot;
|
|
1902
|
+
constructor(projectRoot) {
|
|
1903
|
+
this.ctxoRoot = join14(projectRoot, ".ctxo");
|
|
1904
|
+
}
|
|
1905
|
+
async run(options) {
|
|
1906
|
+
if (!this.isStatsEnabled()) {
|
|
1907
|
+
console.error("[ctxo] Stats collection is disabled in .ctxo/config.yaml");
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
const dbPath = join14(this.ctxoRoot, ".cache", "symbols.db");
|
|
1911
|
+
if (!existsSync12(dbPath)) {
|
|
1912
|
+
if (options.json) {
|
|
1913
|
+
process.stdout.write(JSON.stringify(this.emptyReport(options.days)) + "\n");
|
|
1914
|
+
} else {
|
|
1915
|
+
console.error("[ctxo] No usage data yet. Start using Ctxo MCP tools to collect stats.");
|
|
1916
|
+
}
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const SQL = await initSqlJs();
|
|
1920
|
+
const buffer = readFileSync9(dbPath);
|
|
1921
|
+
const db = new SQL.Database(buffer);
|
|
1922
|
+
const recorder = new SessionRecorderAdapter(db);
|
|
1923
|
+
try {
|
|
1924
|
+
if (options.clear) {
|
|
1925
|
+
recorder.clear();
|
|
1926
|
+
const data = db.export();
|
|
1927
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
1928
|
+
writeFileSync7(dbPath, Buffer.from(data));
|
|
1929
|
+
console.error("[ctxo] Session data cleared.");
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
if (options.days != null) {
|
|
1933
|
+
if (!Number.isInteger(options.days) || options.days <= 0) {
|
|
1934
|
+
console.error("[ctxo] --days must be a positive integer");
|
|
1935
|
+
process.exit(1);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
const stats = recorder.queryStats(options.days);
|
|
1940
|
+
if (stats.totalCalls === 0) {
|
|
1941
|
+
if (options.json) {
|
|
1942
|
+
process.stdout.write(JSON.stringify(this.emptyReport(options.days)) + "\n");
|
|
1943
|
+
} else {
|
|
1944
|
+
console.error("[ctxo] No usage data yet. Start using Ctxo MCP tools to collect stats.");
|
|
1945
|
+
}
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
if (options.json) {
|
|
1949
|
+
process.stdout.write(JSON.stringify(this.buildReport(stats, options.days), null, 2) + "\n");
|
|
1950
|
+
} else {
|
|
1951
|
+
this.printHumanReadable(stats, options.days);
|
|
1952
|
+
}
|
|
1953
|
+
} finally {
|
|
1954
|
+
db.close();
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
isStatsEnabled() {
|
|
1958
|
+
const configPath = join14(this.ctxoRoot, "config.yaml");
|
|
1959
|
+
if (!existsSync12(configPath)) return true;
|
|
1960
|
+
try {
|
|
1961
|
+
const raw = readFileSync9(configPath, "utf-8");
|
|
1962
|
+
const match = raw.match(/stats:\s*\n\s*enabled:\s*(true|false)/);
|
|
1963
|
+
if (match) return match[1] !== "false";
|
|
1964
|
+
return true;
|
|
1965
|
+
} catch {
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
buildReport(stats, days) {
|
|
1970
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1971
|
+
const from = days != null ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
1972
|
+
return {
|
|
1973
|
+
timeRange: { from, to: now, daysFilter: days ?? null },
|
|
1974
|
+
summary: {
|
|
1975
|
+
totalCalls: stats.totalCalls,
|
|
1976
|
+
totalTokensServed: stats.totalTokensServed
|
|
1977
|
+
},
|
|
1978
|
+
topTools: stats.topTools,
|
|
1979
|
+
topSymbols: stats.topSymbols,
|
|
1980
|
+
detailLevelDistribution: stats.detailLevelDistribution.map((d) => ({
|
|
1981
|
+
level: d.level,
|
|
1982
|
+
count: d.count,
|
|
1983
|
+
percentage: d.percentage
|
|
1984
|
+
}))
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
emptyReport(days) {
|
|
1988
|
+
return this.buildReport({
|
|
1989
|
+
totalCalls: 0,
|
|
1990
|
+
totalTokensServed: 0,
|
|
1991
|
+
topTools: [],
|
|
1992
|
+
topSymbols: [],
|
|
1993
|
+
detailLevelDistribution: []
|
|
1994
|
+
}, days);
|
|
1995
|
+
}
|
|
1996
|
+
printHumanReadable(stats, days) {
|
|
1997
|
+
const header = days != null ? `last ${days} days` : "all time";
|
|
1998
|
+
console.error("");
|
|
1999
|
+
console.error(` Usage Summary (${header})`);
|
|
2000
|
+
console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2001
|
+
console.error(` Total tool calls: ${this.formatNumber(stats.totalCalls)}`);
|
|
2002
|
+
console.error(` Total tokens served: ${this.formatTokens(stats.totalTokensServed)}`);
|
|
2003
|
+
if (stats.topTools.length > 0) {
|
|
2004
|
+
console.error("");
|
|
2005
|
+
console.error(" Top Tools");
|
|
2006
|
+
console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2007
|
+
for (const t of stats.topTools) {
|
|
2008
|
+
const name = t.tool.padEnd(24);
|
|
2009
|
+
const calls = `${this.formatNumber(t.calls)} calls`.padEnd(14);
|
|
2010
|
+
console.error(` ${name}${calls}avg ${this.formatNumber(t.avgTokens)} tokens`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (stats.topSymbols.length > 0) {
|
|
2014
|
+
console.error("");
|
|
2015
|
+
console.error(" Top Queried Symbols");
|
|
2016
|
+
console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2017
|
+
for (const s of stats.topSymbols) {
|
|
2018
|
+
const name = s.name.padEnd(32);
|
|
2019
|
+
console.error(` ${name}${this.formatNumber(s.queries)} queries`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (stats.detailLevelDistribution.length > 0) {
|
|
2023
|
+
console.error("");
|
|
2024
|
+
console.error(" Detail Level Distribution");
|
|
2025
|
+
console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2026
|
+
for (const d of stats.detailLevelDistribution) {
|
|
2027
|
+
const bar = this.renderBar(d.percentage);
|
|
2028
|
+
const pct = `${d.percentage}%`.padStart(4);
|
|
2029
|
+
console.error(` ${d.level}: ${bar} ${pct}`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
console.error("");
|
|
2033
|
+
}
|
|
2034
|
+
renderBar(percentage) {
|
|
2035
|
+
const filled = Math.round(percentage / 10);
|
|
2036
|
+
const empty = 10 - filled;
|
|
2037
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
2038
|
+
}
|
|
2039
|
+
formatNumber(n) {
|
|
2040
|
+
return n.toLocaleString("en-US");
|
|
2041
|
+
}
|
|
2042
|
+
formatTokens(n) {
|
|
2043
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2044
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
2045
|
+
return String(n);
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
|
|
2049
|
+
// src/cli/doctor-command.ts
|
|
2050
|
+
import { join as join21 } from "path";
|
|
2051
|
+
import { existsSync as existsSync19 } from "fs";
|
|
2052
|
+
|
|
2053
|
+
// src/adapters/diagnostics/health-checker.ts
|
|
2054
|
+
var log3 = createLogger("ctxo:doctor");
|
|
2055
|
+
var HealthChecker = class {
|
|
2056
|
+
checks;
|
|
2057
|
+
constructor(checks) {
|
|
2058
|
+
this.checks = checks;
|
|
2059
|
+
}
|
|
2060
|
+
async runAll(ctx) {
|
|
2061
|
+
const settled = await Promise.allSettled(
|
|
2062
|
+
this.checks.map((check) => check.run(ctx))
|
|
2063
|
+
);
|
|
2064
|
+
const results = settled.map((outcome, i) => {
|
|
2065
|
+
const check = this.checks[i];
|
|
2066
|
+
if (outcome.status === "fulfilled") {
|
|
2067
|
+
const r = outcome.value;
|
|
2068
|
+
log3.info(`${r.id}: ${r.status.toUpperCase()} (${r.message})`);
|
|
2069
|
+
return r;
|
|
2070
|
+
}
|
|
2071
|
+
const message = outcome.reason?.message ?? "Unknown error";
|
|
2072
|
+
log3.error(`${check.id}: FAIL (${message})`);
|
|
2073
|
+
return {
|
|
2074
|
+
id: check.id,
|
|
2075
|
+
title: check.title,
|
|
2076
|
+
status: "fail",
|
|
2077
|
+
message: `Check crashed: ${message}`
|
|
2078
|
+
};
|
|
2079
|
+
});
|
|
2080
|
+
const summary = {
|
|
2081
|
+
pass: results.filter((r) => r.status === "pass").length,
|
|
2082
|
+
warn: results.filter((r) => r.status === "warn").length,
|
|
2083
|
+
fail: results.filter((r) => r.status === "fail").length
|
|
2084
|
+
};
|
|
2085
|
+
const exitCode = summary.fail > 0 ? 1 : 0;
|
|
2086
|
+
return { checks: results, summary, exitCode };
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
// src/adapters/diagnostics/doctor-reporter.ts
|
|
2091
|
+
var ICONS = {
|
|
2092
|
+
pass: "\u2713",
|
|
2093
|
+
warn: "\u26A0",
|
|
2094
|
+
fail: "\u2717"
|
|
2095
|
+
};
|
|
2096
|
+
var DoctorReporter = class {
|
|
2097
|
+
formatHuman(report) {
|
|
2098
|
+
const lines = ["ctxo doctor \u2014 Health Check", ""];
|
|
2099
|
+
for (const check of report.checks) {
|
|
2100
|
+
lines.push(this.formatCheckLine(check));
|
|
2101
|
+
}
|
|
2102
|
+
lines.push("");
|
|
2103
|
+
lines.push(` Summary: ${report.summary.pass} passed, ${report.summary.warn} warnings, ${report.summary.fail} failures`);
|
|
2104
|
+
return lines.join("\n");
|
|
2105
|
+
}
|
|
2106
|
+
formatQuiet(report) {
|
|
2107
|
+
const lines = [];
|
|
2108
|
+
for (const check of report.checks) {
|
|
2109
|
+
if (check.status === "warn" || check.status === "fail") {
|
|
2110
|
+
lines.push(this.formatCheckLine(check));
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (lines.length > 0) lines.push("");
|
|
2114
|
+
lines.push(` Summary: ${report.summary.pass} passed, ${report.summary.warn} warnings, ${report.summary.fail} failures`);
|
|
2115
|
+
return lines.join("\n");
|
|
2116
|
+
}
|
|
2117
|
+
formatJson(report) {
|
|
2118
|
+
return JSON.stringify({
|
|
2119
|
+
checks: report.checks.map((c) => ({
|
|
2120
|
+
name: c.id,
|
|
2121
|
+
status: c.status,
|
|
2122
|
+
value: c.value ?? null,
|
|
2123
|
+
message: c.message,
|
|
2124
|
+
...c.fix ? { fix: c.fix } : {}
|
|
2125
|
+
})),
|
|
2126
|
+
summary: report.summary,
|
|
2127
|
+
exitCode: report.exitCode
|
|
2128
|
+
}, null, 2);
|
|
2129
|
+
}
|
|
2130
|
+
formatCheckLine(check) {
|
|
2131
|
+
const icon = ICONS[check.status] ?? "?";
|
|
2132
|
+
const title = check.title.padEnd(24);
|
|
2133
|
+
const suffix = check.fix ? ` (${check.fix})` : "";
|
|
2134
|
+
return ` ${icon} ${title} ${check.message}${suffix}`;
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
|
|
2138
|
+
// src/adapters/diagnostics/checks/runtime-check.ts
|
|
2139
|
+
import { createRequire as createRequire3 } from "module";
|
|
2140
|
+
var require2 = createRequire3(import.meta.url);
|
|
2141
|
+
function pass(id, title, message) {
|
|
2142
|
+
return { id, title, status: "pass", message };
|
|
2143
|
+
}
|
|
2144
|
+
function warn(id, title, message, fix) {
|
|
2145
|
+
return { id, title, status: "warn", message, fix };
|
|
2146
|
+
}
|
|
2147
|
+
function checkNodeVersion(version) {
|
|
2148
|
+
const id = "node_version";
|
|
2149
|
+
const title = "Node.js version";
|
|
2150
|
+
const major = parseInt(version.slice(1).split(".")[0] ?? "0", 10);
|
|
2151
|
+
if (major >= 20) return { id, title, status: "pass", message: `Node.js ${version} (required: \u226520)`, value: version };
|
|
2152
|
+
if (major >= 18) return { id, title, status: "warn", message: `Node.js ${version} \u2014 v20+ recommended`, fix: "Upgrade to Node.js 20 or later", value: version };
|
|
2153
|
+
return { id, title, status: "fail", message: `Node.js ${version} \u2014 v20+ required`, fix: "Upgrade to Node.js 20 or later", value: version };
|
|
2154
|
+
}
|
|
2155
|
+
var NodeVersionCheck = class {
|
|
2156
|
+
id = "node_version";
|
|
2157
|
+
title = "Node.js version";
|
|
2158
|
+
async run(_ctx) {
|
|
2159
|
+
return checkNodeVersion(process.version);
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
var TsMorphCheck = class {
|
|
2163
|
+
id = "ts_morph";
|
|
2164
|
+
title = "TypeScript plugin (@ctxo/lang-typescript)";
|
|
2165
|
+
async run(_ctx) {
|
|
2166
|
+
if (tryResolve("@ctxo/lang-typescript")) {
|
|
2167
|
+
return pass(this.id, this.title, "available");
|
|
2168
|
+
}
|
|
2169
|
+
return warn(
|
|
2170
|
+
this.id,
|
|
2171
|
+
this.title,
|
|
2172
|
+
"@ctxo/lang-typescript not installed \u2014 TypeScript/JavaScript indexing disabled",
|
|
2173
|
+
'Run "npm install @ctxo/lang-typescript" (or "ctxo install typescript")'
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
var TreeSitterCheck = class {
|
|
2178
|
+
id = "tree_sitter";
|
|
2179
|
+
title = "Go / C# plugins (@ctxo/lang-go, @ctxo/lang-csharp)";
|
|
2180
|
+
async run(_ctx) {
|
|
2181
|
+
const goOk = tryResolve("@ctxo/lang-go");
|
|
2182
|
+
const csOk = tryResolve("@ctxo/lang-csharp");
|
|
2183
|
+
if (goOk && csOk) return pass(this.id, this.title, "both plugins available");
|
|
2184
|
+
if (goOk || csOk) {
|
|
2185
|
+
const missing = goOk ? "@ctxo/lang-csharp" : "@ctxo/lang-go";
|
|
2186
|
+
return warn(
|
|
2187
|
+
this.id,
|
|
2188
|
+
this.title,
|
|
2189
|
+
`${missing} not installed`,
|
|
2190
|
+
`Run "npm install ${missing}" (or "ctxo install ${missing === "@ctxo/lang-go" ? "go" : "csharp"}")`
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
return warn(
|
|
2194
|
+
this.id,
|
|
2195
|
+
this.title,
|
|
2196
|
+
"No Go/C# plugin installed \u2014 extended language indexing disabled",
|
|
2197
|
+
'Run "npm install @ctxo/lang-go @ctxo/lang-csharp"'
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
function tryResolve(packageName) {
|
|
2202
|
+
try {
|
|
2203
|
+
require2.resolve(`${packageName}/package.json`);
|
|
2204
|
+
return true;
|
|
2205
|
+
} catch {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// src/cli/version-command.ts
|
|
2211
|
+
import { SUPPORTED_API_VERSION as SUPPORTED_API_VERSION2 } from "@ctxo/plugin-api";
|
|
2212
|
+
function gatherVersionInfo(discovered) {
|
|
2213
|
+
const plugins = discovered.map(({ plugin, specifier }) => ({
|
|
2214
|
+
name: specifier,
|
|
2215
|
+
id: plugin.id,
|
|
2216
|
+
version: plugin.version,
|
|
2217
|
+
apiVersion: plugin.apiVersion,
|
|
2218
|
+
compatible: plugin.apiVersion === SUPPORTED_API_VERSION2
|
|
2219
|
+
}));
|
|
2220
|
+
return {
|
|
2221
|
+
ctxo: getVersion(),
|
|
2222
|
+
pluginApiVersion: SUPPORTED_API_VERSION2,
|
|
2223
|
+
plugins,
|
|
2224
|
+
runtime: {
|
|
2225
|
+
node: process.version,
|
|
2226
|
+
platform: process.platform,
|
|
2227
|
+
arch: process.arch
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
function formatShort(info) {
|
|
2232
|
+
return `ctxo ${info.ctxo}`;
|
|
2233
|
+
}
|
|
2234
|
+
function formatVerbose(info) {
|
|
2235
|
+
const lines = [];
|
|
2236
|
+
lines.push(`ctxo ${info.ctxo}`);
|
|
2237
|
+
lines.push(`Plugin API: ${info.pluginApiVersion}`);
|
|
2238
|
+
lines.push(`Runtime: Node ${info.runtime.node} on ${info.runtime.platform}/${info.runtime.arch}`);
|
|
2239
|
+
lines.push("");
|
|
2240
|
+
if (info.plugins.length === 0) {
|
|
2241
|
+
lines.push("Plugins: (none installed)");
|
|
2242
|
+
lines.push("");
|
|
2243
|
+
lines.push('Install a language plugin, e.g. "npm i -D @ctxo/lang-typescript"');
|
|
2244
|
+
return lines.join("\n");
|
|
2245
|
+
}
|
|
2246
|
+
const nameCol = Math.max(7, ...info.plugins.map((p) => p.name.length));
|
|
2247
|
+
const verCol = Math.max(7, ...info.plugins.map((p) => p.version.length));
|
|
2248
|
+
const apiCol = Math.max(3, ...info.plugins.map((p) => p.apiVersion.length));
|
|
2249
|
+
lines.push(`Plugins: ${info.plugins.length} installed`);
|
|
2250
|
+
lines.push(
|
|
2251
|
+
` ${"NAME".padEnd(nameCol)} ${"VERSION".padEnd(verCol)} ${"API".padEnd(apiCol)} STATUS`
|
|
2252
|
+
);
|
|
2253
|
+
for (const p of info.plugins) {
|
|
2254
|
+
const status = p.compatible ? "OK" : `incompatible (core wants ${info.pluginApiVersion})`;
|
|
2255
|
+
lines.push(
|
|
2256
|
+
` ${p.name.padEnd(nameCol)} ${p.version.padEnd(verCol)} ${p.apiVersion.padEnd(apiCol)} ${status}`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
return lines.join("\n");
|
|
2260
|
+
}
|
|
2261
|
+
function formatJson(info) {
|
|
2262
|
+
return JSON.stringify(info, null, 2);
|
|
2263
|
+
}
|
|
2264
|
+
var VersionCommand = class {
|
|
2265
|
+
constructor(projectRoot) {
|
|
2266
|
+
this.projectRoot = projectRoot;
|
|
2267
|
+
}
|
|
2268
|
+
projectRoot;
|
|
2269
|
+
async run(options = {}) {
|
|
2270
|
+
const info = await this.collect();
|
|
2271
|
+
if (options.json) {
|
|
2272
|
+
process.stdout.write(formatJson(info) + "\n");
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (options.verbose) {
|
|
2276
|
+
process.stdout.write(formatVerbose(info) + "\n");
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
process.stdout.write(formatShort(info) + "\n");
|
|
2280
|
+
}
|
|
2281
|
+
async collect() {
|
|
2282
|
+
const manifestPath = loadManifestPath(this.projectRoot);
|
|
2283
|
+
if (!manifestPath) {
|
|
2284
|
+
return gatherVersionInfo([]);
|
|
2285
|
+
}
|
|
2286
|
+
const { plugins } = await discoverPlugins({ manifestPath });
|
|
2287
|
+
return gatherVersionInfo(plugins);
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
|
|
2291
|
+
// src/adapters/diagnostics/checks/versions-check.ts
|
|
2292
|
+
var VersionsCheck = class {
|
|
2293
|
+
id = "versions";
|
|
2294
|
+
title = "Versions";
|
|
2295
|
+
async run(ctx) {
|
|
2296
|
+
const info = await collectInfo(ctx);
|
|
2297
|
+
const incompatible = info.plugins.filter((p) => !p.compatible);
|
|
2298
|
+
const message = buildSummary(info);
|
|
2299
|
+
if (incompatible.length > 0) {
|
|
2300
|
+
return {
|
|
2301
|
+
id: this.id,
|
|
2302
|
+
title: this.title,
|
|
2303
|
+
status: "warn",
|
|
2304
|
+
message,
|
|
2305
|
+
fix: `Upgrade or replace plugins with apiVersion != ${info.pluginApiVersion}: ${incompatible.map((p) => p.name).join(", ")}`,
|
|
2306
|
+
value: formatVerbose(info)
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
return {
|
|
2310
|
+
id: this.id,
|
|
2311
|
+
title: this.title,
|
|
2312
|
+
status: "pass",
|
|
2313
|
+
message,
|
|
2314
|
+
value: formatVerbose(info)
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
async function collectInfo(ctx) {
|
|
2319
|
+
const manifestPath = loadManifestPath(ctx.projectRoot);
|
|
2320
|
+
if (!manifestPath) return gatherVersionInfo([]);
|
|
2321
|
+
const { plugins } = await discoverPlugins({ manifestPath });
|
|
2322
|
+
return gatherVersionInfo(plugins);
|
|
2323
|
+
}
|
|
2324
|
+
function buildSummary(info) {
|
|
2325
|
+
const pluginCount = info.plugins.length;
|
|
2326
|
+
const pluginLabel = pluginCount === 0 ? "no plugins" : `${pluginCount} plugin${pluginCount === 1 ? "" : "s"}`;
|
|
2327
|
+
return `ctxo ${info.ctxo} (API ${info.pluginApiVersion}) \u2014 ${pluginLabel}, Node ${info.runtime.node}`;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/adapters/diagnostics/checks/language-coverage-check.ts
|
|
2331
|
+
import { createRequire as createRequire4 } from "module";
|
|
2332
|
+
var require3 = createRequire4(import.meta.url);
|
|
2333
|
+
var LanguageCoverageCheck = class {
|
|
2334
|
+
id = "language_coverage";
|
|
2335
|
+
title = "Language coverage";
|
|
2336
|
+
async run(ctx) {
|
|
2337
|
+
const detection = detectLanguages(ctx.projectRoot);
|
|
2338
|
+
const needed = decideNeededLanguages(detection);
|
|
2339
|
+
if (needed.length === 0) {
|
|
2340
|
+
return {
|
|
2341
|
+
id: this.id,
|
|
2342
|
+
title: this.title,
|
|
2343
|
+
status: "pass",
|
|
2344
|
+
message: "no source languages detected"
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
const missing = [];
|
|
2348
|
+
for (const lang of needed) {
|
|
2349
|
+
if (!pluginResolves(officialPluginFor(lang))) {
|
|
2350
|
+
missing.push(lang);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
if (missing.length === 0) {
|
|
2354
|
+
const summary = needed.map((l) => `${l}`).join(", ");
|
|
2355
|
+
return {
|
|
2356
|
+
id: this.id,
|
|
2357
|
+
title: this.title,
|
|
2358
|
+
status: "pass",
|
|
2359
|
+
message: `plugins installed for: ${summary}`
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
const packages = missing.map(officialPluginFor).join(" ");
|
|
2363
|
+
const shortList = missing.join(", ");
|
|
2364
|
+
return {
|
|
2365
|
+
id: this.id,
|
|
2366
|
+
title: this.title,
|
|
2367
|
+
status: "warn",
|
|
2368
|
+
message: `missing plugins for: ${shortList}`,
|
|
2369
|
+
fix: `Run "ctxo install ${missing.join(" ")}" or "npm install -D ${packages}"`
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
function pluginResolves(packageName) {
|
|
2374
|
+
try {
|
|
2375
|
+
require3.resolve(`${packageName}/package.json`);
|
|
2376
|
+
return true;
|
|
2377
|
+
} catch {
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// src/adapters/diagnostics/checks/git-check.ts
|
|
2383
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
2384
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2385
|
+
import { join as join15 } from "path";
|
|
2386
|
+
var GitBinaryCheck = class {
|
|
2387
|
+
id = "git_binary";
|
|
2388
|
+
title = "Git available";
|
|
2389
|
+
async run(_ctx) {
|
|
2390
|
+
try {
|
|
2391
|
+
const version = execFileSync4("git", ["--version"], { encoding: "utf-8" }).trim();
|
|
2392
|
+
return { id: this.id, title: this.title, status: "pass", message: version, value: version };
|
|
2393
|
+
} catch {
|
|
2394
|
+
return { id: this.id, title: this.title, status: "fail", message: "git not found in PATH", fix: "Install git and ensure it is in your PATH" };
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
var GitRepoCheck = class {
|
|
2399
|
+
id = "git_repo";
|
|
2400
|
+
title = "Git repository";
|
|
2401
|
+
async run(ctx) {
|
|
2402
|
+
if (existsSync13(join15(ctx.projectRoot, ".git"))) {
|
|
2403
|
+
return { id: this.id, title: this.title, status: "pass", message: ctx.projectRoot, value: ctx.projectRoot };
|
|
2404
|
+
}
|
|
2405
|
+
return { id: this.id, title: this.title, status: "fail", message: "Not a git repository", fix: 'Run "git init" or clone a repository' };
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// src/adapters/diagnostics/checks/index-check.ts
|
|
2410
|
+
import { existsSync as existsSync14 } from "fs";
|
|
2411
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
2412
|
+
import { join as join16 } from "path";
|
|
2413
|
+
function pass2(id, title, message, value) {
|
|
2414
|
+
return { id, title, status: "pass", message, value };
|
|
2415
|
+
}
|
|
2416
|
+
function warn2(id, title, message, fix, value) {
|
|
2417
|
+
return { id, title, status: "warn", message, fix, value };
|
|
2418
|
+
}
|
|
2419
|
+
function fail(id, title, message, fix, value) {
|
|
2420
|
+
return { id, title, status: "fail", message, fix, value };
|
|
2421
|
+
}
|
|
2422
|
+
function getIndices(ctx) {
|
|
2423
|
+
return ctx.indices ?? new JsonIndexReader(ctx.ctxoRoot).readAll();
|
|
2424
|
+
}
|
|
2425
|
+
var IndexDirectoryCheck = class {
|
|
2426
|
+
id = "index_directory";
|
|
2427
|
+
title = "Index directory";
|
|
2428
|
+
async run(ctx) {
|
|
2429
|
+
const indexDir = join16(ctx.ctxoRoot, "index");
|
|
2430
|
+
if (!existsSync14(indexDir)) {
|
|
2431
|
+
return fail(this.id, this.title, "No index directory", 'Run "ctxo index"');
|
|
2432
|
+
}
|
|
2433
|
+
const indices = getIndices(ctx);
|
|
2434
|
+
if (indices.length === 0) {
|
|
2435
|
+
return fail(this.id, this.title, "Index directory empty", 'Run "ctxo index"');
|
|
2436
|
+
}
|
|
2437
|
+
return pass2(this.id, this.title, `${indices.length} files indexed`, String(indices.length));
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
var IndexFreshnessCheck = class {
|
|
2441
|
+
id = "index_freshness";
|
|
2442
|
+
title = "Index freshness";
|
|
2443
|
+
async run(ctx) {
|
|
2444
|
+
const indices = getIndices(ctx);
|
|
2445
|
+
if (indices.length === 0) {
|
|
2446
|
+
return fail(this.id, this.title, "No indexed files to check", 'Run "ctxo index"');
|
|
2447
|
+
}
|
|
2448
|
+
const indexedFiles = indices.map((idx) => idx.file);
|
|
2449
|
+
const staleness = new StalenessDetector(ctx.projectRoot, ctx.ctxoRoot);
|
|
2450
|
+
const warning = staleness.check(indexedFiles);
|
|
2451
|
+
if (!warning) {
|
|
2452
|
+
return pass2(this.id, this.title, `All ${indices.length} files fresh`, `0/${indices.length} stale`);
|
|
2453
|
+
}
|
|
2454
|
+
const staleCount = warning.staleFiles.length;
|
|
2455
|
+
const pct = staleCount / indices.length;
|
|
2456
|
+
const value = `${staleCount}/${indices.length} stale`;
|
|
2457
|
+
if (pct <= 0.1) {
|
|
2458
|
+
return warn2(this.id, this.title, `${staleCount} of ${indices.length} files stale`, 'Run "ctxo index"', value);
|
|
2459
|
+
}
|
|
2460
|
+
return fail(this.id, this.title, `${staleCount} of ${indices.length} files stale (${Math.round(pct * 100)}%)`, 'Run "ctxo index"', value);
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
var SymbolCountCheck = class {
|
|
2464
|
+
id = "symbol_count";
|
|
2465
|
+
title = "Symbol count";
|
|
2466
|
+
async run(ctx) {
|
|
2467
|
+
const indices = getIndices(ctx);
|
|
2468
|
+
const total = indices.reduce((sum, idx) => sum + idx.symbols.length, 0);
|
|
2469
|
+
if (total > 0) {
|
|
2470
|
+
return pass2(this.id, this.title, `${total.toLocaleString()} symbols`, String(total));
|
|
2471
|
+
}
|
|
2472
|
+
return fail(this.id, this.title, "0 symbols (index empty)", 'Run "ctxo index"', "0");
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
var EdgeCountCheck = class {
|
|
2476
|
+
id = "edge_count";
|
|
2477
|
+
title = "Edge count";
|
|
2478
|
+
async run(ctx) {
|
|
2479
|
+
const indices = getIndices(ctx);
|
|
2480
|
+
const total = indices.reduce((sum, idx) => sum + idx.edges.length, 0);
|
|
2481
|
+
if (total > 0) {
|
|
2482
|
+
return pass2(this.id, this.title, `${total.toLocaleString()} edges`, String(total));
|
|
2483
|
+
}
|
|
2484
|
+
return warn2(this.id, this.title, "0 edges (possible isolated files)", void 0, "0");
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
var OrphanedFilesCheck = class {
|
|
2488
|
+
id = "orphaned_files";
|
|
2489
|
+
title = "Orphaned index files";
|
|
2490
|
+
async run(ctx) {
|
|
2491
|
+
const indices = getIndices(ctx);
|
|
2492
|
+
if (indices.length === 0) {
|
|
2493
|
+
return pass2(this.id, this.title, "no index files to check");
|
|
2494
|
+
}
|
|
2495
|
+
const sourceFiles = this.getSourceFiles(ctx.projectRoot);
|
|
2496
|
+
if (sourceFiles.size === 0) {
|
|
2497
|
+
return pass2(this.id, this.title, "none (git ls-files unavailable)");
|
|
2498
|
+
}
|
|
2499
|
+
const orphaned = indices.filter((idx) => !sourceFiles.has(idx.file));
|
|
2500
|
+
if (orphaned.length === 0) {
|
|
2501
|
+
return pass2(this.id, this.title, "none", "0");
|
|
2502
|
+
}
|
|
2503
|
+
if (orphaned.length <= 5) {
|
|
2504
|
+
return warn2(this.id, this.title, `${orphaned.length} orphaned index files`, "Delete orphaned files from .ctxo/index/", String(orphaned.length));
|
|
2505
|
+
}
|
|
2506
|
+
return fail(this.id, this.title, `${orphaned.length} orphaned index files`, "Delete orphaned files from .ctxo/index/", String(orphaned.length));
|
|
2507
|
+
}
|
|
2508
|
+
getSourceFiles(projectRoot) {
|
|
2509
|
+
try {
|
|
2510
|
+
const output = execFileSync5("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
2511
|
+
cwd: projectRoot,
|
|
2512
|
+
encoding: "utf-8"
|
|
2513
|
+
});
|
|
2514
|
+
return new Set(output.split("\n").map((l) => l.trim()).filter((l) => l.length > 0));
|
|
2515
|
+
} catch {
|
|
2516
|
+
return /* @__PURE__ */ new Set();
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
var CoChangesCacheCheck = class {
|
|
2521
|
+
id = "co_changes_cache";
|
|
2522
|
+
title = "Co-changes cache";
|
|
2523
|
+
async run(ctx) {
|
|
2524
|
+
const coChangesPath = join16(ctx.ctxoRoot, "index", "co-changes.json");
|
|
2525
|
+
if (existsSync14(coChangesPath)) {
|
|
2526
|
+
return pass2(this.id, this.title, "present");
|
|
2527
|
+
}
|
|
2528
|
+
return warn2(this.id, this.title, "missing (co-change analysis disabled)", 'Run "ctxo index" with git history');
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
var SchemaVersionCheck = class {
|
|
2532
|
+
id = "schema_version";
|
|
2533
|
+
title = "Schema version";
|
|
2534
|
+
async run(ctx) {
|
|
2535
|
+
const manager = new SchemaManager(ctx.ctxoRoot);
|
|
2536
|
+
const stored = manager.readStoredVersion();
|
|
2537
|
+
const current = manager.currentVersion();
|
|
2538
|
+
if (!stored) {
|
|
2539
|
+
return fail(this.id, this.title, "No schema version found", 'Run "ctxo index"');
|
|
2540
|
+
}
|
|
2541
|
+
if (manager.isCompatible()) {
|
|
2542
|
+
return pass2(this.id, this.title, `${stored} (current)`, stored);
|
|
2543
|
+
}
|
|
2544
|
+
return fail(this.id, this.title, `${stored} (expected ${current})`, 'Run "ctxo index" to re-index with current schema', stored);
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
// src/adapters/diagnostics/checks/storage-check.ts
|
|
2549
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
|
|
2550
|
+
import { join as join17 } from "path";
|
|
2551
|
+
var SqliteCacheCheck = class {
|
|
2552
|
+
id = "sqlite_cache";
|
|
2553
|
+
title = "SQLite cache";
|
|
2554
|
+
async run(ctx) {
|
|
2555
|
+
const dbPath = join17(ctx.ctxoRoot, ".cache", "symbols.db");
|
|
2556
|
+
if (!existsSync15(dbPath)) {
|
|
2557
|
+
return { id: this.id, title: this.title, status: "warn", message: "SQLite cache missing", fix: 'Run "ctxo sync" to rebuild' };
|
|
2558
|
+
}
|
|
2559
|
+
try {
|
|
2560
|
+
const { default: initSqlJs2 } = await import("sql.js");
|
|
2561
|
+
const SQL = await initSqlJs2();
|
|
2562
|
+
const buffer = readFileSync10(dbPath);
|
|
2563
|
+
const db = new SQL.Database(new Uint8Array(buffer));
|
|
2564
|
+
const result = db.exec("PRAGMA integrity_check");
|
|
2565
|
+
db.close();
|
|
2566
|
+
const firstRow = result[0]?.values[0];
|
|
2567
|
+
if (firstRow && firstRow[0] === "ok") {
|
|
2568
|
+
return { id: this.id, title: this.title, status: "pass", message: "integrity_check passed" };
|
|
2569
|
+
}
|
|
2570
|
+
return { id: this.id, title: this.title, status: "fail", message: `SQLite corrupt: ${String(firstRow?.[0])}`, fix: 'Run "ctxo sync" to rebuild' };
|
|
2571
|
+
} catch (err) {
|
|
2572
|
+
return { id: this.id, title: this.title, status: "fail", message: `SQLite unreadable: ${err.message}`, fix: 'Run "ctxo sync" to rebuild' };
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
|
|
2577
|
+
// src/adapters/diagnostics/checks/config-check.ts
|
|
2578
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
|
|
2579
|
+
import { join as join18 } from "path";
|
|
2580
|
+
var ConfigFileCheck = class {
|
|
2581
|
+
id = "config_file";
|
|
2582
|
+
title = "Config file";
|
|
2583
|
+
async run(ctx) {
|
|
2584
|
+
const configPath = join18(ctx.ctxoRoot, "config.yaml");
|
|
2585
|
+
if (!existsSync16(configPath)) {
|
|
2586
|
+
return { id: this.id, title: this.title, status: "warn", message: "No config.yaml (using defaults)" };
|
|
2587
|
+
}
|
|
2588
|
+
try {
|
|
2589
|
+
const content = readFileSync11(configPath, "utf-8");
|
|
2590
|
+
if (content.trim().length === 0) {
|
|
2591
|
+
return { id: this.id, title: this.title, status: "warn", message: "config.yaml is empty (using defaults)" };
|
|
2592
|
+
}
|
|
2593
|
+
if (content.includes(" ")) {
|
|
2594
|
+
return { id: this.id, title: this.title, status: "fail", message: "Invalid config.yaml: tabs are not allowed in YAML", fix: "Replace tabs with spaces in .ctxo/config.yaml" };
|
|
2595
|
+
}
|
|
2596
|
+
return { id: this.id, title: this.title, status: "pass", message: ".ctxo/config.yaml valid" };
|
|
2597
|
+
} catch (err) {
|
|
2598
|
+
return { id: this.id, title: this.title, status: "fail", message: `Cannot read config.yaml: ${err.message}`, fix: "Check file permissions for .ctxo/config.yaml" };
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
// src/adapters/diagnostics/checks/disk-check.ts
|
|
2604
|
+
import { existsSync as existsSync17, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2605
|
+
import { join as join19 } from "path";
|
|
2606
|
+
var DEFAULT_THRESHOLDS = { warnMb: 100, failMb: 500 };
|
|
2607
|
+
var DiskUsageCheck = class {
|
|
2608
|
+
id = "disk_usage";
|
|
2609
|
+
title = "Disk usage";
|
|
2610
|
+
thresholds;
|
|
2611
|
+
constructor(thresholds = DEFAULT_THRESHOLDS) {
|
|
2612
|
+
this.thresholds = thresholds;
|
|
2613
|
+
}
|
|
2614
|
+
async run(ctx) {
|
|
2615
|
+
if (!existsSync17(ctx.ctxoRoot)) {
|
|
2616
|
+
return { id: this.id, title: this.title, status: "pass", message: "no .ctxo/ directory", value: "0" };
|
|
2617
|
+
}
|
|
2618
|
+
const bytes = this.getDirSize(ctx.ctxoRoot);
|
|
2619
|
+
const mb = bytes / (1024 * 1024);
|
|
2620
|
+
const formatted = mb < 1 ? `${Math.round(bytes / 1024)} KB` : `${mb.toFixed(1)} MB`;
|
|
2621
|
+
if (mb < this.thresholds.warnMb) {
|
|
2622
|
+
return { id: this.id, title: this.title, status: "pass", message: formatted, value: formatted };
|
|
2623
|
+
}
|
|
2624
|
+
if (mb < this.thresholds.failMb) {
|
|
2625
|
+
return { id: this.id, title: this.title, status: "warn", message: `${formatted} \u2014 consider pruning orphaned index files`, value: formatted };
|
|
2626
|
+
}
|
|
2627
|
+
return { id: this.id, title: this.title, status: "fail", message: `${formatted} \u2014 unusually large`, fix: "Check for stale data in .ctxo/", value: formatted };
|
|
2628
|
+
}
|
|
2629
|
+
getDirSize(dirPath) {
|
|
2630
|
+
let total = 0;
|
|
2631
|
+
try {
|
|
2632
|
+
const entries = readdirSync3(dirPath, { withFileTypes: true, recursive: true });
|
|
2633
|
+
for (const entry of entries) {
|
|
2634
|
+
if (entry.isFile()) {
|
|
2635
|
+
try {
|
|
2636
|
+
const parent = entry.parentPath ?? entry.path ?? dirPath;
|
|
2637
|
+
total += statSync2(join19(parent, entry.name)).size;
|
|
2638
|
+
} catch {
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
} catch {
|
|
2643
|
+
}
|
|
2644
|
+
return total;
|
|
2645
|
+
}
|
|
2646
|
+
};
|
|
2647
|
+
|
|
2648
|
+
// src/cli/doctor-fix.ts
|
|
2649
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2650
|
+
import { appendFileSync, existsSync as existsSync18, mkdirSync as mkdirSync5, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
2651
|
+
import { dirname as dirname7, join as join20 } from "path";
|
|
2652
|
+
async function applyFixes(report, options) {
|
|
2653
|
+
if (isCI(process.env) && !options.yes && !options.dryRun) {
|
|
2654
|
+
return {
|
|
2655
|
+
attempts: [],
|
|
2656
|
+
halted: true,
|
|
2657
|
+
reason: "CI environment detected; refuse to mutate without --yes"
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
const attempts = [];
|
|
2661
|
+
const fixableIds = new Set(report.checks.filter((c) => c.status !== "pass").map((c) => c.id));
|
|
2662
|
+
let configOk = true;
|
|
2663
|
+
if (fixableIds.has("config_file")) {
|
|
2664
|
+
const attempt = await run(
|
|
2665
|
+
"Config",
|
|
2666
|
+
"create from template",
|
|
2667
|
+
() => fixConfig(options.projectRoot),
|
|
2668
|
+
options
|
|
2669
|
+
);
|
|
2670
|
+
attempts.push(attempt);
|
|
2671
|
+
configOk = attempt.status === "success" || attempt.status === "skipped" || attempt.status === "planned";
|
|
2672
|
+
}
|
|
2673
|
+
if (!configOk) {
|
|
2674
|
+
return finish(attempts, options, true, "Config fix failed; halting before hooks/plugins/index");
|
|
2675
|
+
}
|
|
2676
|
+
if (fixableIds.has("git_hooks") || fixableIds.has("hooks")) {
|
|
2677
|
+
attempts.push(
|
|
2678
|
+
await run(
|
|
2679
|
+
"Git hooks",
|
|
2680
|
+
"re-install ctxo git hooks",
|
|
2681
|
+
() => fixHooks(options.projectRoot),
|
|
2682
|
+
options
|
|
2683
|
+
)
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
let pluginsOk = true;
|
|
2687
|
+
if (fixableIds.has("language_coverage") || hasMissingPlugins(options.projectRoot)) {
|
|
2688
|
+
const attempt = await run(
|
|
2689
|
+
"Language plugins",
|
|
2690
|
+
"install missing @ctxo/lang-* packages",
|
|
2691
|
+
() => fixLanguageCoverage(options.projectRoot, options.dryRun ?? false),
|
|
2692
|
+
options
|
|
2693
|
+
);
|
|
2694
|
+
attempts.push(attempt);
|
|
2695
|
+
pluginsOk = attempt.status === "success" || attempt.status === "skipped" || attempt.status === "planned";
|
|
2696
|
+
}
|
|
2697
|
+
if (pluginsOk) {
|
|
2698
|
+
if (fixableIds.has("index_directory") || fixableIds.has("index_freshness") || fixableIds.has("symbol_count")) {
|
|
2699
|
+
attempts.push(
|
|
2700
|
+
await run(
|
|
2701
|
+
"Index",
|
|
2702
|
+
"rebuild JSON index",
|
|
2703
|
+
() => fixIndex(options.projectRoot, options.dryRun ?? false),
|
|
2704
|
+
options
|
|
2705
|
+
)
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
if (fixableIds.has("orphaned_files")) {
|
|
2709
|
+
attempts.push(
|
|
2710
|
+
await run(
|
|
2711
|
+
"Orphaned index files",
|
|
2712
|
+
"delete index entries without a matching source file",
|
|
2713
|
+
() => pruneOrphans(options.projectRoot, options.ctxoRoot, options.dryRun ?? false),
|
|
2714
|
+
options
|
|
2715
|
+
)
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
} else {
|
|
2719
|
+
attempts.push({ name: "Index", action: "rebuild JSON index", status: "skipped", error: "plugin install failed" });
|
|
2720
|
+
}
|
|
2721
|
+
if (fixableIds.has("sqlite_cache") || fixableIds.has("schema_version") || fixableIds.has("co_changes_cache")) {
|
|
2722
|
+
attempts.push(
|
|
2723
|
+
await run(
|
|
2724
|
+
"SQLite cache",
|
|
2725
|
+
"delete .ctxo/.cache and resync",
|
|
2726
|
+
() => fixSqliteCache(options.projectRoot, options.ctxoRoot, options.dryRun ?? false),
|
|
2727
|
+
options
|
|
2728
|
+
)
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
return finish(attempts, options, false);
|
|
2732
|
+
}
|
|
2733
|
+
function finish(attempts, options, halted, reason) {
|
|
2734
|
+
writeLog(options.ctxoRoot, attempts, halted, reason);
|
|
2735
|
+
return reason ? { attempts, halted, reason } : { attempts, halted };
|
|
2736
|
+
}
|
|
2737
|
+
async function run(name, action, body, options) {
|
|
2738
|
+
if (options.dryRun) {
|
|
2739
|
+
return { name, action, status: "planned" };
|
|
2740
|
+
}
|
|
2741
|
+
const start = Date.now();
|
|
2742
|
+
try {
|
|
2743
|
+
await body();
|
|
2744
|
+
return { name, action, status: "success", durationMs: Date.now() - start };
|
|
2745
|
+
} catch (err) {
|
|
2746
|
+
return {
|
|
2747
|
+
name,
|
|
2748
|
+
action,
|
|
2749
|
+
status: "failed",
|
|
2750
|
+
durationMs: Date.now() - start,
|
|
2751
|
+
error: err.message
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
async function fixConfig(projectRoot) {
|
|
2756
|
+
const result = ensureConfig(projectRoot);
|
|
2757
|
+
if (result.action === "skipped") {
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
async function fixHooks(projectRoot) {
|
|
2762
|
+
new InitCommand(projectRoot).installHooks();
|
|
2763
|
+
}
|
|
2764
|
+
function hasMissingPlugins(projectRoot) {
|
|
2765
|
+
const detection = detectLanguages(projectRoot);
|
|
2766
|
+
const needed = decideNeededLanguages(detection);
|
|
2767
|
+
for (const lang of needed) {
|
|
2768
|
+
if (!canResolvePlugin(lang)) return true;
|
|
2769
|
+
}
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
async function fixLanguageCoverage(projectRoot, dryRun) {
|
|
2773
|
+
const detection = detectLanguages(projectRoot);
|
|
2774
|
+
const needed = decideNeededLanguages(detection);
|
|
2775
|
+
const missing = needed.filter((lang) => !canResolvePlugin(lang));
|
|
2776
|
+
if (missing.length === 0) return;
|
|
2777
|
+
if (dryRun) return;
|
|
2778
|
+
await new InstallCommand(projectRoot).run({ languages: missing, yes: true });
|
|
2779
|
+
}
|
|
2780
|
+
async function fixIndex(projectRoot, dryRun) {
|
|
2781
|
+
if (dryRun) return;
|
|
2782
|
+
await new IndexCommand(projectRoot).run({});
|
|
2783
|
+
}
|
|
2784
|
+
async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
2785
|
+
if (dryRun) return;
|
|
2786
|
+
const indexDir = join20(ctxoRoot, "index");
|
|
2787
|
+
if (!existsSync18(indexDir)) return;
|
|
2788
|
+
const sourceFiles = listGitFiles2(projectRoot);
|
|
2789
|
+
if (sourceFiles.size === 0) return;
|
|
2790
|
+
const reader = new JsonIndexReader(ctxoRoot);
|
|
2791
|
+
const indices = reader.readAll();
|
|
2792
|
+
let removed = 0;
|
|
2793
|
+
for (const idx of indices) {
|
|
2794
|
+
if (sourceFiles.has(idx.file)) continue;
|
|
2795
|
+
const jsonPath = join20(indexDir, `${idx.file}.json`);
|
|
2796
|
+
try {
|
|
2797
|
+
if (existsSync18(jsonPath)) {
|
|
2798
|
+
unlinkSync2(jsonPath);
|
|
2799
|
+
removed += 1;
|
|
2800
|
+
pruneEmptyParents(jsonPath, indexDir);
|
|
2801
|
+
}
|
|
2802
|
+
} catch {
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
if (removed === 0) return;
|
|
2806
|
+
}
|
|
2807
|
+
function pruneEmptyParents(filePath, stopAt) {
|
|
2808
|
+
let dir = dirname7(filePath);
|
|
2809
|
+
while (dir !== stopAt && dir.startsWith(stopAt)) {
|
|
2810
|
+
try {
|
|
2811
|
+
rmSync2(dir, { recursive: false });
|
|
2812
|
+
} catch {
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
dir = dirname7(dir);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
function listGitFiles2(projectRoot) {
|
|
2819
|
+
try {
|
|
2820
|
+
const out = execFileSync6(
|
|
2821
|
+
"git",
|
|
2822
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
2823
|
+
{ cwd: projectRoot, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
2824
|
+
);
|
|
2825
|
+
return new Set(out.split("\n").filter(Boolean));
|
|
2826
|
+
} catch {
|
|
2827
|
+
return /* @__PURE__ */ new Set();
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
async function fixSqliteCache(projectRoot, ctxoRoot, dryRun) {
|
|
2831
|
+
const cacheDir = join20(ctxoRoot, ".cache");
|
|
2832
|
+
if (dryRun) return;
|
|
2833
|
+
if (existsSync18(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
|
|
2834
|
+
await new SyncCommand(projectRoot).run();
|
|
2835
|
+
}
|
|
2836
|
+
function canResolvePlugin(lang) {
|
|
2837
|
+
try {
|
|
2838
|
+
const { createRequire: createRequire5 } = __require("module");
|
|
2839
|
+
const r = createRequire5(import.meta.url);
|
|
2840
|
+
r.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
2841
|
+
return true;
|
|
2842
|
+
} catch {
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
function isCI(env) {
|
|
2847
|
+
return env["CI"] === "true" || env["CI"] === "1";
|
|
2848
|
+
}
|
|
2849
|
+
function writeLog(ctxoRoot, attempts, halted, reason) {
|
|
2850
|
+
try {
|
|
2851
|
+
if (!existsSync18(ctxoRoot)) mkdirSync5(ctxoRoot, { recursive: true });
|
|
2852
|
+
const path = join20(ctxoRoot, "doctor-fix.log");
|
|
2853
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2854
|
+
const lines = [`# ${ts} doctor --fix${halted ? " (halted: " + reason + ")" : ""}`];
|
|
2855
|
+
for (const a of attempts) {
|
|
2856
|
+
const dur = a.durationMs != null ? ` (${a.durationMs}ms)` : "";
|
|
2857
|
+
const err = a.error ? ` \u2014 ${a.error}` : "";
|
|
2858
|
+
lines.push(`- [${a.status}] ${a.name}: ${a.action}${dur}${err}`);
|
|
2859
|
+
}
|
|
2860
|
+
lines.push("");
|
|
2861
|
+
appendFileSync(path, lines.join("\n"));
|
|
2862
|
+
} catch {
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
function formatFixReport(report) {
|
|
2866
|
+
const lines = [];
|
|
2867
|
+
for (const a of report.attempts) {
|
|
2868
|
+
const icon = a.status === "success" ? "\u2713" : a.status === "planned" ? "\xB7" : a.status === "skipped" ? "-" : "\u2717";
|
|
2869
|
+
const dur = a.durationMs != null ? ` (${a.durationMs}ms)` : "";
|
|
2870
|
+
const err = a.error ? ` \u2014 ${a.error}` : "";
|
|
2871
|
+
lines.push(` ${icon} ${a.name}: ${a.action}${dur}${err}`);
|
|
2872
|
+
}
|
|
2873
|
+
if (report.halted && report.reason) {
|
|
2874
|
+
lines.push(` ! halted: ${report.reason}`);
|
|
2875
|
+
}
|
|
2876
|
+
return lines.join("\n");
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// src/cli/doctor-command.ts
|
|
2880
|
+
var DoctorCommand = class {
|
|
2881
|
+
projectRoot;
|
|
2882
|
+
ctxoRoot;
|
|
2883
|
+
constructor(projectRoot) {
|
|
2884
|
+
this.projectRoot = projectRoot;
|
|
2885
|
+
this.ctxoRoot = join21(projectRoot, ".ctxo");
|
|
2886
|
+
}
|
|
2887
|
+
async run(options = {}) {
|
|
2888
|
+
const { getVersion: getVersion2 } = await import("./cli-router-RA4EBMVV.js");
|
|
2889
|
+
console.error(`ctxo v${getVersion2()} \u2014 health check
|
|
2890
|
+
`);
|
|
2891
|
+
const checks = [
|
|
2892
|
+
new VersionsCheck(),
|
|
2893
|
+
new NodeVersionCheck(),
|
|
2894
|
+
new GitBinaryCheck(),
|
|
2895
|
+
new GitRepoCheck(),
|
|
2896
|
+
new IndexDirectoryCheck(),
|
|
2897
|
+
new IndexFreshnessCheck(),
|
|
2898
|
+
new SqliteCacheCheck(),
|
|
2899
|
+
new ConfigFileCheck(),
|
|
2900
|
+
new TsMorphCheck(),
|
|
2901
|
+
new TreeSitterCheck(),
|
|
2902
|
+
new LanguageCoverageCheck(),
|
|
2903
|
+
new DiskUsageCheck(),
|
|
2904
|
+
new SymbolCountCheck(),
|
|
2905
|
+
new EdgeCountCheck(),
|
|
2906
|
+
new OrphanedFilesCheck(),
|
|
2907
|
+
new CoChangesCacheCheck(),
|
|
2908
|
+
new SchemaVersionCheck()
|
|
2909
|
+
];
|
|
2910
|
+
const indexDir = join21(this.ctxoRoot, "index");
|
|
2911
|
+
const indices = existsSync19(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
|
|
2912
|
+
const checker = new HealthChecker(checks);
|
|
2913
|
+
const report = await checker.runAll({
|
|
2914
|
+
projectRoot: this.projectRoot,
|
|
2915
|
+
ctxoRoot: this.ctxoRoot,
|
|
2916
|
+
indices
|
|
2917
|
+
});
|
|
2918
|
+
const reporter = new DoctorReporter();
|
|
2919
|
+
let output;
|
|
2920
|
+
if (options.json) {
|
|
2921
|
+
output = reporter.formatJson(report);
|
|
2922
|
+
process.stdout.write(output + "\n");
|
|
2923
|
+
} else if (options.quiet) {
|
|
2924
|
+
output = reporter.formatQuiet(report);
|
|
2925
|
+
console.error(output);
|
|
2926
|
+
} else {
|
|
2927
|
+
output = reporter.formatHuman(report);
|
|
2928
|
+
console.error(output);
|
|
2929
|
+
}
|
|
2930
|
+
if (options.fix) {
|
|
2931
|
+
console.error("");
|
|
2932
|
+
console.error(options.dryRun ? "[ctxo] --fix --dry-run: planned actions" : "[ctxo] --fix: applying remediation");
|
|
2933
|
+
const fixReport = await applyFixes(report, {
|
|
2934
|
+
projectRoot: this.projectRoot,
|
|
2935
|
+
ctxoRoot: this.ctxoRoot,
|
|
2936
|
+
yes: options.yes,
|
|
2937
|
+
dryRun: options.dryRun
|
|
2938
|
+
});
|
|
2939
|
+
console.error(formatFixReport(fixReport));
|
|
2940
|
+
if (fixReport.halted) {
|
|
2941
|
+
process.exitCode = 2;
|
|
2942
|
+
}
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
if (report.exitCode !== 0) {
|
|
2946
|
+
process.exitCode = 1;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
};
|
|
2950
|
+
|
|
2951
|
+
// src/cli/visualize-command.ts
|
|
2952
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync20 } from "fs";
|
|
2953
|
+
import { join as join22, basename } from "path";
|
|
2954
|
+
import { execFile } from "child_process";
|
|
2955
|
+
var VisualizeCommand = class {
|
|
2956
|
+
constructor(projectRoot) {
|
|
2957
|
+
this.projectRoot = projectRoot;
|
|
2958
|
+
this.ctxoRoot = join22(projectRoot, ".ctxo");
|
|
2959
|
+
}
|
|
2960
|
+
projectRoot;
|
|
2961
|
+
ctxoRoot;
|
|
2962
|
+
async run(options = {}) {
|
|
2963
|
+
const reader = new JsonIndexReader(this.ctxoRoot);
|
|
2964
|
+
const indices = reader.readAll();
|
|
2965
|
+
if (indices.length === 0) {
|
|
2966
|
+
console.error('[ctxo] No index found. Run "ctxo index" first.');
|
|
2967
|
+
process.exit(1);
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
console.error(`[ctxo] Building graph from ${indices.length} files...`);
|
|
2971
|
+
const graph = new SymbolGraph();
|
|
2972
|
+
for (const fileIndex of indices) {
|
|
2973
|
+
for (const sym of fileIndex.symbols) {
|
|
2974
|
+
graph.addNode(sym);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
for (const fileIndex of indices) {
|
|
2978
|
+
for (const edge of fileIndex.edges) {
|
|
2979
|
+
graph.addEdge(edge);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
console.error(`[ctxo] Graph: ${graph.nodeCount} symbols, ${graph.edgeCount} edges`);
|
|
2983
|
+
const pageRank = new PageRankCalculator();
|
|
2984
|
+
const prResult = pageRank.calculate(graph, { limit: 1e5 });
|
|
2985
|
+
const prMap = /* @__PURE__ */ new Map();
|
|
2986
|
+
for (const entry of prResult.rankings) {
|
|
2987
|
+
prMap.set(entry.symbolId, entry.score);
|
|
2988
|
+
}
|
|
2989
|
+
const deadDetector = new DeadCodeDetector();
|
|
2990
|
+
const deadResult = deadDetector.detect(graph);
|
|
2991
|
+
const deadMap = /* @__PURE__ */ new Map();
|
|
2992
|
+
for (const d of deadResult.deadSymbols) {
|
|
2993
|
+
deadMap.set(d.symbolId, { confidence: d.confidence, reason: d.reason });
|
|
2994
|
+
}
|
|
2995
|
+
const complexityMap = /* @__PURE__ */ new Map();
|
|
2996
|
+
for (const fileIndex of indices) {
|
|
2997
|
+
if (fileIndex.complexity) {
|
|
2998
|
+
for (const c of fileIndex.complexity) {
|
|
2999
|
+
complexityMap.set(c.symbolId, c.cyclomatic);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
const antiPatternFiles = /* @__PURE__ */ new Set();
|
|
3004
|
+
for (const fileIndex of indices) {
|
|
3005
|
+
if (fileIndex.antiPatterns.length > 0) {
|
|
3006
|
+
antiPatternFiles.add(fileIndex.file);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
const filePaths = [...new Set(indices.map((i) => i.file))];
|
|
3010
|
+
const overlay = new ArchitecturalOverlay();
|
|
3011
|
+
const overlayResult = overlay.classify(filePaths);
|
|
3012
|
+
const fileLayerMap = /* @__PURE__ */ new Map();
|
|
3013
|
+
for (const [layer, files] of Object.entries(overlayResult.layers)) {
|
|
3014
|
+
for (const file of files) {
|
|
3015
|
+
fileLayerMap.set(file, layer);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
const totalSymbols = graph.nodeCount;
|
|
3019
|
+
let nodes = graph.allNodes().map((sym) => {
|
|
3020
|
+
const file = sym.symbolId.split("::")[0] ?? "";
|
|
3021
|
+
const dead = deadMap.get(sym.symbolId);
|
|
3022
|
+
return {
|
|
3023
|
+
id: sym.symbolId,
|
|
3024
|
+
name: sym.name,
|
|
3025
|
+
kind: sym.kind,
|
|
3026
|
+
file,
|
|
3027
|
+
startLine: sym.startLine,
|
|
3028
|
+
layer: fileLayerMap.get(file) ?? "Unknown",
|
|
3029
|
+
pageRank: prMap.get(sym.symbolId) ?? 0,
|
|
3030
|
+
isDead: dead !== void 0,
|
|
3031
|
+
...dead ? { deadConfidence: dead.confidence, deadReason: dead.reason } : {},
|
|
3032
|
+
cyclomatic: complexityMap.get(sym.symbolId),
|
|
3033
|
+
hasAntiPattern: antiPatternFiles.has(file),
|
|
3034
|
+
inDegree: graph.getReverseEdges(sym.symbolId).length,
|
|
3035
|
+
outDegree: graph.getForwardEdges(sym.symbolId).length
|
|
3036
|
+
};
|
|
3037
|
+
});
|
|
3038
|
+
if (options.maxNodes && nodes.length > options.maxNodes) {
|
|
3039
|
+
nodes.sort((a, b) => b.pageRank - a.pageRank);
|
|
3040
|
+
nodes = nodes.slice(0, options.maxNodes);
|
|
3041
|
+
}
|
|
3042
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
3043
|
+
const edges = graph.allEdges().filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to)).map((e) => ({ source: e.from, target: e.to, kind: e.kind }));
|
|
3044
|
+
const filesInfo = indices.filter((fi) => nodeIds.has(fi.symbols[0]?.symbolId ?? "___") || fi.symbols.some((s) => nodeIds.has(s.symbolId))).map((fi) => ({
|
|
3045
|
+
file: fi.file,
|
|
3046
|
+
intent: fi.intent.map((i) => ({ hash: i.hash, message: i.message, date: i.date })),
|
|
3047
|
+
antiPatterns: fi.antiPatterns.map((a) => ({ hash: a.hash, message: a.message, date: a.date }))
|
|
3048
|
+
}));
|
|
3049
|
+
const payload = {
|
|
3050
|
+
projectName: basename(this.projectRoot),
|
|
3051
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
3052
|
+
totalSymbols,
|
|
3053
|
+
shownSymbols: nodes.length,
|
|
3054
|
+
nodes,
|
|
3055
|
+
edges,
|
|
3056
|
+
layers: overlayResult.layers,
|
|
3057
|
+
files: filesInfo
|
|
3058
|
+
};
|
|
3059
|
+
const templatePath = this.getTemplatePath();
|
|
3060
|
+
const template = readFileSync12(templatePath, "utf-8");
|
|
3061
|
+
const html = template.replace("/*__CTXO_DATA__*/null", JSON.stringify(payload));
|
|
3062
|
+
const outputPath = options.output ?? join22(this.ctxoRoot, "visualize.html");
|
|
3063
|
+
writeFileSync6(outputPath, html, "utf-8");
|
|
3064
|
+
console.error(`[ctxo] Wrote visualization to ${outputPath}`);
|
|
3065
|
+
console.error(`[ctxo] ${nodes.length} symbols, ${edges.length} edges, ${deadResult.deadSymbols.length} dead`);
|
|
3066
|
+
if (!options.noBrowser) {
|
|
3067
|
+
this.openInBrowser(outputPath);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
getTemplatePath() {
|
|
3071
|
+
let dir = import.meta.dirname;
|
|
3072
|
+
for (let i = 0; i < 10; i++) {
|
|
3073
|
+
const candidate = join22(dir, "pages", "visualize-template.html");
|
|
3074
|
+
if (existsSync20(candidate)) return candidate;
|
|
3075
|
+
const parent = join22(dir, "..");
|
|
3076
|
+
if (parent === dir) break;
|
|
3077
|
+
dir = parent;
|
|
3078
|
+
}
|
|
3079
|
+
throw new Error("Could not find visualize-template.html. Ensure the pages/ directory exists.");
|
|
3080
|
+
}
|
|
3081
|
+
openInBrowser(filePath) {
|
|
3082
|
+
const onError = (err) => {
|
|
3083
|
+
if (err) {
|
|
3084
|
+
console.error(`[ctxo] Could not open browser: ${err.message}`);
|
|
3085
|
+
console.error(`[ctxo] Open manually: ${filePath}`);
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
if (process.platform === "win32") {
|
|
3089
|
+
execFile("cmd.exe", ["/c", "start", "", filePath], onError);
|
|
3090
|
+
} else if (process.platform === "darwin") {
|
|
3091
|
+
execFile("open", [filePath], onError);
|
|
3092
|
+
} else {
|
|
3093
|
+
execFile("xdg-open", [filePath], onError);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
|
|
3098
|
+
// src/cli/cli-router.ts
|
|
3099
|
+
function getVersion() {
|
|
3100
|
+
let dir = import.meta.dirname;
|
|
3101
|
+
for (let i = 0; i < 10; i++) {
|
|
3102
|
+
const pkg = join23(dir, "package.json");
|
|
3103
|
+
if (existsSync21(pkg)) {
|
|
3104
|
+
try {
|
|
3105
|
+
const json = JSON.parse(readFileSync13(pkg, "utf-8"));
|
|
3106
|
+
return json.version ?? "unknown";
|
|
3107
|
+
} catch {
|
|
3108
|
+
break;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
const parent = join23(dir, "..");
|
|
3112
|
+
if (parent === dir) break;
|
|
3113
|
+
dir = parent;
|
|
3114
|
+
}
|
|
3115
|
+
return "unknown";
|
|
3116
|
+
}
|
|
3117
|
+
function locateGitRoot(startDir) {
|
|
3118
|
+
let dir = startDir;
|
|
3119
|
+
for (let i = 0; i < 12; i++) {
|
|
3120
|
+
if (existsSync21(join23(dir, ".git"))) return dir;
|
|
3121
|
+
const parent = dirname8(dir);
|
|
3122
|
+
if (parent === dir) break;
|
|
3123
|
+
dir = parent;
|
|
3124
|
+
}
|
|
3125
|
+
return null;
|
|
3126
|
+
}
|
|
3127
|
+
var CliRouter = class {
|
|
3128
|
+
projectRoot;
|
|
3129
|
+
constructor(projectRoot) {
|
|
3130
|
+
this.projectRoot = locateGitRoot(projectRoot) ?? projectRoot;
|
|
3131
|
+
}
|
|
3132
|
+
async route(args) {
|
|
3133
|
+
const command = args[0];
|
|
3134
|
+
if (command === "--version" || command === "-v" || command === "-V") {
|
|
3135
|
+
const verbose = args.includes("--verbose");
|
|
3136
|
+
const json = args.includes("--json");
|
|
3137
|
+
await new VersionCommand(this.projectRoot).run({ verbose, json });
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
if (command === "version") {
|
|
3141
|
+
const json = args.includes("--json");
|
|
3142
|
+
const short = args.includes("--short");
|
|
3143
|
+
await new VersionCommand(this.projectRoot).run({
|
|
3144
|
+
verbose: !json && !short,
|
|
3145
|
+
json
|
|
3146
|
+
});
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
if (!command || command === "--help" || command === "-h") {
|
|
3150
|
+
this.printHelp();
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
switch (command) {
|
|
3154
|
+
case "index": {
|
|
3155
|
+
const fileIdx = args.indexOf("--file");
|
|
3156
|
+
const fileArg = fileIdx !== -1 ? args[fileIdx + 1] : void 0;
|
|
3157
|
+
if (fileIdx !== -1 && (!fileArg || fileArg.startsWith("--"))) {
|
|
3158
|
+
console.error("[ctxo] --file requires a path argument");
|
|
3159
|
+
process.exit(1);
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const checkArg = args.includes("--check");
|
|
3163
|
+
const skipHistory = args.includes("--skip-history");
|
|
3164
|
+
const installMissing = args.includes("--install-missing");
|
|
3165
|
+
const maxHistoryIdx = args.indexOf("--max-history");
|
|
3166
|
+
const maxHistoryArg = maxHistoryIdx !== -1 ? Number(args[maxHistoryIdx + 1]) : void 0;
|
|
3167
|
+
if (maxHistoryIdx !== -1 && (!maxHistoryArg || isNaN(maxHistoryArg) || maxHistoryArg < 1)) {
|
|
3168
|
+
console.error("[ctxo] --max-history requires a positive integer");
|
|
3169
|
+
process.exit(1);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
await new IndexCommand(this.projectRoot).run({
|
|
3173
|
+
file: fileArg,
|
|
3174
|
+
check: checkArg,
|
|
3175
|
+
skipHistory,
|
|
3176
|
+
maxHistory: maxHistoryArg,
|
|
3177
|
+
installMissing
|
|
3178
|
+
});
|
|
3179
|
+
break;
|
|
3180
|
+
}
|
|
3181
|
+
case "sync":
|
|
3182
|
+
await new SyncCommand(this.projectRoot).run();
|
|
3183
|
+
break;
|
|
3184
|
+
case "watch":
|
|
3185
|
+
await new WatchCommand(this.projectRoot).run();
|
|
3186
|
+
break;
|
|
3187
|
+
case "verify-index":
|
|
3188
|
+
await new VerifyCommand(this.projectRoot).run();
|
|
3189
|
+
break;
|
|
3190
|
+
case "status":
|
|
3191
|
+
new StatusCommand(this.projectRoot).run();
|
|
3192
|
+
break;
|
|
3193
|
+
case "init": {
|
|
3194
|
+
const toolsIdx = args.indexOf("--tools");
|
|
3195
|
+
const toolsArg = toolsIdx !== -1 ? args[toolsIdx + 1] : void 0;
|
|
3196
|
+
const yesArg = args.includes("--yes") || args.includes("-y");
|
|
3197
|
+
const rulesOnly = args.includes("--rules");
|
|
3198
|
+
const dryRun = args.includes("--dry-run");
|
|
3199
|
+
const noInstall = args.includes("--no-install");
|
|
3200
|
+
const tools = toolsArg ? toolsArg.split(",").map((t) => t.trim()) : void 0;
|
|
3201
|
+
await new InitCommand(this.projectRoot).run({ tools, yes: yesArg, rulesOnly, dryRun, noInstall });
|
|
3202
|
+
break;
|
|
3203
|
+
}
|
|
3204
|
+
case "stats": {
|
|
3205
|
+
const daysIdx = args.indexOf("--days");
|
|
3206
|
+
const daysArg = daysIdx !== -1 ? Number(args[daysIdx + 1]) : void 0;
|
|
3207
|
+
const jsonArg = args.includes("--json");
|
|
3208
|
+
const clearArg = args.includes("--clear");
|
|
3209
|
+
await new StatsCommand(this.projectRoot).run({ days: daysArg, json: jsonArg, clear: clearArg });
|
|
3210
|
+
break;
|
|
3211
|
+
}
|
|
3212
|
+
case "doctor": {
|
|
3213
|
+
const jsonArg = args.includes("--json");
|
|
3214
|
+
const quietArg = args.includes("--quiet");
|
|
3215
|
+
const fixArg = args.includes("--fix");
|
|
3216
|
+
const dryRunArg = args.includes("--dry-run");
|
|
3217
|
+
const yesArg = args.includes("--yes") || args.includes("-y");
|
|
3218
|
+
await new DoctorCommand(this.projectRoot).run({
|
|
3219
|
+
json: jsonArg,
|
|
3220
|
+
quiet: quietArg,
|
|
3221
|
+
fix: fixArg,
|
|
3222
|
+
dryRun: dryRunArg,
|
|
3223
|
+
yes: yesArg
|
|
3224
|
+
});
|
|
3225
|
+
break;
|
|
3226
|
+
}
|
|
3227
|
+
case "install": {
|
|
3228
|
+
const languages = [];
|
|
3229
|
+
const flagValues = {};
|
|
3230
|
+
for (let i = 1; i < args.length; i++) {
|
|
3231
|
+
const a = args[i];
|
|
3232
|
+
if (a === "--yes" || a === "-y") flagValues["yes"] = true;
|
|
3233
|
+
else if (a === "--global" || a === "-g") flagValues["global"] = true;
|
|
3234
|
+
else if (a === "--dry-run") flagValues["dryRun"] = true;
|
|
3235
|
+
else if (a === "--force") flagValues["force"] = true;
|
|
3236
|
+
else if (a === "--pm") {
|
|
3237
|
+
const next = args[++i];
|
|
3238
|
+
if (!next) {
|
|
3239
|
+
console.error("[ctxo] --pm requires a value");
|
|
3240
|
+
process.exit(1);
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
flagValues["pm"] = next;
|
|
3244
|
+
} else if (a === "--version") {
|
|
3245
|
+
const next = args[++i];
|
|
3246
|
+
if (!next) {
|
|
3247
|
+
console.error("[ctxo] --version requires a value");
|
|
3248
|
+
process.exit(1);
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
flagValues["version"] = next;
|
|
3252
|
+
} else if (a.startsWith("--")) {
|
|
3253
|
+
console.error(`[ctxo] Unknown install flag: ${a}`);
|
|
3254
|
+
process.exit(1);
|
|
3255
|
+
return;
|
|
3256
|
+
} else {
|
|
3257
|
+
languages.push(a);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
await new InstallCommand(this.projectRoot).run({
|
|
3261
|
+
languages,
|
|
3262
|
+
yes: flagValues["yes"] === true,
|
|
3263
|
+
global: flagValues["global"] === true,
|
|
3264
|
+
dryRun: flagValues["dryRun"] === true,
|
|
3265
|
+
force: flagValues["force"] === true,
|
|
3266
|
+
pm: typeof flagValues["pm"] === "string" ? flagValues["pm"] : void 0,
|
|
3267
|
+
version: typeof flagValues["version"] === "string" ? flagValues["version"] : void 0
|
|
3268
|
+
});
|
|
3269
|
+
break;
|
|
3270
|
+
}
|
|
3271
|
+
case "visualize": {
|
|
3272
|
+
const outputIdx = args.indexOf("--output");
|
|
3273
|
+
const outputArg = outputIdx !== -1 ? args[outputIdx + 1] : void 0;
|
|
3274
|
+
const maxNodesIdx = args.indexOf("--max-nodes");
|
|
3275
|
+
const maxNodesArg = maxNodesIdx !== -1 ? Number(args[maxNodesIdx + 1]) : void 0;
|
|
3276
|
+
const noBrowser = args.includes("--no-browser");
|
|
3277
|
+
if (maxNodesIdx !== -1 && (!maxNodesArg || isNaN(maxNodesArg) || maxNodesArg < 1)) {
|
|
3278
|
+
console.error("[ctxo] --max-nodes requires a positive integer");
|
|
3279
|
+
process.exit(1);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
await new VisualizeCommand(this.projectRoot).run({ output: outputArg, maxNodes: maxNodesArg, noBrowser });
|
|
3283
|
+
break;
|
|
3284
|
+
}
|
|
3285
|
+
default:
|
|
3286
|
+
console.error(`[ctxo] Unknown command: "${command}". Run "ctxo --help" for usage.`);
|
|
3287
|
+
process.exit(1);
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
printHelp() {
|
|
3292
|
+
const v = getVersion();
|
|
3293
|
+
console.error(`
|
|
3294
|
+
ctxo v${v} \u2014 MCP server for dependency-aware codebase context
|
|
3295
|
+
|
|
3296
|
+
Usage:
|
|
3297
|
+
ctxo Start MCP server (stdio transport)
|
|
3298
|
+
ctxo index Build full codebase index (--max-history N, default 20)
|
|
3299
|
+
ctxo sync Rebuild SQLite cache from committed JSON index
|
|
3300
|
+
ctxo watch Start file watcher for incremental re-indexing
|
|
3301
|
+
ctxo verify-index CI gate: fail if index is stale
|
|
3302
|
+
ctxo status Show index manifest
|
|
3303
|
+
ctxo init Interactive setup (tools, rules, hooks)
|
|
3304
|
+
ctxo init --rules Regenerate AI tool rules only
|
|
3305
|
+
ctxo init --tools a,b -y Non-interactive (claude-code,cursor,...)
|
|
3306
|
+
ctxo init --dry-run Preview what would be created
|
|
3307
|
+
ctxo stats Show usage statistics (--json, --days N, --clear)
|
|
3308
|
+
ctxo doctor Health check all subsystems (--json, --quiet)
|
|
3309
|
+
ctxo visualize Generate interactive dependency graph HTML
|
|
3310
|
+
ctxo visualize --max-nodes N Limit to top N symbols by PageRank
|
|
3311
|
+
ctxo visualize --output PATH Write HTML to custom path
|
|
3312
|
+
ctxo visualize --no-browser Skip auto-opening browser
|
|
3313
|
+
ctxo install [<lang>...] Install language plugins (detects if omitted)
|
|
3314
|
+
ctxo install python -y Batch install, no prompt
|
|
3315
|
+
ctxo install ts --global Install globally
|
|
3316
|
+
ctxo install py --dry-run Show command without running
|
|
3317
|
+
ctxo --version Print core version
|
|
3318
|
+
ctxo --version --verbose Print core + plugins + runtime
|
|
3319
|
+
ctxo --version --json Machine-readable version payload
|
|
3320
|
+
ctxo version Subcommand \u2014 verbose by default
|
|
3321
|
+
ctxo --help Show this help message
|
|
3322
|
+
`.trim());
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
export {
|
|
3326
|
+
CliRouter,
|
|
3327
|
+
getVersion,
|
|
3328
|
+
locateGitRoot
|
|
3329
|
+
};
|
|
3330
|
+
//# sourceMappingURL=cli-router-RA4EBMVV.js.map
|