@ctxo/cli 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/chunk-2Q76LH2H.js +186 -0
- package/dist/chunk-72CEQEQD.js +53 -0
- package/dist/{chunk-WB7WZKX4.js → chunk-A4URKKPP.js} +916 -1938
- package/dist/chunk-BBSJB4CG.js +128 -0
- package/dist/{chunk-DT3ZH4AU.js → chunk-EC6INGPV.js} +6 -1
- package/dist/chunk-I7RPXT6L.js +1241 -0
- package/dist/chunk-IT426P3F.js +1036 -0
- package/dist/chunk-JDL6GGTO.js +87 -0
- package/dist/chunk-LAX6VVBU.js +98 -0
- package/dist/{cli-router-DDHH4WQH.js → cli-router-W56INOQ3.js} +980 -504
- package/dist/index.js +13 -2874
- package/dist/internal.d.ts +834 -0
- package/dist/internal.js +13 -0
- package/dist/{json-index-reader-OXTRTGE6.js → json-index-reader-DKEIP2QR.js} +6 -4
- package/dist/mcp-server-IYCCE74E.js +2336 -0
- package/dist/module-host-KTGDODES.js +13 -0
- package/dist/module-loader-B3LYF3W7.js +14 -0
- package/dist/plugin-discovery-XRXI3IJF.js +13 -0
- package/llms-full.txt +121 -2
- package/llms.txt +24 -1
- package/package.json +15 -3
- package/dist/atomic-write-BAFH464Q.js.map +0 -1
- package/dist/chunk-5DSKZJNN.js.map +0 -1
- package/dist/chunk-DT3ZH4AU.js.map +0 -1
- package/dist/chunk-PSKDDLSF.js.map +0 -1
- package/dist/chunk-RFEXMBTG.js.map +0 -1
- package/dist/chunk-WB7WZKX4.js.map +0 -1
- package/dist/cli-router-DDHH4WQH.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/json-index-reader-OXTRTGE6.js.map +0 -1
- package/dist/staleness-detector-4HJYBQ7Z.js.map +0 -1
|
@@ -6,24 +6,43 @@ import {
|
|
|
6
6
|
CoChangeReader,
|
|
7
7
|
CommunitySnapshotWriter,
|
|
8
8
|
ContextsViewReader,
|
|
9
|
-
DATA_EDGE_KINDS,
|
|
10
9
|
DeadCodeDetector,
|
|
11
10
|
DriftDetector,
|
|
12
11
|
PageRankCalculator,
|
|
13
12
|
RevertDetector,
|
|
14
13
|
SessionRecorderAdapter,
|
|
15
|
-
SimpleGitAdapter,
|
|
16
14
|
SqliteStorageAdapter,
|
|
15
|
+
buildCoChangeMatrix,
|
|
16
|
+
detectWorkspace,
|
|
17
|
+
findCoChangedFilesForBlastRadius
|
|
18
|
+
} from "./chunk-A4URKKPP.js";
|
|
19
|
+
import {
|
|
20
|
+
FAST_ROUTE_COMMANDS,
|
|
21
|
+
isKnownModuleCommand,
|
|
22
|
+
loadModulesFor,
|
|
23
|
+
moduleHelpLines,
|
|
24
|
+
runFastRoute,
|
|
25
|
+
runModuleCommand
|
|
26
|
+
} from "./chunk-BBSJB4CG.js";
|
|
27
|
+
import {
|
|
28
|
+
DOMAIN_MODULE_PACKAGE,
|
|
29
|
+
loadCtxoModules
|
|
30
|
+
} from "./chunk-LAX6VVBU.js";
|
|
31
|
+
import {
|
|
32
|
+
discoverPlugins,
|
|
33
|
+
resolvePackageCandidates,
|
|
34
|
+
resolvePluginPackageJson
|
|
35
|
+
} from "./chunk-2Q76LH2H.js";
|
|
36
|
+
import {
|
|
37
|
+
DATA_EDGE_KINDS,
|
|
38
|
+
SimpleGitAdapter,
|
|
17
39
|
SymbolGraph,
|
|
18
40
|
SymbolTokenizer,
|
|
19
|
-
buildCoChangeMatrix,
|
|
20
41
|
communityDetectorOptions,
|
|
21
|
-
compareCodepoint,
|
|
22
42
|
createTestFileClassifier,
|
|
23
|
-
detectWorkspace,
|
|
24
43
|
factsRequested,
|
|
25
44
|
filesWithTestAnnotations,
|
|
26
|
-
|
|
45
|
+
hasCSharpAnnotations,
|
|
27
46
|
indexIgnorePatterns,
|
|
28
47
|
indexIgnoreProjectPatterns,
|
|
29
48
|
indexTestPatterns,
|
|
@@ -34,17 +53,24 @@ import {
|
|
|
34
53
|
resolveCoChangeConfig,
|
|
35
54
|
resolveContextsViewConfig,
|
|
36
55
|
selectWindow,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} from "./chunk-
|
|
56
|
+
watchSnapshotMinFileChanges,
|
|
57
|
+
windowCutoff
|
|
58
|
+
} from "./chunk-I7RPXT6L.js";
|
|
40
59
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
compareCodepoint,
|
|
61
|
+
getCliPackageRoot,
|
|
62
|
+
getVersion,
|
|
63
|
+
locateGitRoot
|
|
64
|
+
} from "./chunk-72CEQEQD.js";
|
|
43
65
|
import {
|
|
44
66
|
COMMUNITY_HISTORY_DIR,
|
|
45
67
|
INDEX_ARTIFACT_FILES,
|
|
46
|
-
JsonIndexReader
|
|
47
|
-
|
|
68
|
+
JsonIndexReader,
|
|
69
|
+
isMarkedArtifact
|
|
70
|
+
} from "./chunk-EC6INGPV.js";
|
|
71
|
+
import {
|
|
72
|
+
atomicWrite
|
|
73
|
+
} from "./chunk-RFEXMBTG.js";
|
|
48
74
|
import {
|
|
49
75
|
createLogger
|
|
50
76
|
} from "./chunk-5DSKZJNN.js";
|
|
@@ -52,15 +78,11 @@ import {
|
|
|
52
78
|
StalenessDetector
|
|
53
79
|
} from "./chunk-PSKDDLSF.js";
|
|
54
80
|
|
|
55
|
-
// src/cli/cli-router.ts
|
|
56
|
-
import { readFileSync as readFileSync19, existsSync as existsSync30 } from "fs";
|
|
57
|
-
import { dirname as dirname10, join as join36 } from "path";
|
|
58
|
-
|
|
59
81
|
// src/cli/index-command.ts
|
|
60
82
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
61
|
-
import { readFileSync as
|
|
62
|
-
import { join as join11, relative as relative2, extname as extname3, isAbsolute
|
|
63
|
-
import { createRequire as
|
|
83
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, existsSync as existsSync11, statSync, readdirSync as readdirSync2 } from "fs";
|
|
84
|
+
import { join as join11, relative as relative2, extname as extname3, isAbsolute, resolve as resolve3 } from "path";
|
|
85
|
+
import { createRequire as createRequire2 } from "module";
|
|
64
86
|
|
|
65
87
|
// src/core/staleness/content-hasher.ts
|
|
66
88
|
import { createHash } from "crypto";
|
|
@@ -91,140 +113,12 @@ var LanguageAdapterRegistry = class {
|
|
|
91
113
|
};
|
|
92
114
|
|
|
93
115
|
// src/cli/plugin-loader.ts
|
|
94
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
95
|
-
import { dirname as dirname2, join } from "path";
|
|
96
|
-
|
|
97
|
-
// src/adapters/language/plugin-discovery.ts
|
|
98
116
|
import { existsSync, readFileSync } from "fs";
|
|
99
|
-
import {
|
|
100
|
-
|
|
101
|
-
import { pathToFileURL } from "url";
|
|
102
|
-
import { SUPPORTED_API_VERSIONS } from "@ctxo/plugin-api";
|
|
103
|
-
var log = createLogger("ctxo:plugin-discovery");
|
|
104
|
-
var OFFICIAL_PREFIX = "@ctxo/lang-";
|
|
105
|
-
var COMMUNITY_PREFIX = "ctxo-lang-";
|
|
106
|
-
function readManifestDeps(manifestPath) {
|
|
107
|
-
try {
|
|
108
|
-
const raw = readFileSync(manifestPath, "utf-8");
|
|
109
|
-
const manifest = JSON.parse(raw);
|
|
110
|
-
const all = {
|
|
111
|
-
...manifest.dependencies,
|
|
112
|
-
...manifest.devDependencies,
|
|
113
|
-
...manifest.peerDependencies
|
|
114
|
-
};
|
|
115
|
-
return Object.keys(all).filter(
|
|
116
|
-
(name) => name.startsWith(OFFICIAL_PREFIX) || name.startsWith(COMMUNITY_PREFIX)
|
|
117
|
-
);
|
|
118
|
-
} catch (err) {
|
|
119
|
-
log.warn(`Failed to read manifest ${manifestPath}: ${err.message}`);
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function isLocalSpecifier(spec) {
|
|
124
|
-
return spec.startsWith(".") || spec.startsWith("/") || isAbsolute(spec);
|
|
125
|
-
}
|
|
126
|
-
function pickEsmEntry(exportsField) {
|
|
127
|
-
if (!exportsField) return void 0;
|
|
128
|
-
if (typeof exportsField === "string") return exportsField;
|
|
129
|
-
if (typeof exportsField !== "object") return void 0;
|
|
130
|
-
const table = exportsField;
|
|
131
|
-
const dot = "." in table ? table["."] : exportsField;
|
|
132
|
-
if (typeof dot === "string") return dot;
|
|
133
|
-
if (!dot || typeof dot !== "object") return void 0;
|
|
134
|
-
for (const key of ["import", "node", "default"]) {
|
|
135
|
-
const nested = dot[key];
|
|
136
|
-
if (typeof nested === "string") return nested;
|
|
137
|
-
if (nested && typeof nested === "object") {
|
|
138
|
-
for (const inner of ["default", "import", "node"]) {
|
|
139
|
-
const v = nested[inner];
|
|
140
|
-
if (typeof v === "string") return v;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return void 0;
|
|
145
|
-
}
|
|
146
|
-
var cliFallbackRequire = createRequire(import.meta.url);
|
|
147
|
-
function resolvePluginPackageJson(specifier, projectRequire) {
|
|
148
|
-
try {
|
|
149
|
-
return projectRequire.resolve(`${specifier}/package.json`);
|
|
150
|
-
} catch (primary) {
|
|
151
|
-
try {
|
|
152
|
-
return cliFallbackRequire.resolve(`${specifier}/package.json`);
|
|
153
|
-
} catch {
|
|
154
|
-
throw primary;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function resolvePluginEntryUrl(specifier, manifestRequire) {
|
|
159
|
-
const pkgJsonPath = resolvePluginPackageJson(specifier, manifestRequire);
|
|
160
|
-
const pkgDir = dirname(pkgJsonPath);
|
|
161
|
-
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
162
|
-
const entry = pickEsmEntry(pkgJson.exports) ?? pkgJson.module ?? pkgJson.main ?? "index.js";
|
|
163
|
-
return pathToFileURL(resolve(pkgDir, entry)).href;
|
|
164
|
-
}
|
|
165
|
-
async function tryLoad(specifier, baseDir, manifestRequire) {
|
|
166
|
-
try {
|
|
167
|
-
const target = isLocalSpecifier(specifier) ? pathToFileURL(resolve(baseDir, specifier)).href : resolvePluginEntryUrl(specifier, manifestRequire);
|
|
168
|
-
const mod = await import(target);
|
|
169
|
-
const candidate = mod.default ?? mod.plugin ?? mod;
|
|
170
|
-
if (!candidate || typeof candidate !== "object") {
|
|
171
|
-
return { specifier, reason: "module export is not an object" };
|
|
172
|
-
}
|
|
173
|
-
if (!SUPPORTED_API_VERSIONS.includes(candidate.apiVersion)) {
|
|
174
|
-
return {
|
|
175
|
-
specifier,
|
|
176
|
-
reason: `unsupported apiVersion ${String(candidate.apiVersion)} (supported: ${SUPPORTED_API_VERSIONS.join(", ")})`
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (typeof candidate.createAdapter !== "function") {
|
|
180
|
-
return { specifier, reason: "missing createAdapter()" };
|
|
181
|
-
}
|
|
182
|
-
if (typeof candidate.id !== "string" || candidate.id.length === 0) {
|
|
183
|
-
return { specifier, reason: "missing or empty id" };
|
|
184
|
-
}
|
|
185
|
-
if (!Array.isArray(candidate.extensions)) {
|
|
186
|
-
return { specifier, reason: "extensions must be an array" };
|
|
187
|
-
}
|
|
188
|
-
if (candidate.tier !== "syntax" && candidate.tier !== "full") {
|
|
189
|
-
return { specifier, reason: `invalid tier ${String(candidate.tier)}` };
|
|
190
|
-
}
|
|
191
|
-
return candidate;
|
|
192
|
-
} catch (err) {
|
|
193
|
-
return { specifier, reason: err.message };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
async function discoverPlugins(options) {
|
|
197
|
-
const { manifestPath } = options;
|
|
198
|
-
if (!existsSync(manifestPath)) {
|
|
199
|
-
log.info(`Manifest not found at ${manifestPath} \u2014 no plugins discovered`);
|
|
200
|
-
return { plugins: [], failures: [] };
|
|
201
|
-
}
|
|
202
|
-
const baseDir = dirname(manifestPath);
|
|
203
|
-
const specifiers = options.explicit ?? readManifestDeps(manifestPath);
|
|
204
|
-
const manifestRequire = createRequire(pathToFileURL(manifestPath).href);
|
|
205
|
-
const plugins = [];
|
|
206
|
-
const failures = [];
|
|
207
|
-
for (const spec of specifiers) {
|
|
208
|
-
if (options.shouldSkipSpecifier?.(spec)) {
|
|
209
|
-
log.info(`Skipping plugin ${spec} (matched ignoreProjects)`);
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
const result = await tryLoad(spec, baseDir, manifestRequire);
|
|
213
|
-
if ("apiVersion" in result) {
|
|
214
|
-
plugins.push({ plugin: result, specifier: spec });
|
|
215
|
-
} else {
|
|
216
|
-
failures.push(result);
|
|
217
|
-
log.warn(`Skipped plugin ${spec}: ${result.reason}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return { plugins, failures };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// src/cli/plugin-loader.ts
|
|
224
|
-
var log2 = createLogger("ctxo:plugin-loader");
|
|
117
|
+
import { dirname, join } from "path";
|
|
118
|
+
var log = createLogger("ctxo:plugin-loader");
|
|
225
119
|
function manifestHasPlugins(path) {
|
|
226
120
|
try {
|
|
227
|
-
const raw = JSON.parse(
|
|
121
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
228
122
|
const all = { ...raw.dependencies, ...raw.devDependencies, ...raw.peerDependencies };
|
|
229
123
|
return Object.keys(all).some((n) => n.startsWith("@ctxo/lang-") || n.startsWith("ctxo-lang-"));
|
|
230
124
|
} catch {
|
|
@@ -236,18 +130,18 @@ function loadManifestPath(projectRoot) {
|
|
|
236
130
|
}
|
|
237
131
|
function resolveManifestPath(projectRoot) {
|
|
238
132
|
const projectManifest = join(projectRoot, "package.json");
|
|
239
|
-
if (
|
|
133
|
+
if (existsSync(projectManifest) && manifestHasPlugins(projectManifest)) {
|
|
240
134
|
return projectManifest;
|
|
241
135
|
}
|
|
242
136
|
let dir = import.meta.dirname;
|
|
243
137
|
for (let i = 0; i < 12; i++) {
|
|
244
138
|
const candidate = join(dir, "package.json");
|
|
245
|
-
if (
|
|
246
|
-
const parent =
|
|
139
|
+
if (existsSync(candidate) && manifestHasPlugins(candidate)) return candidate;
|
|
140
|
+
const parent = dirname(dir);
|
|
247
141
|
if (parent === dir) break;
|
|
248
142
|
dir = parent;
|
|
249
143
|
}
|
|
250
|
-
return
|
|
144
|
+
return existsSync(projectManifest) ? projectManifest : null;
|
|
251
145
|
}
|
|
252
146
|
function buildContext(projectRoot, workspace, pluginId, capabilities) {
|
|
253
147
|
const pluginLog = createLogger(`ctxo:plugin:${pluginId}`);
|
|
@@ -273,7 +167,7 @@ function hostCapabilities(plugins, options) {
|
|
|
273
167
|
async function loadPlugins(projectRoot, options = {}) {
|
|
274
168
|
const manifestPath = resolveManifestPath(projectRoot);
|
|
275
169
|
if (!manifestPath) {
|
|
276
|
-
|
|
170
|
+
log.info("No package.json found for plugin discovery");
|
|
277
171
|
return [];
|
|
278
172
|
}
|
|
279
173
|
const workspace = detectWorkspace(projectRoot);
|
|
@@ -291,20 +185,20 @@ async function loadPlugins(projectRoot, options = {}) {
|
|
|
291
185
|
const adapter = plugin.createAdapter(ctx);
|
|
292
186
|
loaded.push({ plugin, adapter, specifier });
|
|
293
187
|
} catch (err) {
|
|
294
|
-
|
|
188
|
+
log.warn(`Plugin ${plugin.id} createAdapter failed: ${err.message}`);
|
|
295
189
|
}
|
|
296
190
|
}
|
|
297
191
|
return loaded;
|
|
298
192
|
}
|
|
299
193
|
|
|
300
194
|
// src/cli/install-command.ts
|
|
301
|
-
import { existsSync as
|
|
302
|
-
import { createRequire
|
|
195
|
+
import { existsSync as existsSync5 } from "fs";
|
|
196
|
+
import { createRequire } from "module";
|
|
303
197
|
import { join as join5 } from "path";
|
|
304
198
|
|
|
305
199
|
// src/core/detection/detect-languages.ts
|
|
306
200
|
import { execFileSync } from "child_process";
|
|
307
|
-
import { existsSync as
|
|
201
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
308
202
|
import { extname as extname2, join as join2 } from "path";
|
|
309
203
|
var KNOWN_LANGUAGES = [
|
|
310
204
|
"typescript",
|
|
@@ -337,29 +231,29 @@ var EXTENSION_LANGUAGE = {
|
|
|
337
231
|
var MANIFEST_PROBES = [
|
|
338
232
|
{
|
|
339
233
|
id: "typescript",
|
|
340
|
-
check: (root) =>
|
|
234
|
+
check: (root) => existsSync2(join2(root, "tsconfig.json")) ? "tsconfig.json" : null
|
|
341
235
|
},
|
|
342
236
|
{
|
|
343
237
|
id: "python",
|
|
344
238
|
check: (root) => {
|
|
345
|
-
if (
|
|
346
|
-
if (
|
|
347
|
-
if (
|
|
239
|
+
if (existsSync2(join2(root, "pyproject.toml"))) return "pyproject.toml";
|
|
240
|
+
if (existsSync2(join2(root, "setup.py"))) return "setup.py";
|
|
241
|
+
if (existsSync2(join2(root, "requirements.txt"))) return "requirements.txt";
|
|
348
242
|
return null;
|
|
349
243
|
}
|
|
350
244
|
},
|
|
351
245
|
{
|
|
352
246
|
id: "java",
|
|
353
247
|
check: (root) => {
|
|
354
|
-
if (
|
|
355
|
-
if (
|
|
356
|
-
if (
|
|
248
|
+
if (existsSync2(join2(root, "pom.xml"))) return "pom.xml";
|
|
249
|
+
if (existsSync2(join2(root, "build.gradle"))) return "build.gradle";
|
|
250
|
+
if (existsSync2(join2(root, "build.gradle.kts"))) return "build.gradle.kts";
|
|
357
251
|
return null;
|
|
358
252
|
}
|
|
359
253
|
},
|
|
360
254
|
{
|
|
361
255
|
id: "go",
|
|
362
|
-
check: (root) =>
|
|
256
|
+
check: (root) => existsSync2(join2(root, "go.mod")) ? "go.mod" : null
|
|
363
257
|
},
|
|
364
258
|
{
|
|
365
259
|
id: "csharp",
|
|
@@ -377,11 +271,11 @@ var MANIFEST_PROBES = [
|
|
|
377
271
|
},
|
|
378
272
|
{
|
|
379
273
|
id: "rust",
|
|
380
|
-
check: (root) =>
|
|
274
|
+
check: (root) => existsSync2(join2(root, "Cargo.toml")) ? "Cargo.toml" : null
|
|
381
275
|
},
|
|
382
276
|
{
|
|
383
277
|
id: "ruby",
|
|
384
|
-
check: (root) =>
|
|
278
|
+
check: (root) => existsSync2(join2(root, "Gemfile")) ? "Gemfile" : null
|
|
385
279
|
}
|
|
386
280
|
];
|
|
387
281
|
function listGitFiles(root) {
|
|
@@ -441,8 +335,8 @@ function officialPluginFor(language) {
|
|
|
441
335
|
}
|
|
442
336
|
|
|
443
337
|
// src/core/install/package-manager.ts
|
|
444
|
-
import { existsSync as
|
|
445
|
-
import { dirname as
|
|
338
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
339
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
446
340
|
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
447
341
|
function isPackageManager(value) {
|
|
448
342
|
return PACKAGE_MANAGERS.includes(value);
|
|
@@ -478,9 +372,9 @@ function readPackageManagerField(startDir) {
|
|
|
478
372
|
let dir = startDir;
|
|
479
373
|
for (let i = 0; i < 12; i++) {
|
|
480
374
|
const pkg = join3(dir, "package.json");
|
|
481
|
-
if (
|
|
375
|
+
if (existsSync3(pkg)) {
|
|
482
376
|
try {
|
|
483
|
-
const parsed = JSON.parse(
|
|
377
|
+
const parsed = JSON.parse(readFileSync2(pkg, "utf-8"));
|
|
484
378
|
const value = parsed.packageManager;
|
|
485
379
|
if (typeof value === "string") {
|
|
486
380
|
const [nameRaw] = value.split("@");
|
|
@@ -492,7 +386,7 @@ function readPackageManagerField(startDir) {
|
|
|
492
386
|
} catch {
|
|
493
387
|
}
|
|
494
388
|
}
|
|
495
|
-
const parent =
|
|
389
|
+
const parent = dirname2(dir);
|
|
496
390
|
if (parent === dir) break;
|
|
497
391
|
dir = parent;
|
|
498
392
|
}
|
|
@@ -500,9 +394,9 @@ function readPackageManagerField(startDir) {
|
|
|
500
394
|
}
|
|
501
395
|
function readConfigPackageManager(projectRoot) {
|
|
502
396
|
const path = join3(projectRoot, ".ctxo", "config.yaml");
|
|
503
|
-
if (!
|
|
397
|
+
if (!existsSync3(path)) return null;
|
|
504
398
|
try {
|
|
505
|
-
const raw =
|
|
399
|
+
const raw = readFileSync2(path, "utf-8");
|
|
506
400
|
const match = raw.match(/^packageManager:\s*(\S+)\s*$/m);
|
|
507
401
|
if (!match) return null;
|
|
508
402
|
const value = match[1];
|
|
@@ -523,11 +417,11 @@ function detectFromLockfileWalkUp(startDir) {
|
|
|
523
417
|
for (let i = 0; i < 12; i++) {
|
|
524
418
|
for (const probe of probes) {
|
|
525
419
|
const path = join3(dir, probe.file);
|
|
526
|
-
if (
|
|
420
|
+
if (existsSync3(path)) {
|
|
527
421
|
return { manager: probe.manager, detail: probe.file };
|
|
528
422
|
}
|
|
529
423
|
}
|
|
530
|
-
const parent =
|
|
424
|
+
const parent = dirname2(dir);
|
|
531
425
|
if (parent === dir) break;
|
|
532
426
|
dir = parent;
|
|
533
427
|
}
|
|
@@ -536,9 +430,9 @@ function detectFromLockfileWalkUp(startDir) {
|
|
|
536
430
|
function readWorkspaceLinks(projectRoot) {
|
|
537
431
|
const links = /* @__PURE__ */ new Set();
|
|
538
432
|
const pkgPath = join3(projectRoot, "package.json");
|
|
539
|
-
if (!
|
|
433
|
+
if (!existsSync3(pkgPath)) return links;
|
|
540
434
|
try {
|
|
541
|
-
const pkg = JSON.parse(
|
|
435
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
542
436
|
for (const group of [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies, pkg.optionalDependencies]) {
|
|
543
437
|
if (!group) continue;
|
|
544
438
|
for (const [name, spec] of Object.entries(group)) {
|
|
@@ -553,11 +447,11 @@ function readWorkspaceLinks(projectRoot) {
|
|
|
553
447
|
}
|
|
554
448
|
}
|
|
555
449
|
function isWorkspaceRoot(projectRoot) {
|
|
556
|
-
if (
|
|
450
|
+
if (existsSync3(join3(projectRoot, "pnpm-workspace.yaml"))) return true;
|
|
557
451
|
const pkgPath = join3(projectRoot, "package.json");
|
|
558
|
-
if (!
|
|
452
|
+
if (!existsSync3(pkgPath)) return false;
|
|
559
453
|
try {
|
|
560
|
-
const pkg = JSON.parse(
|
|
454
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
561
455
|
const ws = pkg.workspaces;
|
|
562
456
|
if (Array.isArray(ws) && ws.length > 0) return true;
|
|
563
457
|
if (ws && typeof ws === "object" && Array.isArray(ws.packages)) return true;
|
|
@@ -600,16 +494,16 @@ function buildInstallCommand(manager, packages, options = {}) {
|
|
|
600
494
|
// src/core/install/run-package-manager.ts
|
|
601
495
|
import { spawn } from "child_process";
|
|
602
496
|
function runPackageManager(command, args, cwd) {
|
|
603
|
-
return new Promise((
|
|
497
|
+
return new Promise((resolve5, reject) => {
|
|
604
498
|
const child = process.platform === "win32" ? spawn([command, ...args].join(" "), { cwd, stdio: "inherit", shell: true }) : spawn(command, [...args], { cwd, stdio: "inherit", shell: false });
|
|
605
|
-
child.on("exit", (code) =>
|
|
499
|
+
child.on("exit", (code) => resolve5(code ?? 0));
|
|
606
500
|
child.on("error", (err) => reject(err));
|
|
607
501
|
});
|
|
608
502
|
}
|
|
609
503
|
|
|
610
504
|
// src/core/detection/detect-java-runtime.ts
|
|
611
505
|
import { spawnSync } from "child_process";
|
|
612
|
-
import { existsSync as
|
|
506
|
+
import { existsSync as existsSync4 } from "fs";
|
|
613
507
|
import { join as join4 } from "path";
|
|
614
508
|
function parseJavaMajor(output) {
|
|
615
509
|
const m = output.match(/version "([^"]+)"/);
|
|
@@ -625,7 +519,7 @@ function detectJavaMajor() {
|
|
|
625
519
|
}
|
|
626
520
|
const home = process.env["CTXO_JAVA_HOME"] || process.env["JAVA_HOME"];
|
|
627
521
|
const bin = home ? join4(home, "bin", process.platform === "win32" ? "java.exe" : "java") : "java";
|
|
628
|
-
if (home && !
|
|
522
|
+
if (home && !existsSync4(bin)) {
|
|
629
523
|
_javaMajorCache = null;
|
|
630
524
|
return void 0;
|
|
631
525
|
}
|
|
@@ -647,6 +541,23 @@ function javaRuntimeAvailable() {
|
|
|
647
541
|
}
|
|
648
542
|
|
|
649
543
|
// src/cli/install-command.ts
|
|
544
|
+
var INSTALL_EXTRAS = { domain: DOMAIN_MODULE_PACKAGE };
|
|
545
|
+
function resolveInstallTargets(raw) {
|
|
546
|
+
const languages = [];
|
|
547
|
+
const extras = [];
|
|
548
|
+
const unknown = [];
|
|
549
|
+
for (const name of raw) {
|
|
550
|
+
const extra = Object.hasOwn(INSTALL_EXTRAS, name.toLowerCase()) ? INSTALL_EXTRAS[name.toLowerCase()] : void 0;
|
|
551
|
+
if (extra !== void 0) {
|
|
552
|
+
if (!extras.includes(extra)) extras.push(extra);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const lang = normalizeLanguage(name);
|
|
556
|
+
if (lang === null) unknown.push(name);
|
|
557
|
+
else if (!languages.includes(lang)) languages.push(lang);
|
|
558
|
+
}
|
|
559
|
+
return { languages, extras, unknown };
|
|
560
|
+
}
|
|
650
561
|
function resolveJavaPackages(opts) {
|
|
651
562
|
if (opts.fullTier && opts.syntaxOnly) throw new Error("Cannot combine --full-tier with --syntax-only.");
|
|
652
563
|
const base = ["@ctxo/lang-java"];
|
|
@@ -657,8 +568,8 @@ function resolveJavaPackages(opts) {
|
|
|
657
568
|
function buildPluginSpecifiers(languages, opts) {
|
|
658
569
|
const out = [];
|
|
659
570
|
const seen = /* @__PURE__ */ new Set();
|
|
660
|
-
const add = (pkg) => {
|
|
661
|
-
const spec =
|
|
571
|
+
const add = (pkg, version = opts.version) => {
|
|
572
|
+
const spec = version ? `${pkg}@${version}` : pkg;
|
|
662
573
|
if (seen.has(spec)) return;
|
|
663
574
|
seen.add(spec);
|
|
664
575
|
out.push(spec);
|
|
@@ -670,6 +581,7 @@ function buildPluginSpecifiers(languages, opts) {
|
|
|
670
581
|
add(officialPluginFor(lang));
|
|
671
582
|
}
|
|
672
583
|
}
|
|
584
|
+
for (const pkg of opts.extras ?? []) add(pkg, opts.version ?? opts.cliVersion);
|
|
673
585
|
return out;
|
|
674
586
|
}
|
|
675
587
|
var InstallCommand = class {
|
|
@@ -678,8 +590,9 @@ var InstallCommand = class {
|
|
|
678
590
|
}
|
|
679
591
|
projectRoot;
|
|
680
592
|
async run(options = {}) {
|
|
681
|
-
const resolvedLangs = this.resolveLanguages(options);
|
|
682
|
-
if (resolvedLangs.length === 0) {
|
|
593
|
+
const { languages: resolvedLangs, extras } = this.resolveLanguages(options);
|
|
594
|
+
if (resolvedLangs.length === 0 && extras.length === 0) {
|
|
595
|
+
if (process.exitCode === 1) return;
|
|
683
596
|
console.error('[ctxo] Nothing to install. Specify a language (e.g. "ctxo install python") or add one to your project.');
|
|
684
597
|
process.exitCode = 0;
|
|
685
598
|
return;
|
|
@@ -695,10 +608,10 @@ var InstallCommand = class {
|
|
|
695
608
|
process.exitCode = 1;
|
|
696
609
|
return;
|
|
697
610
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
611
|
+
const noManifest = !options.global && !existsSync5(join5(this.projectRoot, "package.json"));
|
|
612
|
+
const installGlobal = (options.global ?? false) || noManifest;
|
|
613
|
+
if (noManifest) {
|
|
614
|
+
console.error("[ctxo] No package.json here: installing globally, so the MCP server, hooks and CLI share one copy.");
|
|
702
615
|
}
|
|
703
616
|
if (!options.force && !options.global && isLockedCI(process.env)) {
|
|
704
617
|
console.error("[ctxo] CI environment detected with a frozen lockfile. Refusing to mutate dependencies. Use --force or switch to --global.");
|
|
@@ -715,7 +628,9 @@ var InstallCommand = class {
|
|
|
715
628
|
jreAvailable,
|
|
716
629
|
fullTier: options.fullTier,
|
|
717
630
|
syntaxOnly: options.syntaxOnly,
|
|
718
|
-
version: options.version
|
|
631
|
+
version: options.version,
|
|
632
|
+
extras,
|
|
633
|
+
cliVersion: getVersion()
|
|
719
634
|
});
|
|
720
635
|
} catch (err) {
|
|
721
636
|
console.error(`[ctxo] ${err.message}`);
|
|
@@ -723,15 +638,15 @@ var InstallCommand = class {
|
|
|
723
638
|
return;
|
|
724
639
|
}
|
|
725
640
|
const invocation = buildInstallCommand(resolution.manager, specifiers, {
|
|
726
|
-
global:
|
|
727
|
-
workspaceRoot: !
|
|
641
|
+
global: installGlobal,
|
|
642
|
+
workspaceRoot: !installGlobal && isWorkspaceRoot(this.projectRoot)
|
|
728
643
|
});
|
|
729
644
|
const plan = {
|
|
730
645
|
languages: resolvedLangs,
|
|
731
646
|
packages: specifiers,
|
|
732
647
|
manager: resolution.manager,
|
|
733
648
|
managerSource: resolution.source,
|
|
734
|
-
global:
|
|
649
|
+
global: installGlobal,
|
|
735
650
|
command: invocation.command,
|
|
736
651
|
args: [...invocation.args]
|
|
737
652
|
};
|
|
@@ -753,30 +668,26 @@ var InstallCommand = class {
|
|
|
753
668
|
resolveLanguages(options) {
|
|
754
669
|
const requested = options.languages ?? [];
|
|
755
670
|
if (requested.length > 0) {
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
process.exitCode = 1;
|
|
764
|
-
return [];
|
|
765
|
-
}
|
|
766
|
-
if (!out.includes(lang)) out.push(lang);
|
|
671
|
+
const { languages, extras, unknown } = resolveInstallTargets(requested);
|
|
672
|
+
if (unknown.length > 0) {
|
|
673
|
+
console.error(
|
|
674
|
+
`[ctxo] Unknown language "${unknown[0]}". Known: ${[...KNOWN_LANGUAGES, ...Object.keys(INSTALL_EXTRAS)].join(", ")}. Community plugins: see the ctxo-lang-template scaffolding guide.`
|
|
675
|
+
);
|
|
676
|
+
process.exitCode = 1;
|
|
677
|
+
return { languages: [], extras: [] };
|
|
767
678
|
}
|
|
768
|
-
return
|
|
679
|
+
return { languages, extras };
|
|
769
680
|
}
|
|
770
681
|
const detection = detectLanguages(this.projectRoot);
|
|
771
682
|
const needed = decideNeededLanguages(detection);
|
|
772
|
-
if (needed.length === 0) return [];
|
|
683
|
+
if (needed.length === 0) return { languages: [], extras: [] };
|
|
773
684
|
const missing = needed.filter((lang) => !isPluginInstalled(lang));
|
|
774
685
|
if (missing.length === 0) {
|
|
775
686
|
console.error("[ctxo] All detected languages already have plugins installed.");
|
|
776
|
-
return [];
|
|
687
|
+
return { languages: [], extras: [] };
|
|
777
688
|
}
|
|
778
689
|
console.error(`[ctxo] Detected missing plugins for: ${missing.join(", ")}`);
|
|
779
|
-
return missing;
|
|
690
|
+
return { languages: missing, extras: [] };
|
|
780
691
|
}
|
|
781
692
|
};
|
|
782
693
|
function normalizeLanguage(input) {
|
|
@@ -799,7 +710,7 @@ function normalizeLanguage(input) {
|
|
|
799
710
|
};
|
|
800
711
|
return aliases[lowered] ?? null;
|
|
801
712
|
}
|
|
802
|
-
var installRequire =
|
|
713
|
+
var installRequire = createRequire(import.meta.url);
|
|
803
714
|
function isPluginInstalled(lang) {
|
|
804
715
|
try {
|
|
805
716
|
installRequire.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
@@ -814,9 +725,9 @@ function isLockedCI(env) {
|
|
|
814
725
|
}
|
|
815
726
|
|
|
816
727
|
// src/cli/read-source.ts
|
|
817
|
-
import { readFileSync as
|
|
728
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
818
729
|
function readSourceText(absolutePath) {
|
|
819
|
-
const bytes =
|
|
730
|
+
const bytes = readFileSync3(absolutePath);
|
|
820
731
|
if (bytes.length >= 2) {
|
|
821
732
|
const first = bytes[0];
|
|
822
733
|
const second = bytes[1];
|
|
@@ -835,7 +746,7 @@ function decodeUtf16(bytes, bigEndian) {
|
|
|
835
746
|
|
|
836
747
|
// src/cli/index-cochange.ts
|
|
837
748
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
838
|
-
import { existsSync as
|
|
749
|
+
import { existsSync as existsSync6 } from "fs";
|
|
839
750
|
import { join as join6 } from "path";
|
|
840
751
|
function changeLogSize(windowCommits, maxHistory) {
|
|
841
752
|
return windowCommits === "all" ? "all" : Math.max(windowCommits, maxHistory * 50);
|
|
@@ -844,9 +755,7 @@ function writeCoChangeMatrix(options) {
|
|
|
844
755
|
const { writer, projectRoot, commits, head, headCommittedAt, coChange, indexedFiles } = options;
|
|
845
756
|
const isIncluded = coChangeFileFilter(projectRoot, indexedFiles, coChange);
|
|
846
757
|
if (isIncluded === void 0) return;
|
|
847
|
-
const
|
|
848
|
-
const cutoff = since !== void 0 ? sinceCutoff(headCommittedAt, since) : void 0;
|
|
849
|
-
const windowed = selectWindow(commits, coChange.maxCommits, cutoff);
|
|
758
|
+
const windowed = selectWindow(commits, coChange.maxCommits, windowCutoff(coChange, headCommittedAt));
|
|
850
759
|
const matrix = buildCoChangeMatrix(windowed, {
|
|
851
760
|
isIncluded,
|
|
852
761
|
maxCommits: coChange.maxCommits,
|
|
@@ -876,7 +785,7 @@ function coChangeFileFilter(projectRoot, indexedFiles, coChange) {
|
|
|
876
785
|
const isExcluded = makeGlobMatcher([...coChange.exclude]);
|
|
877
786
|
const present = /* @__PURE__ */ new Set();
|
|
878
787
|
for (const path of candidates) {
|
|
879
|
-
if (!isExcluded(path) &&
|
|
788
|
+
if (!isExcluded(path) && existsSync6(join6(projectRoot, path))) present.add(path);
|
|
880
789
|
}
|
|
881
790
|
return (path) => present.has(path);
|
|
882
791
|
}
|
|
@@ -903,14 +812,14 @@ function describeCoChangeWindow(coChange, commitsScanned) {
|
|
|
903
812
|
}
|
|
904
813
|
|
|
905
814
|
// src/cli/index-contexts-view.ts
|
|
906
|
-
import { existsSync as
|
|
815
|
+
import { existsSync as existsSync8 } from "fs";
|
|
907
816
|
import { join as join8 } from "path";
|
|
908
817
|
|
|
909
818
|
// src/adapters/storage/architecture-artifacts-writer.ts
|
|
910
|
-
import { existsSync as
|
|
911
|
-
import { join as join7, resolve
|
|
819
|
+
import { existsSync as existsSync7, mkdirSync, readFileSync as readFileSync4 } from "fs";
|
|
820
|
+
import { join as join7, resolve } from "path";
|
|
912
821
|
import { tmpdir } from "os";
|
|
913
|
-
var
|
|
822
|
+
var log2 = createLogger("ctxo:architecture-artifacts");
|
|
914
823
|
var DRIFT_FILE_NAME = "drift-events.json";
|
|
915
824
|
var VIOLATIONS_FILE_NAME = "boundary-violations.json";
|
|
916
825
|
var CONTEXTS_VIEW_FILE_NAME = "contexts-view.json";
|
|
@@ -937,7 +846,7 @@ var ArchitectureArtifactsWriter = class {
|
|
|
937
846
|
const path = join7(this.indexDir, CONTEXTS_VIEW_FILE_NAME);
|
|
938
847
|
const content = JSON.stringify(view, null, 2);
|
|
939
848
|
try {
|
|
940
|
-
if (
|
|
849
|
+
if (readFileSync4(path, "utf-8") === content) return false;
|
|
941
850
|
} catch {
|
|
942
851
|
}
|
|
943
852
|
mkdirSync(this.indexDir, { recursive: true });
|
|
@@ -946,28 +855,28 @@ var ArchitectureArtifactsWriter = class {
|
|
|
946
855
|
}
|
|
947
856
|
readDriftEvents() {
|
|
948
857
|
const path = join7(this.indexDir, DRIFT_FILE_NAME);
|
|
949
|
-
if (!
|
|
858
|
+
if (!existsSync7(path)) return void 0;
|
|
950
859
|
try {
|
|
951
|
-
return JSON.parse(
|
|
860
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
952
861
|
} catch (err) {
|
|
953
|
-
|
|
862
|
+
log2.warn(`failed to parse ${DRIFT_FILE_NAME}: ${err.message}`);
|
|
954
863
|
return void 0;
|
|
955
864
|
}
|
|
956
865
|
}
|
|
957
866
|
readBoundaryViolations() {
|
|
958
867
|
const path = join7(this.indexDir, VIOLATIONS_FILE_NAME);
|
|
959
|
-
if (!
|
|
868
|
+
if (!existsSync7(path)) return void 0;
|
|
960
869
|
try {
|
|
961
|
-
return JSON.parse(
|
|
870
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
962
871
|
} catch (err) {
|
|
963
|
-
|
|
872
|
+
log2.warn(`failed to parse ${VIOLATIONS_FILE_NAME}: ${err.message}`);
|
|
964
873
|
return void 0;
|
|
965
874
|
}
|
|
966
875
|
}
|
|
967
876
|
};
|
|
968
877
|
function assertSafeWritePath(ctxoRoot, allowProductionPath) {
|
|
969
|
-
const resolvedRoot =
|
|
970
|
-
const resolvedTmp =
|
|
878
|
+
const resolvedRoot = resolve(ctxoRoot);
|
|
879
|
+
const resolvedTmp = resolve(tmpdir());
|
|
971
880
|
if (resolvedRoot.startsWith(resolvedTmp)) return;
|
|
972
881
|
if (allowProductionPath) return;
|
|
973
882
|
throw new Error(
|
|
@@ -1851,14 +1760,9 @@ function fileOfSymbol(symbolId) {
|
|
|
1851
1760
|
const cut = symbolId.indexOf("::");
|
|
1852
1761
|
return cut === -1 ? symbolId : symbolId.slice(0, cut);
|
|
1853
1762
|
}
|
|
1854
|
-
function hasCSharpAnnotations(indices) {
|
|
1855
|
-
return indices.some(
|
|
1856
|
-
(index) => index.file.endsWith(".cs") && index.symbols.some((symbol) => symbol.annotations && symbol.annotations.length > 0)
|
|
1857
|
-
);
|
|
1858
|
-
}
|
|
1859
1763
|
function computeAndWriteContextsView(options) {
|
|
1860
1764
|
const { projectRoot, ctxoRoot, config, indices, snapshot, symbolPageRank } = options;
|
|
1861
|
-
const exists = options.exists ??
|
|
1765
|
+
const exists = options.exists ?? existsSync8;
|
|
1862
1766
|
const settings = resolveContextsViewConfig(config);
|
|
1863
1767
|
const isTest = createTestFileClassifier(indexTestPatterns(config), filesWithTestAnnotations(indices), {
|
|
1864
1768
|
annotationAware: hasCSharpAnnotations(indices)
|
|
@@ -1913,8 +1817,8 @@ async function extractFileFacts(adapter, relativePath, source) {
|
|
|
1913
1817
|
}
|
|
1914
1818
|
|
|
1915
1819
|
// src/adapters/storage/json-index-writer.ts
|
|
1916
|
-
import { mkdirSync as mkdirSync2, unlinkSync, existsSync as
|
|
1917
|
-
import { dirname as
|
|
1820
|
+
import { mkdirSync as mkdirSync2, unlinkSync, existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
1821
|
+
import { dirname as dirname3, join as join9, relative, resolve as resolve2, sep } from "path";
|
|
1918
1822
|
import { isDeepStrictEqual } from "util";
|
|
1919
1823
|
var JsonIndexWriter = class {
|
|
1920
1824
|
indexDir;
|
|
@@ -1926,7 +1830,7 @@ var JsonIndexWriter = class {
|
|
|
1926
1830
|
throw new Error("FileIndex.file must not be empty");
|
|
1927
1831
|
}
|
|
1928
1832
|
const targetPath = this.resolveIndexPath(fileIndex.file);
|
|
1929
|
-
mkdirSync2(
|
|
1833
|
+
mkdirSync2(dirname3(targetPath), { recursive: true });
|
|
1930
1834
|
const sorted = this.sortKeys(fileIndex);
|
|
1931
1835
|
const json = JSON.stringify(sorted, null, 2);
|
|
1932
1836
|
this.atomicWrite(targetPath, json);
|
|
@@ -1945,9 +1849,9 @@ var JsonIndexWriter = class {
|
|
|
1945
1849
|
}
|
|
1946
1850
|
/** True when the file at `targetPath` parses and has the same `window` and `entries` as `matrix`. */
|
|
1947
1851
|
holdsSameCoChanges(targetPath, matrix) {
|
|
1948
|
-
if (!
|
|
1852
|
+
if (!existsSync9(targetPath)) return false;
|
|
1949
1853
|
try {
|
|
1950
|
-
const existing = JSON.parse(
|
|
1854
|
+
const existing = JSON.parse(readFileSync5(targetPath, "utf-8"));
|
|
1951
1855
|
if (existing === null || typeof existing !== "object") return false;
|
|
1952
1856
|
const { window, entries } = existing;
|
|
1953
1857
|
return isDeepStrictEqual(window, matrix.window) && isDeepStrictEqual(entries, matrix.entries);
|
|
@@ -1960,7 +1864,7 @@ var JsonIndexWriter = class {
|
|
|
1960
1864
|
}
|
|
1961
1865
|
delete(relativePath) {
|
|
1962
1866
|
const targetPath = this.resolveIndexPath(relativePath);
|
|
1963
|
-
if (
|
|
1867
|
+
if (existsSync9(targetPath)) {
|
|
1964
1868
|
unlinkSync(targetPath);
|
|
1965
1869
|
}
|
|
1966
1870
|
}
|
|
@@ -1970,11 +1874,12 @@ var JsonIndexWriter = class {
|
|
|
1970
1874
|
* an artifact's included. Returns true when a file was deleted and false when there was none.
|
|
1971
1875
|
*
|
|
1972
1876
|
* Throws for a path outside the index folder, and for the artifacts that live there but are not entries
|
|
1973
|
-
* (the co-change matrix, the community snapshot and its history, the architecture artifacts
|
|
1877
|
+
* (the co-change matrix, the community snapshot and its history, the architecture artifacts, and any JSON
|
|
1878
|
+
* carrying the `ctxoArtifact` marker of a module artifact).
|
|
1974
1879
|
*/
|
|
1975
1880
|
deleteEntryFile(jsonPath) {
|
|
1976
|
-
const resolved =
|
|
1977
|
-
const indexDir =
|
|
1881
|
+
const resolved = resolve2(jsonPath);
|
|
1882
|
+
const indexDir = resolve2(this.indexDir);
|
|
1978
1883
|
if (!resolved.startsWith(indexDir + sep)) {
|
|
1979
1884
|
throw new Error(`Not inside the index folder: ${jsonPath}`);
|
|
1980
1885
|
}
|
|
@@ -1982,13 +1887,33 @@ var JsonIndexWriter = class {
|
|
|
1982
1887
|
if (INDEX_ARTIFACT_FILES.includes(inIndex) || inIndex.startsWith(`${COMMUNITY_HISTORY_DIR}/`)) {
|
|
1983
1888
|
throw new Error(`Refusing to delete the index artifact ${inIndex}`);
|
|
1984
1889
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1890
|
+
let text;
|
|
1891
|
+
try {
|
|
1892
|
+
text = readFileSync5(resolved, "utf-8");
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
if (err.code === "ENOENT") return false;
|
|
1895
|
+
throw err;
|
|
1896
|
+
}
|
|
1897
|
+
let data;
|
|
1898
|
+
try {
|
|
1899
|
+
data = JSON.parse(text);
|
|
1900
|
+
} catch {
|
|
1901
|
+
data = void 0;
|
|
1902
|
+
}
|
|
1903
|
+
if (isMarkedArtifact(data)) {
|
|
1904
|
+
throw new Error(`Refusing to delete the index artifact ${inIndex}`);
|
|
1905
|
+
}
|
|
1906
|
+
try {
|
|
1907
|
+
unlinkSync(resolved);
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
if (err.code === "ENOENT") return false;
|
|
1910
|
+
throw err;
|
|
1911
|
+
}
|
|
1987
1912
|
return true;
|
|
1988
1913
|
}
|
|
1989
1914
|
resolveIndexPath(relativePath) {
|
|
1990
|
-
const resolved =
|
|
1991
|
-
const normalizedDir =
|
|
1915
|
+
const resolved = resolve2(this.indexDir, `${relativePath}.json`);
|
|
1916
|
+
const normalizedDir = resolve2(this.indexDir) + sep;
|
|
1992
1917
|
if (!resolved.startsWith(normalizedDir)) {
|
|
1993
1918
|
throw new Error(`Path traversal detected: ${relativePath}`);
|
|
1994
1919
|
}
|
|
@@ -2010,8 +1935,8 @@ var JsonIndexWriter = class {
|
|
|
2010
1935
|
};
|
|
2011
1936
|
|
|
2012
1937
|
// src/adapters/storage/schema-manager.ts
|
|
2013
|
-
import { readFileSync as
|
|
2014
|
-
import { join as join10, dirname as
|
|
1938
|
+
import { readFileSync as readFileSync6, writeFileSync, existsSync as existsSync10, mkdirSync as mkdirSync3 } from "fs";
|
|
1939
|
+
import { join as join10, dirname as dirname4 } from "path";
|
|
2015
1940
|
var CURRENT_SCHEMA_VERSION = "1.0.0";
|
|
2016
1941
|
var SchemaManager = class {
|
|
2017
1942
|
versionFilePath;
|
|
@@ -2022,13 +1947,13 @@ var SchemaManager = class {
|
|
|
2022
1947
|
return CURRENT_SCHEMA_VERSION;
|
|
2023
1948
|
}
|
|
2024
1949
|
readStoredVersion() {
|
|
2025
|
-
if (!
|
|
1950
|
+
if (!existsSync10(this.versionFilePath)) {
|
|
2026
1951
|
return void 0;
|
|
2027
1952
|
}
|
|
2028
|
-
return
|
|
1953
|
+
return readFileSync6(this.versionFilePath, "utf-8").trim();
|
|
2029
1954
|
}
|
|
2030
1955
|
writeVersion() {
|
|
2031
|
-
mkdirSync3(
|
|
1956
|
+
mkdirSync3(dirname4(this.versionFilePath), { recursive: true });
|
|
2032
1957
|
writeFileSync(this.versionFilePath, CURRENT_SCHEMA_VERSION, "utf-8");
|
|
2033
1958
|
}
|
|
2034
1959
|
isCompatible() {
|
|
@@ -2332,7 +2257,7 @@ var CommunityDetector = class {
|
|
|
2332
2257
|
};
|
|
2333
2258
|
|
|
2334
2259
|
// src/cli/index-command.ts
|
|
2335
|
-
var requireCjs =
|
|
2260
|
+
var requireCjs = createRequire2(import.meta.url);
|
|
2336
2261
|
var IndexCommand = class {
|
|
2337
2262
|
projectRoot;
|
|
2338
2263
|
ctxoRoot;
|
|
@@ -2370,8 +2295,10 @@ var IndexCommand = class {
|
|
|
2370
2295
|
const gitAdapter = new SimpleGitAdapter(this.projectRoot);
|
|
2371
2296
|
const incremental = options.files !== void 0 || options.commit !== void 0 || Boolean(options.file);
|
|
2372
2297
|
const commitChanges = options.commit !== void 0 ? await readCommitChanges(gitAdapter, options.commit) : [];
|
|
2298
|
+
const modules = options.modules ?? [];
|
|
2299
|
+
const requestFacts = factsRequested(ignore.config) || modules.some((module) => module.registration.indexCapabilities?.facts === true);
|
|
2373
2300
|
const registry = new LanguageAdapterRegistry();
|
|
2374
|
-
const plugins = await this.registerDiscoveredPlugins(registry, ignore.ignoreProjectPatterns,
|
|
2301
|
+
const plugins = await this.registerDiscoveredPlugins(registry, ignore.ignoreProjectPatterns, requestFacts);
|
|
2375
2302
|
const { tsMorphLike, roslynAdapter, csharpComposite, goTier, javaTier } = plugins;
|
|
2376
2303
|
const csharpTierAtLoad = plugins.csharpTier;
|
|
2377
2304
|
let csharpTier = plugins.csharpTier;
|
|
@@ -2494,7 +2421,7 @@ var IndexCommand = class {
|
|
|
2494
2421
|
console.error(`[ctxo] Edge extraction failed for ${entry.relativePath}: ${err.message}`);
|
|
2495
2422
|
}
|
|
2496
2423
|
}
|
|
2497
|
-
if (
|
|
2424
|
+
if (requestFacts) {
|
|
2498
2425
|
for (const entry of pendingIndices) {
|
|
2499
2426
|
const adapter = registry.getAdapter(entry.relativePath);
|
|
2500
2427
|
if (!adapter) continue;
|
|
@@ -2529,6 +2456,7 @@ var IndexCommand = class {
|
|
|
2529
2456
|
registry.getSupportedExtensions().size === 0 ? "[ctxo] No language plugin is loaded, so no file was indexed. Run `ctxo install` (or `ctxo index --install-missing`). communities.json, its history and the architecture artifacts were left as they are" : "[ctxo] No source file was indexed. communities.json, its history and the architecture artifacts were left as they are"
|
|
2530
2457
|
);
|
|
2531
2458
|
}
|
|
2459
|
+
let phase2ChangeLog;
|
|
2532
2460
|
if (!options.skipHistory && refreshCoChange) {
|
|
2533
2461
|
try {
|
|
2534
2462
|
const coChange = resolveCoChangeConfig(ignore.config, {
|
|
@@ -2537,6 +2465,7 @@ var IndexCommand = class {
|
|
|
2537
2465
|
});
|
|
2538
2466
|
const maxHistory = options.maxHistory ?? 20;
|
|
2539
2467
|
const changeLog = await gitAdapter.getChangeLog(changeLogSize(coChange.maxCommits, maxHistory));
|
|
2468
|
+
if (changeLog !== void 0) phase2ChangeLog = { changeLog, coChange };
|
|
2540
2469
|
if (changeLog === void 0) {
|
|
2541
2470
|
console.error("[ctxo] Git history unavailable: intent[] left empty and co-changes.json not updated");
|
|
2542
2471
|
} else {
|
|
@@ -2569,7 +2498,7 @@ var IndexCommand = class {
|
|
|
2569
2498
|
let communitySnapshot;
|
|
2570
2499
|
let symbolPageRank;
|
|
2571
2500
|
if (options.skipCommunity && refreshProducts) {
|
|
2572
|
-
if (
|
|
2501
|
+
if (existsSync11(join11(this.ctxoRoot, "index", "communities.json"))) {
|
|
2573
2502
|
console.error(
|
|
2574
2503
|
`[ctxo] WARN stale communities.json present \u2014 --skip-community preserved existing snapshot. Run \`ctxo index\` (full) to refresh.`
|
|
2575
2504
|
);
|
|
@@ -2646,6 +2575,9 @@ var IndexCommand = class {
|
|
|
2646
2575
|
}
|
|
2647
2576
|
}
|
|
2648
2577
|
}
|
|
2578
|
+
if (refreshProducts) {
|
|
2579
|
+
await this.runModuleIndexPasses(modules, ignore.config, indexed, phase2ChangeLog);
|
|
2580
|
+
}
|
|
2649
2581
|
if (!options.skipSideEffects) {
|
|
2650
2582
|
this.ensureGitignore();
|
|
2651
2583
|
}
|
|
@@ -2664,6 +2596,49 @@ var IndexCommand = class {
|
|
|
2664
2596
|
if (sqlCount > 0) console.error(`[ctxo] SQL: ${sqlCount} files (syntax tier)`);
|
|
2665
2597
|
if (roslynAdapter) await roslynAdapter.dispose();
|
|
2666
2598
|
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Phase 2e. Runs each module's `indexPass` on the merged index (the one the snapshot, the artifacts and
|
|
2601
|
+
* the contexts view were built from), with a graph built on first use, the host's test-file check and,
|
|
2602
|
+
* when Phase 2 read the change log, the raw commits of the co-change window (renames intact). Each pass
|
|
2603
|
+
* is awaited in turn; one that throws is reported and the next one still runs.
|
|
2604
|
+
*/
|
|
2605
|
+
async runModuleIndexPasses(modules, config, indices, phase2) {
|
|
2606
|
+
const passes = modules.filter((module) => typeof module.registration.indexPass === "function");
|
|
2607
|
+
if (passes.length === 0) return;
|
|
2608
|
+
let ctx;
|
|
2609
|
+
try {
|
|
2610
|
+
const { createModuleHost } = await import("./module-host-KTGDODES.js");
|
|
2611
|
+
const { services } = createModuleHost({ projectRoot: this.projectRoot, mode: "cli" });
|
|
2612
|
+
let graph;
|
|
2613
|
+
ctx = {
|
|
2614
|
+
projectRoot: this.projectRoot,
|
|
2615
|
+
ctxoRoot: this.ctxoRoot,
|
|
2616
|
+
config,
|
|
2617
|
+
indices,
|
|
2618
|
+
graph: () => graph ??= services.buildGraph(indices),
|
|
2619
|
+
isTestFile: services.createTestFileCheck(config, indices),
|
|
2620
|
+
history: phase2 === void 0 ? void 0 : {
|
|
2621
|
+
head: phase2.changeLog.head,
|
|
2622
|
+
headCommittedAt: phase2.changeLog.headCommittedAt,
|
|
2623
|
+
commits: selectWindow(
|
|
2624
|
+
phase2.changeLog.commits,
|
|
2625
|
+
phase2.coChange.maxCommits,
|
|
2626
|
+
windowCutoff(phase2.coChange, phase2.changeLog.headCommittedAt)
|
|
2627
|
+
)
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
console.error(`[ctxo] Module index passes skipped: ${err.message}`);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
for (const module of passes) {
|
|
2635
|
+
try {
|
|
2636
|
+
await module.registration.indexPass(ctx);
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
console.error(`[ctxo] ${module.name} index pass failed: ${err.message}`);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2667
2642
|
async autoInstallMissingPlugins() {
|
|
2668
2643
|
const detection = detectLanguages(this.projectRoot);
|
|
2669
2644
|
const needed = decideNeededLanguages(detection);
|
|
@@ -2743,7 +2718,7 @@ var IndexCommand = class {
|
|
|
2743
2718
|
return output.split("\0").filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) => join11(root, line)).filter((abs) => {
|
|
2744
2719
|
const rel = relative2(this.projectRoot, abs).replace(/\\/g, "/");
|
|
2745
2720
|
return !ignoreFile(rel);
|
|
2746
|
-
}).filter((abs) =>
|
|
2721
|
+
}).filter((abs) => existsSync11(abs));
|
|
2747
2722
|
} catch {
|
|
2748
2723
|
console.error(`[ctxo] git ls-files failed for ${root}`);
|
|
2749
2724
|
return [];
|
|
@@ -2755,16 +2730,16 @@ var IndexCommand = class {
|
|
|
2755
2730
|
*/
|
|
2756
2731
|
discoverWorkspaces(ignoreProject = () => false) {
|
|
2757
2732
|
const pkgPath = join11(this.projectRoot, "package.json");
|
|
2758
|
-
if (!
|
|
2733
|
+
if (!existsSync11(pkgPath)) return { kept: [this.projectRoot], ignored: [] };
|
|
2759
2734
|
try {
|
|
2760
|
-
const pkg = JSON.parse(
|
|
2735
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
2761
2736
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
2762
2737
|
if (!workspaces || workspaces.length === 0) return { kept: [this.projectRoot], ignored: [] };
|
|
2763
2738
|
const resolved = [];
|
|
2764
2739
|
for (const ws of workspaces) {
|
|
2765
2740
|
if (ws.endsWith("/*") || ws.endsWith("\\*")) {
|
|
2766
2741
|
const parentDir = join11(this.projectRoot, ws.slice(0, -2));
|
|
2767
|
-
if (
|
|
2742
|
+
if (existsSync11(parentDir)) {
|
|
2768
2743
|
for (const entry of readdirSync2(parentDir, { withFileTypes: true })) {
|
|
2769
2744
|
if (entry.isDirectory()) {
|
|
2770
2745
|
resolved.push(join11(parentDir, entry.name));
|
|
@@ -2773,7 +2748,7 @@ var IndexCommand = class {
|
|
|
2773
2748
|
}
|
|
2774
2749
|
} else {
|
|
2775
2750
|
const wsPath = join11(this.projectRoot, ws);
|
|
2776
|
-
if (
|
|
2751
|
+
if (existsSync11(wsPath)) {
|
|
2777
2752
|
resolved.push(wsPath);
|
|
2778
2753
|
}
|
|
2779
2754
|
}
|
|
@@ -2808,7 +2783,7 @@ var IndexCommand = class {
|
|
|
2808
2783
|
if (_roslyn) await _roslyn.dispose();
|
|
2809
2784
|
const hasher = new ContentHasher();
|
|
2810
2785
|
const files = this.discoverFilesIn(this.projectRoot);
|
|
2811
|
-
const reader = new (await import("./json-index-reader-
|
|
2786
|
+
const reader = new (await import("./json-index-reader-DKEIP2QR.js")).JsonIndexReader(this.ctxoRoot);
|
|
2812
2787
|
const indices = reader.readAll();
|
|
2813
2788
|
const indexedMap = new Map(indices.map((i) => [i.file, i]));
|
|
2814
2789
|
let staleCount = 0;
|
|
@@ -2821,7 +2796,7 @@ var IndexCommand = class {
|
|
|
2821
2796
|
staleCount++;
|
|
2822
2797
|
continue;
|
|
2823
2798
|
}
|
|
2824
|
-
if (!
|
|
2799
|
+
if (!existsSync11(filePath)) {
|
|
2825
2800
|
console.error(`[ctxo] DELETED: ${relativePath}`);
|
|
2826
2801
|
staleCount++;
|
|
2827
2802
|
continue;
|
|
@@ -2853,7 +2828,7 @@ var IndexCommand = class {
|
|
|
2853
2828
|
# Ctxo local cache (never committed)
|
|
2854
2829
|
${cachePattern}
|
|
2855
2830
|
`;
|
|
2856
|
-
const existing =
|
|
2831
|
+
const existing = existsSync11(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : "";
|
|
2857
2832
|
if (existing.includes(cachePattern)) return;
|
|
2858
2833
|
writeFileSync2(gitignorePath, existing + suffix, "utf-8");
|
|
2859
2834
|
console.error("[ctxo] Added .ctxo/.cache/ to .gitignore");
|
|
@@ -2900,7 +2875,7 @@ ${cachePattern}
|
|
|
2900
2875
|
for (const path of [...candidates].sort(compareCodepoint)) {
|
|
2901
2876
|
if (!isIndexable(path)) {
|
|
2902
2877
|
if (listed.has(path)) skippedListed += 1;
|
|
2903
|
-
} else if (
|
|
2878
|
+
} else if (existsSync11(join11(this.projectRoot, path))) {
|
|
2904
2879
|
changed.push(path);
|
|
2905
2880
|
} else {
|
|
2906
2881
|
missing.push(path);
|
|
@@ -2953,7 +2928,7 @@ ${cachePattern}
|
|
|
2953
2928
|
* have been committed by a machine that has the plugin, and the file is still there.
|
|
2954
2929
|
*/
|
|
2955
2930
|
findStaleEntries(committed, reindexed, isIgnored) {
|
|
2956
|
-
return committed.map((idx) => idx.file).filter((file) => !reindexed.has(file) && (!
|
|
2931
|
+
return committed.map((idx) => idx.file).filter((file) => !reindexed.has(file) && (!existsSync11(join11(this.projectRoot, file)) || isIgnored(file))).sort(compareCodepoint);
|
|
2957
2932
|
}
|
|
2958
2933
|
/**
|
|
2959
2934
|
* Produce the FileIndex list used as input to community detection, the architecture artifacts and the
|
|
@@ -3127,13 +3102,13 @@ async function readCommitChanges(gitAdapter, rev) {
|
|
|
3127
3102
|
function toRepoPath(projectRoot, input) {
|
|
3128
3103
|
const trimmed = input.trim().replace(/\\/g, "/");
|
|
3129
3104
|
if (trimmed === "") return void 0;
|
|
3130
|
-
const relativePath = relative2(projectRoot,
|
|
3131
|
-
if (relativePath === "" ||
|
|
3105
|
+
const relativePath = relative2(projectRoot, resolve3(projectRoot, trimmed));
|
|
3106
|
+
if (relativePath === "" || isAbsolute(relativePath) || relativePath.split(/[\\/]/)[0] === "..") return void 0;
|
|
3132
3107
|
return relativePath.replace(/\\/g, "/");
|
|
3133
3108
|
}
|
|
3134
3109
|
function isInsideDirectory(directory, absolute) {
|
|
3135
3110
|
const relativePath = relative2(directory, absolute);
|
|
3136
|
-
return relativePath !== "" && !
|
|
3111
|
+
return relativePath !== "" && !isAbsolute(relativePath) && relativePath.split(/[\\/]/)[0] !== "..";
|
|
3137
3112
|
}
|
|
3138
3113
|
function countCommunities(snapshot) {
|
|
3139
3114
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -3162,7 +3137,7 @@ var SyncCommand = class {
|
|
|
3162
3137
|
|
|
3163
3138
|
// src/cli/status-command.ts
|
|
3164
3139
|
import { join as join13 } from "path";
|
|
3165
|
-
import { existsSync as
|
|
3140
|
+
import { existsSync as existsSync12 } from "fs";
|
|
3166
3141
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
3167
3142
|
var StatusCommand = class {
|
|
3168
3143
|
projectRoot;
|
|
@@ -3173,7 +3148,7 @@ var StatusCommand = class {
|
|
|
3173
3148
|
}
|
|
3174
3149
|
run() {
|
|
3175
3150
|
const indexDir = join13(this.ctxoRoot, "index");
|
|
3176
|
-
if (!
|
|
3151
|
+
if (!existsSync12(indexDir)) {
|
|
3177
3152
|
console.error('[ctxo] No index found. Run "ctxo index" first.');
|
|
3178
3153
|
return;
|
|
3179
3154
|
}
|
|
@@ -3188,7 +3163,7 @@ var StatusCommand = class {
|
|
|
3188
3163
|
console.error(` Indexed files: ${indices.length}`);
|
|
3189
3164
|
console.error(` Total symbols: ${totalSymbols}`);
|
|
3190
3165
|
console.error(` Total edges: ${totalEdges}`);
|
|
3191
|
-
const cacheExists =
|
|
3166
|
+
const cacheExists = existsSync12(join13(this.ctxoRoot, ".cache", "symbols.db"));
|
|
3192
3167
|
console.error(` SQLite cache: ${cacheExists ? "present" : "missing (run ctxo sync)"}`);
|
|
3193
3168
|
if (indices.length > 0) {
|
|
3194
3169
|
console.error("");
|
|
@@ -3264,13 +3239,13 @@ var VerifyCommand = class {
|
|
|
3264
3239
|
};
|
|
3265
3240
|
|
|
3266
3241
|
// src/cli/init-command.ts
|
|
3267
|
-
import { isAbsolute as
|
|
3268
|
-
import { existsSync as
|
|
3269
|
-
import { createRequire as
|
|
3242
|
+
import { isAbsolute as isAbsolute2, join as join17, relative as relative4 } from "path";
|
|
3243
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, chmodSync } from "fs";
|
|
3244
|
+
import { createRequire as createRequire3 } from "module";
|
|
3270
3245
|
|
|
3271
3246
|
// src/cli/ai-rules.ts
|
|
3272
|
-
import { existsSync as
|
|
3273
|
-
import { join as join15, dirname as
|
|
3247
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, realpathSync, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
|
|
3248
|
+
import { join as join15, dirname as dirname5, relative as relative3, sep as sep2 } from "path";
|
|
3274
3249
|
|
|
3275
3250
|
// src/core/agent-rules/rules-block.ts
|
|
3276
3251
|
var RULES_TEMPLATE_VERSION = "0.13";
|
|
@@ -3564,16 +3539,20 @@ function guardAndSkillsExpected(block) {
|
|
|
3564
3539
|
// src/cli/ai-rules.ts
|
|
3565
3540
|
var CTXO_HOOK_COMMAND = "ctxo gate-hook";
|
|
3566
3541
|
var CLAUDE_SETTINGS_FILE = ".claude/settings.json";
|
|
3567
|
-
var
|
|
3542
|
+
var GUARD_RERUN_PARTS = "rules,skills,guard";
|
|
3543
|
+
function unreadableSettingsWarning(rerun) {
|
|
3544
|
+
return `${CLAUDE_SETTINGS_FILE} is not valid JSON or not a settings object; left unchanged. Fix it, then run "ctxo init --only ${rerun}".`;
|
|
3545
|
+
}
|
|
3568
3546
|
function isPlainObject(value) {
|
|
3569
3547
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3570
3548
|
}
|
|
3571
|
-
|
|
3572
|
-
|
|
3549
|
+
var GUARD_PROBE = { event: "PreToolUse", command: CTXO_HOOK_COMMAND };
|
|
3550
|
+
function readSettings(path, probe = GUARD_PROBE) {
|
|
3551
|
+
if (!existsSync13(path)) return { kind: "missing" };
|
|
3573
3552
|
let json;
|
|
3574
3553
|
let shape;
|
|
3575
3554
|
try {
|
|
3576
|
-
shape = textShape(
|
|
3555
|
+
shape = textShape(readFileSync8(path, "utf-8"));
|
|
3577
3556
|
json = JSON.parse(shape.text);
|
|
3578
3557
|
} catch {
|
|
3579
3558
|
return { kind: "unreadable" };
|
|
@@ -3581,19 +3560,19 @@ function readSettings(path) {
|
|
|
3581
3560
|
if (!isPlainObject(json)) return { kind: "unreadable" };
|
|
3582
3561
|
if (json.hooks === void 0) return { kind: "ok", json, hasHook: false, shape };
|
|
3583
3562
|
if (!isPlainObject(json.hooks)) return { kind: "unreadable" };
|
|
3584
|
-
const
|
|
3585
|
-
if (
|
|
3586
|
-
if (!Array.isArray(
|
|
3587
|
-
const wellFormed =
|
|
3563
|
+
const list = json.hooks[probe.event];
|
|
3564
|
+
if (list === void 0) return { kind: "ok", json, hasHook: false, shape };
|
|
3565
|
+
if (!Array.isArray(list)) return { kind: "unreadable" };
|
|
3566
|
+
const wellFormed = list.filter(
|
|
3588
3567
|
(entry) => isPlainObject(entry) && Array.isArray(entry.hooks)
|
|
3589
3568
|
);
|
|
3590
|
-
const hasHook = wellFormed.some((entry) => entry.hooks.some((h) => isPlainObject(h) && h.command ===
|
|
3591
|
-
if (!hasHook && wellFormed.length !==
|
|
3569
|
+
const hasHook = wellFormed.some((entry) => entry.hooks.some((h) => isPlainObject(h) && h.command === probe.command));
|
|
3570
|
+
if (!hasHook && wellFormed.length !== list.length) return { kind: "unreadable" };
|
|
3592
3571
|
return { kind: "ok", json, hasHook, shape };
|
|
3593
3572
|
}
|
|
3594
|
-
function guardRegistration(projectRoot) {
|
|
3595
|
-
const shared = readSettings(join15(projectRoot, ".claude", "settings.json"));
|
|
3596
|
-
const local = readSettings(join15(projectRoot, ".claude", "settings.local.json"));
|
|
3573
|
+
function guardRegistration(projectRoot, probe = GUARD_PROBE) {
|
|
3574
|
+
const shared = readSettings(join15(projectRoot, ".claude", "settings.json"), probe);
|
|
3575
|
+
const local = readSettings(join15(projectRoot, ".claude", "settings.local.json"), probe);
|
|
3597
3576
|
if (shared.kind === "ok" && shared.hasHook || local.kind === "ok" && local.hasHook) {
|
|
3598
3577
|
return { shared, state: "present" };
|
|
3599
3578
|
}
|
|
@@ -3606,7 +3585,7 @@ function platformRulesState(projectRoot, platform) {
|
|
|
3606
3585
|
const path = join15(projectRoot, platform.file);
|
|
3607
3586
|
let text;
|
|
3608
3587
|
try {
|
|
3609
|
-
text =
|
|
3588
|
+
text = readFileSync8(path, "utf-8");
|
|
3610
3589
|
} catch (err) {
|
|
3611
3590
|
const code = err.code;
|
|
3612
3591
|
if (code === "ENOENT" || code === "EISDIR") return void 0;
|
|
@@ -3639,11 +3618,11 @@ function sameFileOnDisk(a, b) {
|
|
|
3639
3618
|
function linkedRulesPlatforms(projectRoot, platformId) {
|
|
3640
3619
|
const platform = PLATFORMS.find((p) => p.id === platformId);
|
|
3641
3620
|
const path = platform ? join15(projectRoot, platform.file) : void 0;
|
|
3642
|
-
if (!path || !
|
|
3621
|
+
if (!path || !existsSync13(path)) return [];
|
|
3643
3622
|
return PLATFORMS.filter((other) => {
|
|
3644
3623
|
if (other.id === platformId) return false;
|
|
3645
3624
|
const otherPath = join15(projectRoot, other.file);
|
|
3646
|
-
return
|
|
3625
|
+
return existsSync13(otherPath) && sameFileOnDisk(path, otherPath);
|
|
3647
3626
|
}).map((other) => other.id);
|
|
3648
3627
|
}
|
|
3649
3628
|
function firstRulesBlock(projectRoot, platformId) {
|
|
@@ -3706,23 +3685,29 @@ function rulesFileHoldReasons(projectRoot, platformId, optionsFor = () => ({}))
|
|
|
3706
3685
|
return void 0;
|
|
3707
3686
|
}
|
|
3708
3687
|
function ensureClaudeHook(projectRoot, options = {}) {
|
|
3688
|
+
return ensureClaudeHookEntry(projectRoot, { event: "PreToolUse", matcher: "Edit", command: CTXO_HOOK_COMMAND }, options);
|
|
3689
|
+
}
|
|
3690
|
+
function ensureClaudeHookEntry(projectRoot, entry, options = {}) {
|
|
3709
3691
|
const filePath = join15(projectRoot, ".claude", "settings.json");
|
|
3710
|
-
const
|
|
3711
|
-
|
|
3692
|
+
const hookEntry = {
|
|
3693
|
+
...entry.matcher !== void 0 ? { matcher: entry.matcher } : {},
|
|
3694
|
+
hooks: [{ type: "command", command: entry.command }]
|
|
3695
|
+
};
|
|
3696
|
+
const { shared, state } = guardRegistration(projectRoot, { event: entry.event, command: entry.command });
|
|
3712
3697
|
if (state === "present") return { file: CLAUDE_SETTINGS_FILE, action: "skipped" };
|
|
3713
3698
|
if (shared.kind === "unreadable") {
|
|
3714
|
-
return { file: CLAUDE_SETTINGS_FILE, action: "skipped", warning:
|
|
3699
|
+
return { file: CLAUDE_SETTINGS_FILE, action: "skipped", warning: unreadableSettingsWarning(options.rerun ?? GUARD_RERUN_PARTS) };
|
|
3715
3700
|
}
|
|
3716
3701
|
if (shared.kind === "missing") {
|
|
3717
3702
|
if (!options.dryRun) {
|
|
3718
|
-
mkdirSync4(
|
|
3719
|
-
writeFileSync3(filePath, JSON.stringify({ hooks: {
|
|
3703
|
+
mkdirSync4(dirname5(filePath), { recursive: true });
|
|
3704
|
+
writeFileSync3(filePath, JSON.stringify({ hooks: { [entry.event]: [hookEntry] } }, null, 2) + "\n", "utf-8");
|
|
3720
3705
|
}
|
|
3721
3706
|
return { file: CLAUDE_SETTINGS_FILE, action: "created" };
|
|
3722
3707
|
}
|
|
3723
3708
|
const hooks = isPlainObject(shared.json.hooks) ? shared.json.hooks : {};
|
|
3724
|
-
const
|
|
3725
|
-
const updated = { ...shared.json, hooks: { ...hooks,
|
|
3709
|
+
const list = Array.isArray(hooks[entry.event]) ? hooks[entry.event] : [];
|
|
3710
|
+
const updated = { ...shared.json, hooks: { ...hooks, [entry.event]: [...list, hookEntry] } };
|
|
3726
3711
|
const body = JSON.stringify(updated, null, 2) + "\n";
|
|
3727
3712
|
if (!options.dryRun) writeFileSync3(filePath, shared.shape.restore(body), "utf-8");
|
|
3728
3713
|
return { file: CLAUDE_SETTINGS_FILE, action: "updated" };
|
|
@@ -3752,7 +3737,7 @@ var PLATFORMS = [
|
|
|
3752
3737
|
function detectPlatforms(projectRoot) {
|
|
3753
3738
|
const detected = /* @__PURE__ */ new Set();
|
|
3754
3739
|
for (const p of PLATFORMS) {
|
|
3755
|
-
if (p.detectPaths.some((d) =>
|
|
3740
|
+
if (p.detectPaths.some((d) => existsSync13(join15(projectRoot, d)))) {
|
|
3756
3741
|
detected.add(p.id);
|
|
3757
3742
|
}
|
|
3758
3743
|
}
|
|
@@ -3785,7 +3770,7 @@ function installRules(projectRoot, platformId, options = {}) {
|
|
|
3785
3770
|
const { result, content } = planRules(projectRoot, platformId, { tier: options.tier });
|
|
3786
3771
|
if (!options.dryRun && result.action !== "skipped" && content !== void 0) {
|
|
3787
3772
|
const filePath = join15(projectRoot, result.file);
|
|
3788
|
-
if (result.action === "created") mkdirSync4(
|
|
3773
|
+
if (result.action === "created") mkdirSync4(dirname5(filePath), { recursive: true });
|
|
3789
3774
|
writeFileSync3(filePath, content, "utf-8");
|
|
3790
3775
|
}
|
|
3791
3776
|
return result;
|
|
@@ -3798,9 +3783,9 @@ function planRules(projectRoot, platformId, options = {}) {
|
|
|
3798
3783
|
const filePath = join15(projectRoot, file);
|
|
3799
3784
|
let existing = options.existing;
|
|
3800
3785
|
if (existing === void 0) {
|
|
3801
|
-
if (!
|
|
3786
|
+
if (!existsSync13(filePath)) return { result: { file, action: "created" }, content: generateRules(platformId, tier) };
|
|
3802
3787
|
try {
|
|
3803
|
-
existing =
|
|
3788
|
+
existing = readFileSync8(filePath, "utf-8");
|
|
3804
3789
|
} catch (err) {
|
|
3805
3790
|
return { result: { file, action: "skipped", warning: unreadableWarning(file, err) } };
|
|
3806
3791
|
}
|
|
@@ -3844,7 +3829,7 @@ function skillsSourceDir() {
|
|
|
3844
3829
|
let dir = import.meta.dirname;
|
|
3845
3830
|
for (let i = 0; i < 10; i++) {
|
|
3846
3831
|
const candidate = join15(dir, "skills");
|
|
3847
|
-
if (
|
|
3832
|
+
if (existsSync13(join15(candidate, "ctxo-safe-edit", "SKILL.md"))) return candidate;
|
|
3848
3833
|
const parent = join15(dir, "..");
|
|
3849
3834
|
if (parent === dir) break;
|
|
3850
3835
|
dir = parent;
|
|
@@ -3856,13 +3841,13 @@ function skillPlan(projectRoot, platformId) {
|
|
|
3856
3841
|
if (!target) return [];
|
|
3857
3842
|
const src = skillsSourceDir();
|
|
3858
3843
|
return SKILLS.map((skill) => {
|
|
3859
|
-
const bundled =
|
|
3844
|
+
const bundled = readFileSync8(join15(src, skill, "SKILL.md"), "utf-8");
|
|
3860
3845
|
const to = target(projectRoot, skill);
|
|
3861
3846
|
const file = relative3(projectRoot, to).split(sep2).join("/");
|
|
3862
|
-
if (!
|
|
3847
|
+
if (!existsSync13(to)) return { skill, file, state: "missing", bundled, to };
|
|
3863
3848
|
let installed;
|
|
3864
3849
|
try {
|
|
3865
|
-
installed =
|
|
3850
|
+
installed = readFileSync8(to, "utf-8");
|
|
3866
3851
|
} catch (err) {
|
|
3867
3852
|
return { skill, file, state: "missing", bundled, to, blocked: unreadableWarning(file, err) };
|
|
3868
3853
|
}
|
|
@@ -3878,18 +3863,37 @@ function skillStates(projectRoot, platformId) {
|
|
|
3878
3863
|
return skillPlan(projectRoot, platformId).map(({ skill, file, state }) => ({ skill, file, state }));
|
|
3879
3864
|
}
|
|
3880
3865
|
function installSkills(projectRoot, platformId, options = {}) {
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3866
|
+
if (!skillTarget(platformId)) return [];
|
|
3867
|
+
const src = skillsSourceDir();
|
|
3868
|
+
const bundled = SKILLS.map((skill) => ({ skill, content: readFileSync8(join15(src, skill, "SKILL.md"), "utf-8") }));
|
|
3869
|
+
return bundled.map(({ skill, content }) => installSkillContent(projectRoot, platformId, skill, content, options));
|
|
3870
|
+
}
|
|
3871
|
+
function installSkillContent(projectRoot, platformId, name, content, options = {}) {
|
|
3872
|
+
const target = skillTarget(platformId);
|
|
3873
|
+
if (!target) return void 0;
|
|
3874
|
+
const to = target(projectRoot, name);
|
|
3875
|
+
const file = relative3(projectRoot, to).split(sep2).join("/");
|
|
3876
|
+
let installed;
|
|
3877
|
+
try {
|
|
3878
|
+
installed = readFileSync8(to, "utf-8");
|
|
3879
|
+
} catch (err) {
|
|
3880
|
+
if (err.code !== "ENOENT") {
|
|
3881
|
+
return { file, action: "skipped", warning: unreadableWarning(file, err) };
|
|
3886
3882
|
}
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3883
|
+
}
|
|
3884
|
+
if (installed !== void 0) {
|
|
3885
|
+
const version = skillTemplateVersion(installed);
|
|
3886
|
+
const ours = skillTemplateVersion(content);
|
|
3887
|
+
if (version !== void 0 && ours !== void 0 && compareTemplateVersions(version, ours) > 0) {
|
|
3888
|
+
return { file, action: "skipped", warning: `${file}: skill v${version} comes from a newer ctxo; left unchanged` };
|
|
3890
3889
|
}
|
|
3891
|
-
return { file, action:
|
|
3892
|
-
}
|
|
3890
|
+
if (comparableMarkdown(installed) === comparableMarkdown(content)) return { file, action: "skipped" };
|
|
3891
|
+
}
|
|
3892
|
+
if (!options.dryRun) {
|
|
3893
|
+
mkdirSync4(dirname5(to), { recursive: true });
|
|
3894
|
+
writeFileSync3(to, content, "utf-8");
|
|
3895
|
+
}
|
|
3896
|
+
return { file, action: installed === void 0 ? "created" : "updated" };
|
|
3893
3897
|
}
|
|
3894
3898
|
function skillInstallWarnings(projectRoot, platformId, results) {
|
|
3895
3899
|
const newer = new Set(skillStates(projectRoot, platformId).filter((s) => s.state === "newer").map((s) => s.file));
|
|
@@ -3905,7 +3909,7 @@ function skillInstallWarnings(projectRoot, platformId, results) {
|
|
|
3905
3909
|
var GITIGNORE_ENTRY = ".ctxo/.cache/";
|
|
3906
3910
|
function ensureIndexDir(projectRoot, options = {}) {
|
|
3907
3911
|
const dir = join15(projectRoot, ".ctxo", "index");
|
|
3908
|
-
if (
|
|
3912
|
+
if (existsSync13(dir)) return { file: ".ctxo/index/", action: "skipped" };
|
|
3909
3913
|
if (!options.dryRun) mkdirSync4(dir, { recursive: true });
|
|
3910
3914
|
return { file: ".ctxo/index/", action: "created" };
|
|
3911
3915
|
}
|
|
@@ -3913,7 +3917,7 @@ function ensureGitignore(projectRoot, options = {}) {
|
|
|
3913
3917
|
const filePath = join15(projectRoot, ".gitignore");
|
|
3914
3918
|
let existing;
|
|
3915
3919
|
try {
|
|
3916
|
-
existing =
|
|
3920
|
+
existing = readFileSync8(filePath, "utf-8");
|
|
3917
3921
|
} catch (err) {
|
|
3918
3922
|
if (err.code !== "ENOENT") throw err;
|
|
3919
3923
|
if (!options.dryRun) {
|
|
@@ -3970,9 +3974,9 @@ function ensureConfig(projectRoot, options = {}) {
|
|
|
3970
3974
|
const filePath = join15(projectRoot, ".ctxo", "config.yaml");
|
|
3971
3975
|
const file = ".ctxo/config.yaml";
|
|
3972
3976
|
if (options.dryRun) {
|
|
3973
|
-
return { file, action:
|
|
3977
|
+
return { file, action: existsSync13(filePath) ? "skipped" : "created" };
|
|
3974
3978
|
}
|
|
3975
|
-
mkdirSync4(
|
|
3979
|
+
mkdirSync4(dirname5(filePath), { recursive: true });
|
|
3976
3980
|
try {
|
|
3977
3981
|
writeFileSync3(filePath, DEFAULT_CONFIG, { encoding: "utf-8", flag: "wx" });
|
|
3978
3982
|
} catch (err) {
|
|
@@ -3981,7 +3985,15 @@ function ensureConfig(projectRoot, options = {}) {
|
|
|
3981
3985
|
}
|
|
3982
3986
|
return { file, action: "created" };
|
|
3983
3987
|
}
|
|
3984
|
-
var
|
|
3988
|
+
var NPX_MCP_ENTRY = { command: "npx", args: ["-y", "@ctxo/cli"] };
|
|
3989
|
+
function ctxoMcpEntry(kind) {
|
|
3990
|
+
return kind === "global" ? { command: "ctxo", args: [] } : { command: NPX_MCP_ENTRY.command, args: [...NPX_MCP_ENTRY.args] };
|
|
3991
|
+
}
|
|
3992
|
+
function isNpxDefaultEntry(entry) {
|
|
3993
|
+
if (!isPlainObject(entry) || entry["command"] !== NPX_MCP_ENTRY.command) return false;
|
|
3994
|
+
const args = entry["args"];
|
|
3995
|
+
return Array.isArray(args) && args.length === NPX_MCP_ENTRY.args.length && args.every((a, i) => a === NPX_MCP_ENTRY.args[i]);
|
|
3996
|
+
}
|
|
3985
3997
|
function getMcpConfigTargets(selectedPlatformIds) {
|
|
3986
3998
|
const targets = /* @__PURE__ */ new Map();
|
|
3987
3999
|
for (const id of selectedPlatformIds) {
|
|
@@ -4013,18 +4025,20 @@ function ensureMcpConfig(projectRoot, target, options = {}) {
|
|
|
4013
4025
|
const filePath = join15(projectRoot, target.file);
|
|
4014
4026
|
const write = (config) => {
|
|
4015
4027
|
if (options.dryRun) return;
|
|
4016
|
-
mkdirSync4(
|
|
4028
|
+
mkdirSync4(dirname5(filePath), { recursive: true });
|
|
4017
4029
|
writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4018
4030
|
};
|
|
4019
|
-
const
|
|
4031
|
+
const kind = options.installKind ?? "project";
|
|
4032
|
+
const base = ctxoMcpEntry(kind);
|
|
4033
|
+
const entry = target.extraFields ? { ...target.extraFields, ...base } : { ...base };
|
|
4020
4034
|
const fresh = () => ({ [target.serverKey]: { ctxo: entry } });
|
|
4021
|
-
if (!
|
|
4035
|
+
if (!existsSync13(filePath)) {
|
|
4022
4036
|
write(fresh());
|
|
4023
4037
|
return { file: target.file, action: "created" };
|
|
4024
4038
|
}
|
|
4025
4039
|
let text;
|
|
4026
4040
|
try {
|
|
4027
|
-
text =
|
|
4041
|
+
text = readFileSync8(filePath, "utf-8");
|
|
4028
4042
|
} catch (err) {
|
|
4029
4043
|
return { file: target.file, action: "skipped", warning: unreadableWarning(target.file, err) };
|
|
4030
4044
|
}
|
|
@@ -4040,15 +4054,103 @@ function ensureMcpConfig(projectRoot, target, options = {}) {
|
|
|
4040
4054
|
return { file: target.file, action: "updated" };
|
|
4041
4055
|
}
|
|
4042
4056
|
if (servers["ctxo"]) {
|
|
4057
|
+
if (kind === "global" && isNpxDefaultEntry(servers["ctxo"])) {
|
|
4058
|
+
write({ ...parsed, [target.serverKey]: { ...servers, ctxo: { ...servers["ctxo"], ...entry } } });
|
|
4059
|
+
return { file: target.file, action: "updated" };
|
|
4060
|
+
}
|
|
4043
4061
|
return { file: target.file, action: "skipped" };
|
|
4044
4062
|
}
|
|
4045
4063
|
write({ ...parsed, [target.serverKey]: { ...servers, ctxo: entry } });
|
|
4046
4064
|
return { file: target.file, action: "updated" };
|
|
4047
4065
|
}
|
|
4048
4066
|
|
|
4067
|
+
// src/core/install/install-kind.ts
|
|
4068
|
+
function normalizePath(path, platform) {
|
|
4069
|
+
const slashed = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4070
|
+
return platform === "win32" ? slashed.toLowerCase() : slashed;
|
|
4071
|
+
}
|
|
4072
|
+
var RUNNER_CACHE = /\/(_npx|dlx)\/|\/bunx-[^/]*\/|\/\.bun\/install\/cache\//;
|
|
4073
|
+
function cliInstallKind(cliPackageRoot, projectRoot, platform = process.platform) {
|
|
4074
|
+
const cli = normalizePath(cliPackageRoot, platform);
|
|
4075
|
+
if (RUNNER_CACHE.test(`${cli}/`)) return "npx";
|
|
4076
|
+
const marker = "/node_modules/";
|
|
4077
|
+
const at = cli.indexOf(marker);
|
|
4078
|
+
if (at >= 0) {
|
|
4079
|
+
const owner = cli.slice(0, at);
|
|
4080
|
+
const project = normalizePath(projectRoot, platform);
|
|
4081
|
+
if (project === owner || project.startsWith(`${owner}/`)) return "project";
|
|
4082
|
+
}
|
|
4083
|
+
return "global";
|
|
4084
|
+
}
|
|
4085
|
+
function isRecord(value) {
|
|
4086
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4087
|
+
}
|
|
4088
|
+
function commandName(command) {
|
|
4089
|
+
const base = command.replace(/\\/g, "/").split("/").pop() ?? command;
|
|
4090
|
+
return base.toLowerCase().replace(/\.(cmd|exe|bat)$/, "");
|
|
4091
|
+
}
|
|
4092
|
+
var CTXO_SPEC = /^@ctxo\/cli(@|$)/;
|
|
4093
|
+
function launcherOfCommand(command, args) {
|
|
4094
|
+
const name = commandName(command);
|
|
4095
|
+
if (name === "npx") return args.some((a) => CTXO_SPEC.test(a)) ? "npx" : "other";
|
|
4096
|
+
if (name === "ctxo") return "path-binary";
|
|
4097
|
+
return "other";
|
|
4098
|
+
}
|
|
4099
|
+
function innerCommand(entry) {
|
|
4100
|
+
if (!isRecord(entry) || typeof entry["command"] !== "string") return void 0;
|
|
4101
|
+
const args = Array.isArray(entry["args"]) ? entry["args"].filter((a) => typeof a === "string") : [];
|
|
4102
|
+
if (commandName(entry["command"]) === "cmd") {
|
|
4103
|
+
const run2 = args.findIndex((a) => a.toLowerCase() === "/c");
|
|
4104
|
+
const inner = run2 >= 0 ? args[run2 + 1] : void 0;
|
|
4105
|
+
return inner === void 0 ? { command: "cmd", args: [] } : { command: inner, args: args.slice(run2 + 2) };
|
|
4106
|
+
}
|
|
4107
|
+
return { command: entry["command"], args };
|
|
4108
|
+
}
|
|
4109
|
+
function mcpLauncherOf(entry) {
|
|
4110
|
+
const inner = innerCommand(entry);
|
|
4111
|
+
return inner === void 0 ? void 0 : launcherOfCommand(inner.command, inner.args);
|
|
4112
|
+
}
|
|
4113
|
+
function mcpNpxPin(entry) {
|
|
4114
|
+
const inner = innerCommand(entry);
|
|
4115
|
+
if (inner === void 0 || launcherOfCommand(inner.command, inner.args) !== "npx") return void 0;
|
|
4116
|
+
const spec = inner.args.find((a) => CTXO_SPEC.test(a));
|
|
4117
|
+
const version = spec?.startsWith("@ctxo/cli@") ? spec.slice("@ctxo/cli@".length) : "";
|
|
4118
|
+
return version !== "" && version !== "latest" ? version : void 0;
|
|
4119
|
+
}
|
|
4120
|
+
function assessInstallCopies(facts) {
|
|
4121
|
+
if (facts.hooksCallCtxo && !facts.ctxoOnPath) {
|
|
4122
|
+
return {
|
|
4123
|
+
status: "warn",
|
|
4124
|
+
message: 'hooks call "ctxo" but no ctxo is on PATH, so they never run',
|
|
4125
|
+
fix: 'Install @ctxo/cli globally ("npm i -g @ctxo/cli")'
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
if (facts.hooksCallCtxo && facts.launchers.includes("npx")) {
|
|
4129
|
+
const global = facts.globalCliVersion;
|
|
4130
|
+
const differing = global === void 0 ? void 0 : (facts.npxPins ?? []).find((pin) => pin !== global);
|
|
4131
|
+
if (differing !== void 0) {
|
|
4132
|
+
const start = `MCP starts "npx -y @ctxo/cli@${differing}" while hooks run "ctxo" ${global} from PATH: two copies`;
|
|
4133
|
+
return facts.domainInstalled === true ? {
|
|
4134
|
+
status: "warn",
|
|
4135
|
+
message: `${start}, and @ctxo/domain installed in one is missing from the other`,
|
|
4136
|
+
fix: 'Install once globally ("npm i -g @ctxo/cli @ctxo/domain"), then run "ctxo init --only mcp" from that install'
|
|
4137
|
+
} : {
|
|
4138
|
+
status: "warn",
|
|
4139
|
+
message: `${start}, so a package installed next to one is not seen by the other`,
|
|
4140
|
+
fix: 'Install once globally ("npm i -g @ctxo/cli"), then run "ctxo init --only mcp" from that install'
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
return { status: "pass", message: "one copy: MCP and hooks run ctxo from PATH" };
|
|
4144
|
+
}
|
|
4145
|
+
if (facts.hooksCallCtxo && facts.launchers.includes("path-binary")) {
|
|
4146
|
+
return { status: "pass", message: "one copy: MCP and hooks run ctxo from PATH" };
|
|
4147
|
+
}
|
|
4148
|
+
return { status: "pass", message: "nothing to compare (no ctxo hooks or no MCP entry)" };
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4049
4151
|
// src/cli/git-hooks.ts
|
|
4050
4152
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
4051
|
-
import { join as join16, resolve as
|
|
4153
|
+
import { join as join16, resolve as resolve4 } from "path";
|
|
4052
4154
|
var CTXO_START = "# ctxo-start";
|
|
4053
4155
|
var CTXO_END = "# ctxo-end";
|
|
4054
4156
|
var POST_COMMIT_HOOK_VERSION = 2;
|
|
@@ -4107,7 +4209,7 @@ function resolveHooksDir(projectRoot) {
|
|
|
4107
4209
|
encoding: "utf-8",
|
|
4108
4210
|
stdio: ["ignore", "pipe", "ignore"]
|
|
4109
4211
|
}).trim();
|
|
4110
|
-
return answer ?
|
|
4212
|
+
return answer ? resolve4(projectRoot, answer) : fallback;
|
|
4111
4213
|
} catch {
|
|
4112
4214
|
return fallback;
|
|
4113
4215
|
}
|
|
@@ -4147,7 +4249,8 @@ function sharedHooksWarning(hooksDir, scope) {
|
|
|
4147
4249
|
}
|
|
4148
4250
|
|
|
4149
4251
|
// src/cli/init-parts.ts
|
|
4150
|
-
var INIT_PARTS = ["rules", "skills", "guard", "mcp", "git-hooks", "config"];
|
|
4252
|
+
var INIT_PARTS = ["rules", "skills", "guard", "mcp", "git-hooks", "config", "domain"];
|
|
4253
|
+
var OPT_IN_PARTS = /* @__PURE__ */ new Set(["domain"]);
|
|
4151
4254
|
function isInitPart(value) {
|
|
4152
4255
|
return INIT_PARTS.includes(value);
|
|
4153
4256
|
}
|
|
@@ -4172,7 +4275,9 @@ function resolveInitParts(flags) {
|
|
|
4172
4275
|
return new Set(
|
|
4173
4276
|
INIT_PARTS.filter((part) => {
|
|
4174
4277
|
if (flags.noGuard && part === "guard") return false;
|
|
4175
|
-
|
|
4278
|
+
if (part === "domain" && flags.domain && !removed.has(part)) return true;
|
|
4279
|
+
if (flags.only !== void 0) return flags.only.includes(part);
|
|
4280
|
+
return !OPT_IN_PARTS.has(part) && !removed.has(part);
|
|
4176
4281
|
})
|
|
4177
4282
|
);
|
|
4178
4283
|
}
|
|
@@ -4181,7 +4286,19 @@ function pluginStepEnabled(flags) {
|
|
|
4181
4286
|
}
|
|
4182
4287
|
|
|
4183
4288
|
// src/cli/init-command.ts
|
|
4184
|
-
var requireCjs2 =
|
|
4289
|
+
var requireCjs2 = createRequire3(import.meta.url);
|
|
4290
|
+
function projectDeclaresCtxoCli(root) {
|
|
4291
|
+
try {
|
|
4292
|
+
const pkg = JSON.parse(readFileSync9(join17(root, "package.json"), "utf-8"));
|
|
4293
|
+
if (typeof pkg !== "object" || pkg === null) return false;
|
|
4294
|
+
return ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].some((field) => {
|
|
4295
|
+
const deps = pkg[field];
|
|
4296
|
+
return typeof deps === "object" && deps !== null && "@ctxo/cli" in deps;
|
|
4297
|
+
});
|
|
4298
|
+
} catch {
|
|
4299
|
+
return false;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4185
4302
|
function resolveInstalledPlugins(needed) {
|
|
4186
4303
|
const installed = [];
|
|
4187
4304
|
const missing = [];
|
|
@@ -4250,6 +4367,54 @@ function renderBanner(_version, pc) {
|
|
|
4250
4367
|
var RULES_DEPRECATION_NOTICE = "[ctxo] --rules is deprecated and now means --skip git-hooks,guard. Use --only or --skip instead.";
|
|
4251
4368
|
var NOTHING_TO_INSTALL_NOTICE = "[ctxo] Nothing to install: every part is excluded.";
|
|
4252
4369
|
var SHARED_HOOKS_SKIPPED = "git hooks skipped - shared hooks directory (see warning above)";
|
|
4370
|
+
var DOMAIN_PACKAGE_ROW = "@ctxo/domain (package)";
|
|
4371
|
+
var DOMAIN_MISSING_WARNING = '@ctxo/domain is not installed; run "ctxo install domain", then "ctxo init --only domain"';
|
|
4372
|
+
var BOUNDARY_QUESTION = "Boundary violations between named contexts";
|
|
4373
|
+
var BOUNDARY_CHOICES = [
|
|
4374
|
+
{ value: "by-rung", label: "by-rung: fail verify-domain for bounded and boring contexts, warn for the others" },
|
|
4375
|
+
{ value: "report", label: "report: warnings only" }
|
|
4376
|
+
];
|
|
4377
|
+
var INITIAL_ANSWERS = {
|
|
4378
|
+
confirm: async (_message, initial) => initial,
|
|
4379
|
+
select: async (_message, _options, initial) => initial
|
|
4380
|
+
};
|
|
4381
|
+
function nonInteractiveDomainRun(options, toolIds) {
|
|
4382
|
+
return {
|
|
4383
|
+
interactive: false,
|
|
4384
|
+
yes: options.yes === true,
|
|
4385
|
+
noInstall: options.noInstall === true,
|
|
4386
|
+
...options.boundaries !== void 0 ? { boundaries: options.boundaries } : {},
|
|
4387
|
+
prompter: INITIAL_ANSWERS,
|
|
4388
|
+
toolIds
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
function clackPrompter(clack, pc) {
|
|
4392
|
+
const answered = (value) => {
|
|
4393
|
+
if (clack.isCancel(value)) {
|
|
4394
|
+
clack.cancel("Setup cancelled.");
|
|
4395
|
+
process.exit(0);
|
|
4396
|
+
}
|
|
4397
|
+
return value;
|
|
4398
|
+
};
|
|
4399
|
+
return {
|
|
4400
|
+
async confirm(message, initial) {
|
|
4401
|
+
return answered(await clack.confirm({ message: pc.cyan(message), initialValue: initial })) === true;
|
|
4402
|
+
},
|
|
4403
|
+
async select(message, options, initial) {
|
|
4404
|
+
const choices = options.map((option) => ({ value: option.value, label: option.label }));
|
|
4405
|
+
const picked = await clack.select({ message: pc.cyan(message), options: choices, initialValue: initial });
|
|
4406
|
+
return answered(picked);
|
|
4407
|
+
}
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
function configuredBoundaryMode(projectRoot) {
|
|
4411
|
+
const domain = loadConfig(join17(projectRoot, ".ctxo")).config["domain"];
|
|
4412
|
+
if (typeof domain !== "object" || domain === null) return void 0;
|
|
4413
|
+
const boundaries = domain["boundaries"];
|
|
4414
|
+
if (typeof boundaries !== "object" || boundaries === null) return void 0;
|
|
4415
|
+
const enforce = boundaries["enforce"];
|
|
4416
|
+
return enforce === "by-rung" || enforce === "report" ? enforce : void 0;
|
|
4417
|
+
}
|
|
4253
4418
|
var TOOL_PARTS = ["rules", "skills", "guard", "mcp"];
|
|
4254
4419
|
function selectedTools(options) {
|
|
4255
4420
|
return options.tools ? [...new Set(options.tools)] : PLATFORMS.map((p) => p.id);
|
|
@@ -4295,12 +4460,16 @@ var GIT_HOOKS = [
|
|
|
4295
4460
|
];
|
|
4296
4461
|
function displayPath(projectRoot, path) {
|
|
4297
4462
|
const rel = forwardSlashes(relative4(projectRoot, path));
|
|
4298
|
-
return rel === "" || rel.startsWith("..") ||
|
|
4463
|
+
return rel === "" || rel.startsWith("..") || isAbsolute2(rel) ? forwardSlashes(path) : rel;
|
|
4299
4464
|
}
|
|
4300
4465
|
var InitCommand = class {
|
|
4301
4466
|
projectRoot;
|
|
4302
|
-
|
|
4467
|
+
cliAnchor;
|
|
4468
|
+
/** Set when the domain part could not run (missing package, failed init); `run` then ends with exit code 1. */
|
|
4469
|
+
domainFailed = false;
|
|
4470
|
+
constructor(projectRoot, options = {}) {
|
|
4303
4471
|
this.projectRoot = projectRoot;
|
|
4472
|
+
this.cliAnchor = options.cliAnchor;
|
|
4304
4473
|
}
|
|
4305
4474
|
/**
|
|
4306
4475
|
* Installs (or upgrades in place) the post-commit and post-merge blocks in the hooks directory
|
|
@@ -4324,13 +4493,15 @@ var InitCommand = class {
|
|
|
4324
4493
|
}
|
|
4325
4494
|
/**
|
|
4326
4495
|
* What init would do with these options, one row per file, writing nothing: the same pipeline as a
|
|
4327
|
-
* real run over the parts the flags select. Without `tools` it previews every platform (D16)
|
|
4496
|
+
* real run over the parts the flags select. Without `tools` it previews every platform (D16), the
|
|
4497
|
+
* domain part included (PF11). Without @ctxo/domain the domain part is the row `@ctxo/domain (package)`.
|
|
4328
4498
|
*/
|
|
4329
|
-
plan(options = {}) {
|
|
4330
|
-
|
|
4499
|
+
async plan(options = {}) {
|
|
4500
|
+
const toolIds = selectedTools(options);
|
|
4501
|
+
return this.applyParts(resolveInitParts(options), toolIds, true, declinedBy(options), nonInteractiveDomainRun(options, toolIds));
|
|
4331
4502
|
}
|
|
4332
4503
|
async run(options = {}) {
|
|
4333
|
-
if (!
|
|
4504
|
+
if (!existsSync14(join17(this.projectRoot, ".git"))) {
|
|
4334
4505
|
console.error('[ctxo] Not a git repository. Run "git init" first.');
|
|
4335
4506
|
process.exit(1);
|
|
4336
4507
|
}
|
|
@@ -4340,23 +4511,25 @@ var InitCommand = class {
|
|
|
4340
4511
|
console.error(NOTHING_TO_INSTALL_NOTICE);
|
|
4341
4512
|
return;
|
|
4342
4513
|
}
|
|
4343
|
-
if (options.dryRun)
|
|
4344
|
-
if (options.yes || options.tools)
|
|
4345
|
-
|
|
4514
|
+
if (options.dryRun) await this.dryRun(options);
|
|
4515
|
+
else if (options.yes || options.tools) await this.runNonInteractive(options, parts);
|
|
4516
|
+
else await this.runInteractive(options, parts);
|
|
4517
|
+
if (this.domainFailed) process.exitCode = 1;
|
|
4346
4518
|
}
|
|
4347
4519
|
/* ---------------------------------------------------------------- */
|
|
4348
4520
|
/* The parts pipeline */
|
|
4349
4521
|
/* ---------------------------------------------------------------- */
|
|
4350
4522
|
/**
|
|
4351
|
-
* Installs the selected parts in one fixed order: config, skills, guard, rules, MCP, git hooks.
|
|
4523
|
+
* Installs the selected parts in one fixed order: config, skills, guard, rules, MCP, git hooks, domain.
|
|
4352
4524
|
* Skills and the guard go first so each platform's rules tier is resolved from what is on disk after
|
|
4353
4525
|
* them, and so `rulesHold` can see whether a block that expects them must wait (for a file under
|
|
4354
4526
|
* any of its linked names, `rulesFileHoldReasons`). A part not in the
|
|
4355
4527
|
* set adds no row. Bundled skills that cannot be read become a warning row, and the other parts
|
|
4356
4528
|
* still run. `dryRun` gives the same rows and writes nothing; the rules tier and the hold then count
|
|
4357
4529
|
* the skills and guard it would install as present. `declined` is what the user declined in this run.
|
|
4530
|
+
* `domain` says how the domain part (step 7, `applyDomain`) installs @ctxo/domain and asks its questions.
|
|
4358
4531
|
*/
|
|
4359
|
-
applyParts(parts, toolIds, dryRun, declined) {
|
|
4532
|
+
async applyParts(parts, toolIds, dryRun, declined, domain) {
|
|
4360
4533
|
const root = this.projectRoot;
|
|
4361
4534
|
const write = { dryRun };
|
|
4362
4535
|
const results = [];
|
|
@@ -4408,7 +4581,9 @@ var InitCommand = class {
|
|
|
4408
4581
|
}
|
|
4409
4582
|
}
|
|
4410
4583
|
if (parts.has("mcp")) {
|
|
4411
|
-
|
|
4584
|
+
const detected = cliInstallKind(getCliPackageRoot(), root);
|
|
4585
|
+
const installKind = detected === "global" && projectDeclaresCtxoCli(root) ? "project" : detected;
|
|
4586
|
+
for (const target of getMcpConfigTargets([...toolIds])) results.push(ensureMcpConfig(root, target, { ...write, installKind }));
|
|
4412
4587
|
}
|
|
4413
4588
|
if (parts.has("git-hooks")) {
|
|
4414
4589
|
const hooks = this.installHooks(write);
|
|
@@ -4417,8 +4592,81 @@ var InitCommand = class {
|
|
|
4417
4592
|
results.push({ file: displayPath(root, hooks.hooksDir), action: "skipped", warning: SHARED_HOOKS_SKIPPED });
|
|
4418
4593
|
}
|
|
4419
4594
|
}
|
|
4595
|
+
if (parts.has("domain")) {
|
|
4596
|
+
domain.pause?.();
|
|
4597
|
+
try {
|
|
4598
|
+
results.push(...await this.applyDomain(dryRun, domain));
|
|
4599
|
+
} finally {
|
|
4600
|
+
domain.resume?.();
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4420
4603
|
return results;
|
|
4421
4604
|
}
|
|
4605
|
+
/**
|
|
4606
|
+
* Step 7, the domain part. Without @ctxo/domain's init contribution: a dry run gives the row
|
|
4607
|
+
* `@ctxo/domain (package)`; `--no-install`, a non-interactive run without `--yes` or a declined install
|
|
4608
|
+
* question give the missing-package warning (all but the declined question fail the run); otherwise
|
|
4609
|
+
* @ctxo/domain is installed (`ctxo install domain`) and loaded again. Then the contribution's `apply` runs
|
|
4610
|
+
* with the domain tools, the run's prompter, `ensureConfig` and the two writers of `ai-rules.ts`.
|
|
4611
|
+
*/
|
|
4612
|
+
async applyDomain(dryRun, run2) {
|
|
4613
|
+
const missing = { file: "@ctxo/domain", action: "skipped", warning: DOMAIN_MISSING_WARNING };
|
|
4614
|
+
let init = await this.loadDomainInit(false);
|
|
4615
|
+
if (init === void 0) {
|
|
4616
|
+
if (dryRun) return [{ file: DOMAIN_PACKAGE_ROW, action: "created" }];
|
|
4617
|
+
const declined = run2.interactive && run2.installApproved === false;
|
|
4618
|
+
const allowed = !run2.noInstall && (run2.interactive ? run2.installApproved === true : run2.yes);
|
|
4619
|
+
if (!allowed) {
|
|
4620
|
+
if (!declined) this.domainFailed = true;
|
|
4621
|
+
return [missing];
|
|
4622
|
+
}
|
|
4623
|
+
await new InstallCommand(this.projectRoot).run({ languages: ["domain"], yes: true });
|
|
4624
|
+
init = await this.loadDomainInit(true);
|
|
4625
|
+
if (init === void 0) {
|
|
4626
|
+
this.domainFailed = true;
|
|
4627
|
+
return [missing];
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
const root = this.projectRoot;
|
|
4631
|
+
const write = { dryRun };
|
|
4632
|
+
const ctx = {
|
|
4633
|
+
projectRoot: root,
|
|
4634
|
+
toolIds: run2.toolIds,
|
|
4635
|
+
dryRun,
|
|
4636
|
+
yes: run2.yes,
|
|
4637
|
+
...run2.boundaries !== void 0 ? { boundaries: run2.boundaries } : {},
|
|
4638
|
+
prompt: run2.prompter,
|
|
4639
|
+
ensureConfigFile: () => ensureConfig(root, write),
|
|
4640
|
+
ensureClaudeHookEntry: (entry) => ensureClaudeHookEntry(root, entry, { ...write, rerun: "domain" }),
|
|
4641
|
+
installSkill: (platformId, name, content) => installSkillContent(root, platformId, name, content, write)
|
|
4642
|
+
};
|
|
4643
|
+
try {
|
|
4644
|
+
return [...await init.apply(ctx)];
|
|
4645
|
+
} catch (err) {
|
|
4646
|
+
this.domainFailed = true;
|
|
4647
|
+
return [{ file: "@ctxo/domain", action: "skipped", warning: `@ctxo/domain init failed: ${err.message}` }];
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
/**
|
|
4651
|
+
* @ctxo/domain's init contribution, or undefined when the package is not installed, does not load
|
|
4652
|
+
* (version mismatch, load failure: the loader warns) or has no `init`. `refresh` looks again after an install.
|
|
4653
|
+
*/
|
|
4654
|
+
async loadDomainInit(refresh) {
|
|
4655
|
+
try {
|
|
4656
|
+
const [{ DOMAIN_MODULE_PACKAGE: DOMAIN_MODULE_PACKAGE2, loadCtxoModules: loadCtxoModules2 }, { resolvePackageCandidates: resolvePackageCandidates2 }, { createModuleHost }] = await Promise.all([
|
|
4657
|
+
import("./module-loader-B3LYF3W7.js"),
|
|
4658
|
+
import("./plugin-discovery-XRXI3IJF.js"),
|
|
4659
|
+
import("./module-host-KTGDODES.js")
|
|
4660
|
+
]);
|
|
4661
|
+
if (resolvePackageCandidates2(DOMAIN_MODULE_PACKAGE2, this.projectRoot, this.cliAnchor).length === 0) return void 0;
|
|
4662
|
+
const host = createModuleHost({ projectRoot: this.projectRoot, mode: "cli" });
|
|
4663
|
+
const { modules } = await loadCtxoModules2(host, { refresh, cliAnchor: this.cliAnchor });
|
|
4664
|
+
const init = modules[0]?.registration.init;
|
|
4665
|
+
return init !== void 0 && typeof init.apply === "function" ? init : void 0;
|
|
4666
|
+
} catch {
|
|
4667
|
+
return void 0;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4422
4670
|
/**
|
|
4423
4671
|
* The rows for a rules file that `rulesHold` keeps back: the held row, then the warning a write
|
|
4424
4672
|
* would have given (a damaged block or one from a newer ctxo, read by a dry run that writes nothing).
|
|
@@ -4454,7 +4702,8 @@ var InitCommand = class {
|
|
|
4454
4702
|
const showTools = TOOL_PARTS.some((part) => parts.has(part));
|
|
4455
4703
|
const showPlugins = missingPlugins.length > 0;
|
|
4456
4704
|
const showHooks = parts.has("git-hooks");
|
|
4457
|
-
const
|
|
4705
|
+
const showDomain = parts.has("domain");
|
|
4706
|
+
const totalSteps = [showIndex, showTools, showPlugins, showHooks, showDomain].filter(Boolean).length;
|
|
4458
4707
|
let stepNum = 0;
|
|
4459
4708
|
console.error("");
|
|
4460
4709
|
console.error(renderBanner(version, pc));
|
|
@@ -4548,6 +4797,31 @@ var InitCommand = class {
|
|
|
4548
4797
|
}
|
|
4549
4798
|
console.error("");
|
|
4550
4799
|
}
|
|
4800
|
+
const prompter = clackPrompter(clack, pc);
|
|
4801
|
+
let installApproved;
|
|
4802
|
+
let boundaries = options.boundaries;
|
|
4803
|
+
if (showDomain) {
|
|
4804
|
+
stepNum++;
|
|
4805
|
+
console.error(stepHeader(
|
|
4806
|
+
stepNum,
|
|
4807
|
+
totalSteps,
|
|
4808
|
+
"Domain Knowledge",
|
|
4809
|
+
"Registry, fold guard and the ctxo-domain-fold skill (@ctxo/domain).",
|
|
4810
|
+
pc
|
|
4811
|
+
));
|
|
4812
|
+
console.error("");
|
|
4813
|
+
const installed = await this.loadDomainInit(false) !== void 0;
|
|
4814
|
+
if (!installed && !options.noInstall) {
|
|
4815
|
+
installApproved = await prompter.confirm(
|
|
4816
|
+
`Install @ctxo/domain@${getVersion()}? It adds the registry, contexts-touched and the fold guard.`,
|
|
4817
|
+
true
|
|
4818
|
+
);
|
|
4819
|
+
}
|
|
4820
|
+
if ((installed || installApproved === true) && boundaries === void 0 && configuredBoundaryMode(this.projectRoot) === void 0) {
|
|
4821
|
+
boundaries = await prompter.select(BOUNDARY_QUESTION, BOUNDARY_CHOICES, "by-rung");
|
|
4822
|
+
}
|
|
4823
|
+
console.error("");
|
|
4824
|
+
}
|
|
4551
4825
|
const declined = declinedBy(options);
|
|
4552
4826
|
if (parts.has("guard") && toolIds.includes("claude-code") && !options.noGuard) {
|
|
4553
4827
|
const guard = await clack.confirm({
|
|
@@ -4565,7 +4839,19 @@ var InitCommand = class {
|
|
|
4565
4839
|
}
|
|
4566
4840
|
const s = clack.spinner();
|
|
4567
4841
|
s.start(pc.dim("Configuring..."));
|
|
4568
|
-
const
|
|
4842
|
+
const domainRun = {
|
|
4843
|
+
interactive: true,
|
|
4844
|
+
yes: false,
|
|
4845
|
+
noInstall: options.noInstall === true,
|
|
4846
|
+
...boundaries !== void 0 ? { boundaries } : {},
|
|
4847
|
+
...installApproved !== void 0 ? { installApproved } : {},
|
|
4848
|
+
prompter,
|
|
4849
|
+
toolIds: showTools ? toolIds : [...detectPlatforms(this.projectRoot)],
|
|
4850
|
+
// The domain step may install a package and ask the module's own questions: never under a live spinner.
|
|
4851
|
+
pause: () => s.stop(pc.dim("Setting up the domain layer...")),
|
|
4852
|
+
resume: () => s.start(pc.dim("Finalizing..."))
|
|
4853
|
+
};
|
|
4854
|
+
const rows = (await this.applyParts(parts, toolIds, false, declined, domainRun)).map((result) => summaryRow(result, pc));
|
|
4569
4855
|
if (pluginsToInstall.length > 0) {
|
|
4570
4856
|
s.stop(pc.dim("Installing language plugins..."));
|
|
4571
4857
|
const installer = new InstallCommand(this.projectRoot);
|
|
@@ -4578,7 +4864,7 @@ var InitCommand = class {
|
|
|
4578
4864
|
console.error("");
|
|
4579
4865
|
console.error(box(rows, { title: pc.green("\u2713 Summary"), border: pc.green }, pc));
|
|
4580
4866
|
}
|
|
4581
|
-
const hasWork = toolIds.length > 0 || parts.has("git-hooks");
|
|
4867
|
+
const hasWork = toolIds.length > 0 || parts.has("git-hooks") || parts.has("domain");
|
|
4582
4868
|
if (hasWork) {
|
|
4583
4869
|
const steps = [
|
|
4584
4870
|
`${pc.cyan("\u25B6")} ctxo index ${pc.dim("\u2500 build codebase index")}`,
|
|
@@ -4597,7 +4883,8 @@ var InitCommand = class {
|
|
|
4597
4883
|
async runNonInteractive(options, parts) {
|
|
4598
4884
|
const toolIds = selectedTools(options);
|
|
4599
4885
|
exitOnUnknownTools(toolIds);
|
|
4600
|
-
|
|
4886
|
+
const rows = await this.applyParts(parts, toolIds, false, declinedBy(options), nonInteractiveDomainRun(options, toolIds));
|
|
4887
|
+
for (const result of rows) console.error(formatResultLine(result));
|
|
4601
4888
|
if (pluginStepEnabled(options)) {
|
|
4602
4889
|
const missing = this.missingPlugins();
|
|
4603
4890
|
if (missing.length > 0) {
|
|
@@ -4620,10 +4907,11 @@ var InitCommand = class {
|
|
|
4620
4907
|
* hooks directory prints installHooks' warning and no hook rows, as a real run does. The hook rows
|
|
4621
4908
|
* come marked from the pipeline, so the hooks directory is looked up only when git-hooks is selected.
|
|
4622
4909
|
*/
|
|
4623
|
-
dryRun(options) {
|
|
4910
|
+
async dryRun(options) {
|
|
4624
4911
|
exitOnUnknownTools(selectedTools(options));
|
|
4625
4912
|
console.error(DRY_RUN_HEADER);
|
|
4626
|
-
|
|
4913
|
+
const rows = await this.plan(options);
|
|
4914
|
+
for (const result of rows) console.error(formatPlanLine(result));
|
|
4627
4915
|
}
|
|
4628
4916
|
/* ---------------------------------------------------------------- */
|
|
4629
4917
|
/* Helpers */
|
|
@@ -4631,11 +4919,11 @@ var InitCommand = class {
|
|
|
4631
4919
|
async getVersion() {
|
|
4632
4920
|
try {
|
|
4633
4921
|
const pkgPath = join17(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
|
|
4634
|
-
if (
|
|
4635
|
-
return JSON.parse(
|
|
4922
|
+
if (existsSync14(pkgPath)) {
|
|
4923
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
|
|
4636
4924
|
}
|
|
4637
|
-
const { createRequire:
|
|
4638
|
-
const require5 =
|
|
4925
|
+
const { createRequire: createRequire9 } = await import("module");
|
|
4926
|
+
const require5 = createRequire9(import.meta.url);
|
|
4639
4927
|
return require5("../../package.json").version;
|
|
4640
4928
|
} catch {
|
|
4641
4929
|
return void 0;
|
|
@@ -4656,7 +4944,7 @@ var InitCommand = class {
|
|
|
4656
4944
|
let existing = "#!/bin/sh\n";
|
|
4657
4945
|
let hookExists = true;
|
|
4658
4946
|
try {
|
|
4659
|
-
existing =
|
|
4947
|
+
existing = readFileSync9(hookPath, "utf-8");
|
|
4660
4948
|
} catch (err) {
|
|
4661
4949
|
if (err.code !== "ENOENT") throw err;
|
|
4662
4950
|
hookExists = false;
|
|
@@ -4948,7 +5236,7 @@ function readHeadSha() {
|
|
|
4948
5236
|
|
|
4949
5237
|
// src/cli/stats-command.ts
|
|
4950
5238
|
import { join as join19 } from "path";
|
|
4951
|
-
import { existsSync as
|
|
5239
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
|
|
4952
5240
|
import initSqlJs from "sql.js";
|
|
4953
5241
|
var StatsCommand = class {
|
|
4954
5242
|
ctxoRoot;
|
|
@@ -4961,7 +5249,7 @@ var StatsCommand = class {
|
|
|
4961
5249
|
return;
|
|
4962
5250
|
}
|
|
4963
5251
|
const dbPath = join19(this.ctxoRoot, ".cache", "symbols.db");
|
|
4964
|
-
if (!
|
|
5252
|
+
if (!existsSync15(dbPath)) {
|
|
4965
5253
|
if (options.json) {
|
|
4966
5254
|
process.stdout.write(JSON.stringify(this.emptyReport(options.days)) + "\n");
|
|
4967
5255
|
} else {
|
|
@@ -4970,7 +5258,7 @@ var StatsCommand = class {
|
|
|
4970
5258
|
return;
|
|
4971
5259
|
}
|
|
4972
5260
|
const SQL = await initSqlJs();
|
|
4973
|
-
const buffer =
|
|
5261
|
+
const buffer = readFileSync10(dbPath);
|
|
4974
5262
|
const db = new SQL.Database(buffer);
|
|
4975
5263
|
const recorder = new SessionRecorderAdapter(db);
|
|
4976
5264
|
try {
|
|
@@ -5009,9 +5297,9 @@ var StatsCommand = class {
|
|
|
5009
5297
|
}
|
|
5010
5298
|
isStatsEnabled() {
|
|
5011
5299
|
const configPath = join19(this.ctxoRoot, "config.yaml");
|
|
5012
|
-
if (!
|
|
5300
|
+
if (!existsSync15(configPath)) return true;
|
|
5013
5301
|
try {
|
|
5014
|
-
const raw =
|
|
5302
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
5015
5303
|
const match = raw.match(/stats:\s*\n\s*enabled:\s*(true|false)/);
|
|
5016
5304
|
if (match) return match[1] !== "false";
|
|
5017
5305
|
return true;
|
|
@@ -5100,11 +5388,11 @@ var StatsCommand = class {
|
|
|
5100
5388
|
};
|
|
5101
5389
|
|
|
5102
5390
|
// src/cli/doctor-command.ts
|
|
5103
|
-
import { join as
|
|
5391
|
+
import { join as join29 } from "path";
|
|
5104
5392
|
import { existsSync as existsSync25 } from "fs";
|
|
5105
5393
|
|
|
5106
5394
|
// src/adapters/diagnostics/health-checker.ts
|
|
5107
|
-
var
|
|
5395
|
+
var log3 = createLogger("ctxo:doctor");
|
|
5108
5396
|
var HealthChecker = class {
|
|
5109
5397
|
checks;
|
|
5110
5398
|
quiet;
|
|
@@ -5121,12 +5409,12 @@ var HealthChecker = class {
|
|
|
5121
5409
|
if (outcome.status === "fulfilled") {
|
|
5122
5410
|
const r = outcome.value;
|
|
5123
5411
|
if (!this.quiet) {
|
|
5124
|
-
|
|
5412
|
+
log3.info(`${r.id}: ${r.status.toUpperCase()} (${r.message})`);
|
|
5125
5413
|
}
|
|
5126
5414
|
return r;
|
|
5127
5415
|
}
|
|
5128
5416
|
const message = outcome.reason?.message ?? "Unknown error";
|
|
5129
|
-
if (!this.quiet)
|
|
5417
|
+
if (!this.quiet) log3.error(`${check.id}: FAIL (${message})`);
|
|
5130
5418
|
return {
|
|
5131
5419
|
id: check.id,
|
|
5132
5420
|
title: check.title,
|
|
@@ -5193,8 +5481,8 @@ var DoctorReporter = class {
|
|
|
5193
5481
|
};
|
|
5194
5482
|
|
|
5195
5483
|
// src/adapters/diagnostics/checks/runtime-check.ts
|
|
5196
|
-
import { createRequire as
|
|
5197
|
-
var require2 =
|
|
5484
|
+
import { createRequire as createRequire4 } from "module";
|
|
5485
|
+
var require2 = createRequire4(import.meta.url);
|
|
5198
5486
|
function pass(id, title, message) {
|
|
5199
5487
|
return { id, title, status: "pass", message };
|
|
5200
5488
|
}
|
|
@@ -5265,21 +5553,22 @@ function tryResolve(packageName) {
|
|
|
5265
5553
|
}
|
|
5266
5554
|
|
|
5267
5555
|
// src/cli/version-command.ts
|
|
5268
|
-
import { SUPPORTED_API_VERSIONS
|
|
5269
|
-
var NEWEST_API_VERSION =
|
|
5270
|
-
function gatherVersionInfo(discovered) {
|
|
5556
|
+
import { SUPPORTED_API_VERSIONS } from "@ctxo/plugin-api";
|
|
5557
|
+
var NEWEST_API_VERSION = SUPPORTED_API_VERSIONS[SUPPORTED_API_VERSIONS.length - 1];
|
|
5558
|
+
function gatherVersionInfo(discovered, modules = []) {
|
|
5271
5559
|
const plugins = discovered.map(({ plugin, specifier }) => ({
|
|
5272
5560
|
name: specifier,
|
|
5273
5561
|
id: plugin.id,
|
|
5274
5562
|
version: plugin.version,
|
|
5275
5563
|
apiVersion: plugin.apiVersion,
|
|
5276
|
-
compatible:
|
|
5564
|
+
compatible: SUPPORTED_API_VERSIONS.includes(plugin.apiVersion)
|
|
5277
5565
|
}));
|
|
5278
5566
|
return {
|
|
5279
5567
|
ctxo: getVersion(),
|
|
5280
5568
|
pluginApiVersion: NEWEST_API_VERSION,
|
|
5281
|
-
pluginApiVersions:
|
|
5569
|
+
pluginApiVersions: SUPPORTED_API_VERSIONS,
|
|
5282
5570
|
plugins,
|
|
5571
|
+
modules,
|
|
5283
5572
|
runtime: {
|
|
5284
5573
|
node: process.version,
|
|
5285
5574
|
platform: process.platform,
|
|
@@ -5296,6 +5585,11 @@ function formatVerbose(info) {
|
|
|
5296
5585
|
lines.push(`Plugin API: ${info.pluginApiVersions.join(", ")}`);
|
|
5297
5586
|
lines.push(`Runtime: Node ${info.runtime.node} on ${info.runtime.platform}/${info.runtime.arch}`);
|
|
5298
5587
|
lines.push("");
|
|
5588
|
+
for (const m of info.modules.filter((x) => x.status !== "not-installed")) {
|
|
5589
|
+
const label = m.version === void 0 ? m.name : `${m.name} ${m.version}`;
|
|
5590
|
+
lines.push(`Modules: ${label} (${m.status})`);
|
|
5591
|
+
}
|
|
5592
|
+
if (info.modules.some((x) => x.status !== "not-installed")) lines.push("");
|
|
5299
5593
|
if (info.plugins.length === 0) {
|
|
5300
5594
|
lines.push("Plugins: (none installed)");
|
|
5301
5595
|
lines.push("");
|
|
@@ -5320,6 +5614,18 @@ function formatVerbose(info) {
|
|
|
5320
5614
|
function formatJson(info) {
|
|
5321
5615
|
return JSON.stringify(info, null, 2);
|
|
5322
5616
|
}
|
|
5617
|
+
async function collectModuleReports(projectRoot, options = {}) {
|
|
5618
|
+
try {
|
|
5619
|
+
if (resolvePackageCandidates(DOMAIN_MODULE_PACKAGE, projectRoot, options.cliAnchor).length === 0) {
|
|
5620
|
+
return [{ name: DOMAIN_MODULE_PACKAGE, status: "not-installed" }];
|
|
5621
|
+
}
|
|
5622
|
+
const { createModuleHost } = await import("./module-host-KTGDODES.js");
|
|
5623
|
+
const { reports } = await loadCtxoModules(createModuleHost({ projectRoot, mode: "cli" }), { cliAnchor: options.cliAnchor });
|
|
5624
|
+
return reports;
|
|
5625
|
+
} catch (err) {
|
|
5626
|
+
return [{ name: DOMAIN_MODULE_PACKAGE, status: "load-failed", reason: err.message }];
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5323
5629
|
var VersionCommand = class {
|
|
5324
5630
|
constructor(projectRoot) {
|
|
5325
5631
|
this.projectRoot = projectRoot;
|
|
@@ -5338,12 +5644,13 @@ var VersionCommand = class {
|
|
|
5338
5644
|
process.stdout.write(formatShort(info) + "\n");
|
|
5339
5645
|
}
|
|
5340
5646
|
async collect() {
|
|
5647
|
+
const modules = await collectModuleReports(this.projectRoot);
|
|
5341
5648
|
const manifestPath = loadManifestPath(this.projectRoot);
|
|
5342
5649
|
if (!manifestPath) {
|
|
5343
|
-
return gatherVersionInfo([]);
|
|
5650
|
+
return gatherVersionInfo([], modules);
|
|
5344
5651
|
}
|
|
5345
5652
|
const { plugins } = await discoverPlugins({ manifestPath });
|
|
5346
|
-
return gatherVersionInfo(plugins);
|
|
5653
|
+
return gatherVersionInfo(plugins, modules);
|
|
5347
5654
|
}
|
|
5348
5655
|
};
|
|
5349
5656
|
|
|
@@ -5375,10 +5682,11 @@ var VersionsCheck = class {
|
|
|
5375
5682
|
}
|
|
5376
5683
|
};
|
|
5377
5684
|
async function collectInfo(ctx) {
|
|
5685
|
+
const modules = await collectModuleReports(ctx.projectRoot);
|
|
5378
5686
|
const manifestPath = loadManifestPath(ctx.projectRoot);
|
|
5379
|
-
if (!manifestPath) return gatherVersionInfo([]);
|
|
5687
|
+
if (!manifestPath) return gatherVersionInfo([], modules);
|
|
5380
5688
|
const { plugins } = await discoverPlugins({ manifestPath });
|
|
5381
|
-
return gatherVersionInfo(plugins);
|
|
5689
|
+
return gatherVersionInfo(plugins, modules);
|
|
5382
5690
|
}
|
|
5383
5691
|
function buildSummary(info) {
|
|
5384
5692
|
const pluginCount = info.plugins.length;
|
|
@@ -5387,8 +5695,8 @@ function buildSummary(info) {
|
|
|
5387
5695
|
}
|
|
5388
5696
|
|
|
5389
5697
|
// src/adapters/diagnostics/checks/language-coverage-check.ts
|
|
5390
|
-
import { createRequire as
|
|
5391
|
-
var require3 =
|
|
5698
|
+
import { createRequire as createRequire5 } from "module";
|
|
5699
|
+
var require3 = createRequire5(import.meta.url);
|
|
5392
5700
|
var LanguageCoverageCheck = class {
|
|
5393
5701
|
id = "language_coverage";
|
|
5394
5702
|
title = "Language coverage";
|
|
@@ -5439,10 +5747,10 @@ function pluginResolves(packageName) {
|
|
|
5439
5747
|
}
|
|
5440
5748
|
|
|
5441
5749
|
// src/adapters/diagnostics/checks/java-tier-check.ts
|
|
5442
|
-
import { createRequire as
|
|
5443
|
-
import { existsSync as
|
|
5444
|
-
import { join as join20, dirname as
|
|
5445
|
-
var require4 =
|
|
5750
|
+
import { createRequire as createRequire6 } from "module";
|
|
5751
|
+
import { existsSync as existsSync16, readdirSync as readdirSync3 } from "fs";
|
|
5752
|
+
import { join as join20, dirname as dirname6 } from "path";
|
|
5753
|
+
var require4 = createRequire6(import.meta.url);
|
|
5446
5754
|
function evaluateJavaTier(i) {
|
|
5447
5755
|
const id = "java_tier";
|
|
5448
5756
|
const title = "Java analysis tier";
|
|
@@ -5482,8 +5790,8 @@ function evaluateJavaTier(i) {
|
|
|
5482
5790
|
function analyzerJarPresent() {
|
|
5483
5791
|
try {
|
|
5484
5792
|
const pkgJson = require4.resolve("@ctxo/lang-java-analyzer/package.json");
|
|
5485
|
-
const jarDir = join20(
|
|
5486
|
-
return
|
|
5793
|
+
const jarDir = join20(dirname6(pkgJson), "jar");
|
|
5794
|
+
return existsSync16(join20(jarDir, "ctxo-jdt-analyzer-11.jar")) || existsSync16(join20(jarDir, "ctxo-jdt-analyzer-17.jar"));
|
|
5487
5795
|
} catch {
|
|
5488
5796
|
return false;
|
|
5489
5797
|
}
|
|
@@ -5528,7 +5836,7 @@ function findSourceFile(root, extension, maxDepth) {
|
|
|
5528
5836
|
}
|
|
5529
5837
|
function projectHasJava(root) {
|
|
5530
5838
|
for (const f of ["pom.xml", "build.gradle", "build.gradle.kts"]) {
|
|
5531
|
-
if (
|
|
5839
|
+
if (existsSync16(join20(root, f))) return true;
|
|
5532
5840
|
}
|
|
5533
5841
|
return findJavaFile(root, 4);
|
|
5534
5842
|
}
|
|
@@ -5545,10 +5853,10 @@ var JavaTierCheck = class {
|
|
|
5545
5853
|
};
|
|
5546
5854
|
|
|
5547
5855
|
// src/adapters/diagnostics/checks/csharp-tier-check.ts
|
|
5548
|
-
import { createRequire as
|
|
5856
|
+
import { createRequire as createRequire7 } from "module";
|
|
5549
5857
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
5550
|
-
import { existsSync as
|
|
5551
|
-
import { join as join21, dirname as
|
|
5858
|
+
import { existsSync as existsSync17, readdirSync as readdirSync4 } from "fs";
|
|
5859
|
+
import { join as join21, dirname as dirname7 } from "path";
|
|
5552
5860
|
|
|
5553
5861
|
// src/core/detection/detect-dotnet-sdk.ts
|
|
5554
5862
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -5635,10 +5943,10 @@ function restoreFailureMessage(r) {
|
|
|
5635
5943
|
}
|
|
5636
5944
|
function resolveHelperProject(projectRoot) {
|
|
5637
5945
|
try {
|
|
5638
|
-
const requireFromProject =
|
|
5946
|
+
const requireFromProject = createRequire7(join21(projectRoot, "package.json"));
|
|
5639
5947
|
const pkgJson = resolvePluginPackageJson("@ctxo/lang-csharp", requireFromProject);
|
|
5640
|
-
const csproj = join21(
|
|
5641
|
-
return
|
|
5948
|
+
const csproj = join21(dirname7(pkgJson), "tools", "ctxo-roslyn", "ctxo-roslyn.csproj");
|
|
5949
|
+
return existsSync17(csproj) ? csproj : null;
|
|
5642
5950
|
} catch {
|
|
5643
5951
|
return null;
|
|
5644
5952
|
}
|
|
@@ -5686,7 +5994,7 @@ var CSharpTierCheck = class {
|
|
|
5686
5994
|
|
|
5687
5995
|
// src/adapters/diagnostics/checks/git-check.ts
|
|
5688
5996
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
5689
|
-
import { existsSync as
|
|
5997
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
|
|
5690
5998
|
import { join as join22 } from "path";
|
|
5691
5999
|
var GitBinaryCheck = class {
|
|
5692
6000
|
id = "git_binary";
|
|
@@ -5704,7 +6012,7 @@ var GitRepoCheck = class {
|
|
|
5704
6012
|
id = "git_repo";
|
|
5705
6013
|
title = "Git repository";
|
|
5706
6014
|
async run(ctx) {
|
|
5707
|
-
if (
|
|
6015
|
+
if (existsSync18(join22(ctx.projectRoot, ".git"))) {
|
|
5708
6016
|
return { id: this.id, title: this.title, status: "pass", message: ctx.projectRoot, value: ctx.projectRoot };
|
|
5709
6017
|
}
|
|
5710
6018
|
return { id: this.id, title: this.title, status: "fail", message: "Not a git repository", fix: 'Run "git init" or clone a repository' };
|
|
@@ -5714,14 +6022,14 @@ var GitHooksCheck = class {
|
|
|
5714
6022
|
id = "git_hooks";
|
|
5715
6023
|
title = "Git hooks";
|
|
5716
6024
|
async run(ctx) {
|
|
5717
|
-
if (!
|
|
6025
|
+
if (!existsSync18(join22(ctx.projectRoot, ".git"))) {
|
|
5718
6026
|
return { id: this.id, title: this.title, status: "pass", message: "Not a git repository (hooks not applicable)" };
|
|
5719
6027
|
}
|
|
5720
6028
|
const hooksDir = resolveHooksDir(ctx.projectRoot);
|
|
5721
6029
|
const hookPath = join22(hooksDir, "post-commit");
|
|
5722
6030
|
let versions;
|
|
5723
6031
|
try {
|
|
5724
|
-
versions = readCtxoBlockVersions(
|
|
6032
|
+
versions = readCtxoBlockVersions(readFileSync11(hookPath, "utf-8"));
|
|
5725
6033
|
} catch {
|
|
5726
6034
|
versions = [];
|
|
5727
6035
|
}
|
|
@@ -5924,10 +6232,119 @@ var ClaudeGuardHookCheck = class {
|
|
|
5924
6232
|
}
|
|
5925
6233
|
};
|
|
5926
6234
|
|
|
6235
|
+
// src/adapters/diagnostics/checks/install-copies-check.ts
|
|
6236
|
+
import { existsSync as existsSync19, readFileSync as readFileSync12, realpathSync as realpathSync2 } from "fs";
|
|
6237
|
+
import { delimiter, join as join23 } from "path";
|
|
6238
|
+
var MCP_FILES = [
|
|
6239
|
+
{ file: ".mcp.json", keys: ["mcpServers"] },
|
|
6240
|
+
{ file: ".vscode/mcp.json", keys: ["servers"] },
|
|
6241
|
+
{ file: ".gemini/settings.json", keys: ["mcpServers"] },
|
|
6242
|
+
{ file: ".amazonq/mcp.json", keys: ["mcpServers"] },
|
|
6243
|
+
{ file: ".continue/mcpServers/ctxo.json", keys: ["mcpServers"] }
|
|
6244
|
+
];
|
|
6245
|
+
var SETTINGS_FILES = [".claude/settings.json", ".claude/settings.local.json"];
|
|
6246
|
+
function isRecord2(value) {
|
|
6247
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6248
|
+
}
|
|
6249
|
+
function readJson(path) {
|
|
6250
|
+
try {
|
|
6251
|
+
return JSON.parse(readFileSync12(path, "utf-8"));
|
|
6252
|
+
} catch {
|
|
6253
|
+
return void 0;
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
function mcpLaunchers(projectRoot) {
|
|
6257
|
+
const out = [];
|
|
6258
|
+
const npxPins = [];
|
|
6259
|
+
for (const { file, keys } of MCP_FILES) {
|
|
6260
|
+
const json = readJson(join23(projectRoot, file));
|
|
6261
|
+
if (!isRecord2(json)) continue;
|
|
6262
|
+
for (const key of keys) {
|
|
6263
|
+
const servers = json[key];
|
|
6264
|
+
const launcher = isRecord2(servers) ? mcpLauncherOf(servers["ctxo"]) : void 0;
|
|
6265
|
+
if (launcher !== void 0) out.push(launcher);
|
|
6266
|
+
const pin = isRecord2(servers) ? mcpNpxPin(servers["ctxo"]) : void 0;
|
|
6267
|
+
if (pin !== void 0) npxPins.push(pin);
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
return { launchers: out, npxPins };
|
|
6271
|
+
}
|
|
6272
|
+
function hookCallsCtxo(node, depth = 0) {
|
|
6273
|
+
if (depth > 8) return false;
|
|
6274
|
+
if (Array.isArray(node)) return node.some((child) => hookCallsCtxo(child, depth + 1));
|
|
6275
|
+
if (!isRecord2(node)) return false;
|
|
6276
|
+
const command = node["command"];
|
|
6277
|
+
if (typeof command === "string" && command.trimStart().startsWith("ctxo ")) return true;
|
|
6278
|
+
return Object.values(node).some((child) => hookCallsCtxo(child, depth + 1));
|
|
6279
|
+
}
|
|
6280
|
+
function anyHookCallsCtxo(projectRoot) {
|
|
6281
|
+
return SETTINGS_FILES.some((file) => {
|
|
6282
|
+
const json = readJson(join23(projectRoot, file));
|
|
6283
|
+
return isRecord2(json) && hookCallsCtxo(json["hooks"]);
|
|
6284
|
+
});
|
|
6285
|
+
}
|
|
6286
|
+
function binaryOnPath(dirs) {
|
|
6287
|
+
const names = process.platform === "win32" ? ["ctxo.cmd", "ctxo.exe"] : ["ctxo"];
|
|
6288
|
+
return dirs.some((dir) => dir !== "" && names.some((name) => existsSync19(join23(dir, name))));
|
|
6289
|
+
}
|
|
6290
|
+
function globalCliVersion(dirs) {
|
|
6291
|
+
const names = process.platform === "win32" ? ["ctxo.cmd", "ctxo.exe"] : ["ctxo"];
|
|
6292
|
+
for (const dir of dirs) {
|
|
6293
|
+
const found = dir === "" ? void 0 : names.find((name) => existsSync19(join23(dir, name)));
|
|
6294
|
+
if (found === void 0) continue;
|
|
6295
|
+
const candidates = [
|
|
6296
|
+
join23(dir, "node_modules", "@ctxo", "cli", "package.json"),
|
|
6297
|
+
// Windows npm prefix, a global bin dir with its own node_modules
|
|
6298
|
+
join23(dir, "..", "lib", "node_modules", "@ctxo", "cli", "package.json")
|
|
6299
|
+
// unix npm prefix/bin
|
|
6300
|
+
];
|
|
6301
|
+
try {
|
|
6302
|
+
candidates.push(join23(realpathSync2(join23(dir, found)), "..", "..", "package.json"));
|
|
6303
|
+
} catch {
|
|
6304
|
+
}
|
|
6305
|
+
for (const file of candidates) {
|
|
6306
|
+
const json = readJson(file);
|
|
6307
|
+
if (isRecord2(json) && json["name"] === "@ctxo/cli" && typeof json["version"] === "string") return json["version"];
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
return void 0;
|
|
6311
|
+
}
|
|
6312
|
+
var InstallCopiesCheck = class {
|
|
6313
|
+
constructor(options = {}) {
|
|
6314
|
+
this.options = options;
|
|
6315
|
+
}
|
|
6316
|
+
options;
|
|
6317
|
+
id = "install_copies";
|
|
6318
|
+
title = "Install copies";
|
|
6319
|
+
async run(ctx) {
|
|
6320
|
+
try {
|
|
6321
|
+
const pathDirs = this.options.pathDirs ?? (process.env["PATH"] ?? "").split(delimiter);
|
|
6322
|
+
const { launchers, npxPins } = mcpLaunchers(ctx.projectRoot);
|
|
6323
|
+
const globalVersion = globalCliVersion(pathDirs);
|
|
6324
|
+
const verdict = assessInstallCopies({
|
|
6325
|
+
launchers,
|
|
6326
|
+
npxPins,
|
|
6327
|
+
...globalVersion !== void 0 ? { globalCliVersion: globalVersion } : {},
|
|
6328
|
+
hooksCallCtxo: anyHookCallsCtxo(ctx.projectRoot),
|
|
6329
|
+
ctxoOnPath: binaryOnPath(pathDirs),
|
|
6330
|
+
domainInstalled: resolvePackageCandidates(DOMAIN_MODULE_PACKAGE, ctx.projectRoot).length > 0
|
|
6331
|
+
});
|
|
6332
|
+
return { id: this.id, title: this.title, ...verdict };
|
|
6333
|
+
} catch (err) {
|
|
6334
|
+
return {
|
|
6335
|
+
id: this.id,
|
|
6336
|
+
title: this.title,
|
|
6337
|
+
status: "pass",
|
|
6338
|
+
message: `could not be checked: ${err instanceof Error ? err.message : String(err)}`
|
|
6339
|
+
};
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
6342
|
+
};
|
|
6343
|
+
|
|
5927
6344
|
// src/adapters/diagnostics/checks/index-check.ts
|
|
5928
6345
|
import { existsSync as existsSync20 } from "fs";
|
|
5929
6346
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5930
|
-
import { join as
|
|
6347
|
+
import { join as join24 } from "path";
|
|
5931
6348
|
function pass2(id, title, message, value) {
|
|
5932
6349
|
return { id, title, status: "pass", message, value };
|
|
5933
6350
|
}
|
|
@@ -5944,7 +6361,7 @@ var IndexDirectoryCheck = class {
|
|
|
5944
6361
|
id = "index_directory";
|
|
5945
6362
|
title = "Index directory";
|
|
5946
6363
|
async run(ctx) {
|
|
5947
|
-
const indexDir =
|
|
6364
|
+
const indexDir = join24(ctx.ctxoRoot, "index");
|
|
5948
6365
|
if (!existsSync20(indexDir)) {
|
|
5949
6366
|
return fail(this.id, this.title, "No index directory", 'Run "ctxo index"');
|
|
5950
6367
|
}
|
|
@@ -6039,7 +6456,7 @@ var CoChangesCacheCheck = class {
|
|
|
6039
6456
|
id = "co_changes_cache";
|
|
6040
6457
|
title = "Co-changes cache";
|
|
6041
6458
|
async run(ctx) {
|
|
6042
|
-
const coChangesPath =
|
|
6459
|
+
const coChangesPath = join24(ctx.ctxoRoot, "index", "co-changes.json");
|
|
6043
6460
|
if (existsSync20(coChangesPath)) {
|
|
6044
6461
|
return pass2(this.id, this.title, "present");
|
|
6045
6462
|
}
|
|
@@ -6065,12 +6482,12 @@ var SchemaVersionCheck = class {
|
|
|
6065
6482
|
|
|
6066
6483
|
// src/adapters/diagnostics/checks/storage-check.ts
|
|
6067
6484
|
import { existsSync as existsSync21, readFileSync as readFileSync13 } from "fs";
|
|
6068
|
-
import { join as
|
|
6485
|
+
import { join as join25 } from "path";
|
|
6069
6486
|
var SqliteCacheCheck = class {
|
|
6070
6487
|
id = "sqlite_cache";
|
|
6071
6488
|
title = "SQLite cache";
|
|
6072
6489
|
async run(ctx) {
|
|
6073
|
-
const dbPath =
|
|
6490
|
+
const dbPath = join25(ctx.ctxoRoot, ".cache", "symbols.db");
|
|
6074
6491
|
if (!existsSync21(dbPath)) {
|
|
6075
6492
|
return { id: this.id, title: this.title, status: "warn", message: "SQLite cache missing", fix: 'Run "ctxo sync" to rebuild' };
|
|
6076
6493
|
}
|
|
@@ -6094,12 +6511,12 @@ var SqliteCacheCheck = class {
|
|
|
6094
6511
|
|
|
6095
6512
|
// src/adapters/diagnostics/checks/config-check.ts
|
|
6096
6513
|
import { existsSync as existsSync22, readFileSync as readFileSync14 } from "fs";
|
|
6097
|
-
import { join as
|
|
6514
|
+
import { join as join26 } from "path";
|
|
6098
6515
|
var ConfigFileCheck = class {
|
|
6099
6516
|
id = "config_file";
|
|
6100
6517
|
title = "Config file";
|
|
6101
6518
|
async run(ctx) {
|
|
6102
|
-
const configPath =
|
|
6519
|
+
const configPath = join26(ctx.ctxoRoot, "config.yaml");
|
|
6103
6520
|
if (!existsSync22(configPath)) {
|
|
6104
6521
|
return { id: this.id, title: this.title, status: "warn", message: "No config.yaml (using defaults)" };
|
|
6105
6522
|
}
|
|
@@ -6139,7 +6556,7 @@ var ConfigFileCheck = class {
|
|
|
6139
6556
|
|
|
6140
6557
|
// src/adapters/diagnostics/checks/disk-check.ts
|
|
6141
6558
|
import { existsSync as existsSync23, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
6142
|
-
import { join as
|
|
6559
|
+
import { join as join27 } from "path";
|
|
6143
6560
|
var DEFAULT_THRESHOLDS = { warnMb: 100, failMb: 500 };
|
|
6144
6561
|
var DiskUsageCheck = class {
|
|
6145
6562
|
id = "disk_usage";
|
|
@@ -6171,7 +6588,7 @@ var DiskUsageCheck = class {
|
|
|
6171
6588
|
if (entry.isFile()) {
|
|
6172
6589
|
try {
|
|
6173
6590
|
const parent = entry.parentPath ?? entry.path ?? dirPath;
|
|
6174
|
-
total += statSync3(
|
|
6591
|
+
total += statSync3(join27(parent, entry.name)).size;
|
|
6175
6592
|
} catch {
|
|
6176
6593
|
}
|
|
6177
6594
|
}
|
|
@@ -6185,8 +6602,8 @@ var DiskUsageCheck = class {
|
|
|
6185
6602
|
// src/cli/doctor-fix.ts
|
|
6186
6603
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
6187
6604
|
import { appendFileSync, existsSync as existsSync24, mkdirSync as mkdirSync6, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
6188
|
-
import { createRequire as
|
|
6189
|
-
import { dirname as
|
|
6605
|
+
import { createRequire as createRequire8 } from "module";
|
|
6606
|
+
import { dirname as dirname8, join as join28 } from "path";
|
|
6190
6607
|
async function applyFixes(report, options) {
|
|
6191
6608
|
if (isCI(process.env) && !options.yes && !options.dryRun) {
|
|
6192
6609
|
return {
|
|
@@ -6421,7 +6838,7 @@ async function fixIndex(projectRoot, dryRun) {
|
|
|
6421
6838
|
}
|
|
6422
6839
|
async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
6423
6840
|
if (dryRun) return;
|
|
6424
|
-
const indexDir =
|
|
6841
|
+
const indexDir = join28(ctxoRoot, "index");
|
|
6425
6842
|
if (!existsSync24(indexDir)) return;
|
|
6426
6843
|
const sourceFiles = listGitFiles2(projectRoot);
|
|
6427
6844
|
if (sourceFiles.size === 0) return;
|
|
@@ -6430,7 +6847,7 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
|
6430
6847
|
let removed = 0;
|
|
6431
6848
|
for (const idx of indices) {
|
|
6432
6849
|
if (sourceFiles.has(idx.file)) continue;
|
|
6433
|
-
const jsonPath =
|
|
6850
|
+
const jsonPath = join28(indexDir, `${idx.file}.json`);
|
|
6434
6851
|
try {
|
|
6435
6852
|
if (existsSync24(jsonPath)) {
|
|
6436
6853
|
unlinkSync2(jsonPath);
|
|
@@ -6443,14 +6860,14 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
|
6443
6860
|
if (removed === 0) return;
|
|
6444
6861
|
}
|
|
6445
6862
|
function pruneEmptyParents(filePath, stopAt) {
|
|
6446
|
-
let dir =
|
|
6863
|
+
let dir = dirname8(filePath);
|
|
6447
6864
|
while (dir !== stopAt && dir.startsWith(stopAt)) {
|
|
6448
6865
|
try {
|
|
6449
6866
|
rmSync2(dir, { recursive: false });
|
|
6450
6867
|
} catch {
|
|
6451
6868
|
return;
|
|
6452
6869
|
}
|
|
6453
|
-
dir =
|
|
6870
|
+
dir = dirname8(dir);
|
|
6454
6871
|
}
|
|
6455
6872
|
}
|
|
6456
6873
|
function listGitFiles2(projectRoot) {
|
|
@@ -6468,12 +6885,12 @@ function listGitFiles2(projectRoot) {
|
|
|
6468
6885
|
}
|
|
6469
6886
|
}
|
|
6470
6887
|
async function fixSqliteCache(projectRoot, ctxoRoot, dryRun) {
|
|
6471
|
-
const cacheDir =
|
|
6888
|
+
const cacheDir = join28(ctxoRoot, ".cache");
|
|
6472
6889
|
if (dryRun) return;
|
|
6473
6890
|
if (existsSync24(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
|
|
6474
6891
|
await new SyncCommand(projectRoot).run();
|
|
6475
6892
|
}
|
|
6476
|
-
var doctorRequire =
|
|
6893
|
+
var doctorRequire = createRequire8(import.meta.url);
|
|
6477
6894
|
function canResolvePlugin(lang) {
|
|
6478
6895
|
try {
|
|
6479
6896
|
doctorRequire.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
@@ -6488,7 +6905,7 @@ function isCI(env) {
|
|
|
6488
6905
|
function writeLog(ctxoRoot, attempts, halted, reason) {
|
|
6489
6906
|
try {
|
|
6490
6907
|
if (!existsSync24(ctxoRoot)) mkdirSync6(ctxoRoot, { recursive: true });
|
|
6491
|
-
const path =
|
|
6908
|
+
const path = join28(ctxoRoot, "doctor-fix.log");
|
|
6492
6909
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
6493
6910
|
const lines = [`# ${ts} doctor --fix${halted ? " (halted: " + reason + ")" : ""}`];
|
|
6494
6911
|
for (const a of attempts) {
|
|
@@ -6523,11 +6940,10 @@ var DoctorCommand = class {
|
|
|
6523
6940
|
ctxoRoot;
|
|
6524
6941
|
constructor(projectRoot) {
|
|
6525
6942
|
this.projectRoot = projectRoot;
|
|
6526
|
-
this.ctxoRoot =
|
|
6943
|
+
this.ctxoRoot = join29(projectRoot, ".ctxo");
|
|
6527
6944
|
}
|
|
6528
6945
|
async run(options = {}) {
|
|
6529
|
-
|
|
6530
|
-
console.error(`ctxo v${getVersion2()} \u2014 health check
|
|
6946
|
+
console.error(`ctxo v${getVersion()} \u2014 health check
|
|
6531
6947
|
`);
|
|
6532
6948
|
const checks = [
|
|
6533
6949
|
new VersionsCheck(),
|
|
@@ -6538,6 +6954,7 @@ var DoctorCommand = class {
|
|
|
6538
6954
|
new AiRulesCheck(),
|
|
6539
6955
|
new SkillsCheck(),
|
|
6540
6956
|
new ClaudeGuardHookCheck(),
|
|
6957
|
+
new InstallCopiesCheck(),
|
|
6541
6958
|
new IndexDirectoryCheck(),
|
|
6542
6959
|
new IndexFreshnessCheck(),
|
|
6543
6960
|
new SqliteCacheCheck(),
|
|
@@ -6552,9 +6969,11 @@ var DoctorCommand = class {
|
|
|
6552
6969
|
new EdgeCountCheck(),
|
|
6553
6970
|
new OrphanedFilesCheck(),
|
|
6554
6971
|
new CoChangesCacheCheck(),
|
|
6555
|
-
new SchemaVersionCheck()
|
|
6972
|
+
new SchemaVersionCheck(),
|
|
6973
|
+
// Module checks last, so the core report reads the same with or without a module.
|
|
6974
|
+
...(options.modules ?? []).flatMap((module) => module.registration.doctorChecks ?? [])
|
|
6556
6975
|
];
|
|
6557
|
-
const indexDir =
|
|
6976
|
+
const indexDir = join29(this.ctxoRoot, "index");
|
|
6558
6977
|
const indices = existsSync25(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
|
|
6559
6978
|
const checker = new HealthChecker(checks, { quiet: Boolean(options.quiet) });
|
|
6560
6979
|
const report = await checker.runAll({
|
|
@@ -6597,7 +7016,7 @@ var DoctorCommand = class {
|
|
|
6597
7016
|
|
|
6598
7017
|
// src/cli/visualize-command.ts
|
|
6599
7018
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync5, existsSync as existsSync26 } from "fs";
|
|
6600
|
-
import { join as
|
|
7019
|
+
import { join as join30, basename } from "path";
|
|
6601
7020
|
import { execFile } from "child_process";
|
|
6602
7021
|
|
|
6603
7022
|
// src/cli/graph-loader.ts
|
|
@@ -6613,7 +7032,7 @@ function loadGraph(ctxoRoot) {
|
|
|
6613
7032
|
var VisualizeCommand = class {
|
|
6614
7033
|
constructor(projectRoot) {
|
|
6615
7034
|
this.projectRoot = projectRoot;
|
|
6616
|
-
this.ctxoRoot =
|
|
7035
|
+
this.ctxoRoot = join30(projectRoot, ".ctxo");
|
|
6617
7036
|
}
|
|
6618
7037
|
projectRoot;
|
|
6619
7038
|
ctxoRoot;
|
|
@@ -6756,7 +7175,7 @@ var VisualizeCommand = class {
|
|
|
6756
7175
|
const templatePath = this.getTemplatePath();
|
|
6757
7176
|
const template = readFileSync15(templatePath, "utf-8");
|
|
6758
7177
|
const html = template.replace("/*__CTXO_DATA__*/null", () => JSON.stringify(payload).replace(/</g, "\\u003c"));
|
|
6759
|
-
const outputPath = options.output ??
|
|
7178
|
+
const outputPath = options.output ?? join30(this.ctxoRoot, "visualize.html");
|
|
6760
7179
|
writeFileSync5(outputPath, html, "utf-8");
|
|
6761
7180
|
console.error(`[ctxo] Wrote visualization to ${outputPath}`);
|
|
6762
7181
|
console.error(`[ctxo] ${nodes.length} symbols, ${edges.length} edges, ${deadResult.deadSymbols.length} dead`);
|
|
@@ -6767,9 +7186,9 @@ var VisualizeCommand = class {
|
|
|
6767
7186
|
getTemplatePath() {
|
|
6768
7187
|
let dir = import.meta.dirname;
|
|
6769
7188
|
for (let i = 0; i < 10; i++) {
|
|
6770
|
-
const candidate =
|
|
7189
|
+
const candidate = join30(dir, "pages", "visualize-template.html");
|
|
6771
7190
|
if (existsSync26(candidate)) return candidate;
|
|
6772
|
-
const parent =
|
|
7191
|
+
const parent = join30(dir, "..");
|
|
6773
7192
|
if (parent === dir) break;
|
|
6774
7193
|
dir = parent;
|
|
6775
7194
|
}
|
|
@@ -6794,11 +7213,11 @@ var VisualizeCommand = class {
|
|
|
6794
7213
|
|
|
6795
7214
|
// src/cli/update-command.ts
|
|
6796
7215
|
import { existsSync as existsSync27, readFileSync as readFileSync16 } from "fs";
|
|
6797
|
-
import { join as
|
|
7216
|
+
import { join as join31 } from "path";
|
|
6798
7217
|
|
|
6799
7218
|
// src/core/update/registry-client.ts
|
|
6800
7219
|
import { request } from "https";
|
|
6801
|
-
var
|
|
7220
|
+
var log4 = createLogger("ctxo:update:registry");
|
|
6802
7221
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
6803
7222
|
async function fetchDistTags(packageName, options = {}) {
|
|
6804
7223
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -6840,14 +7259,14 @@ function classifyFetchError(name, err) {
|
|
|
6840
7259
|
if (code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
6841
7260
|
return { name, reason: "network", message };
|
|
6842
7261
|
}
|
|
6843
|
-
|
|
7262
|
+
log4.error(`unexpected fetch error for ${name}: ${message}`);
|
|
6844
7263
|
return { name, reason: "network", message };
|
|
6845
7264
|
}
|
|
6846
|
-
var defaultHttpsFetcher = (url, timeoutMs) => new Promise((
|
|
7265
|
+
var defaultHttpsFetcher = (url, timeoutMs) => new Promise((resolve5, reject) => {
|
|
6847
7266
|
const req = request(url, { method: "GET", headers: { "user-agent": "ctxo-update", accept: "application/json" } }, (res) => {
|
|
6848
7267
|
const chunks = [];
|
|
6849
7268
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
6850
|
-
res.on("end", () =>
|
|
7269
|
+
res.on("end", () => resolve5({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf-8") }));
|
|
6851
7270
|
res.on("error", reject);
|
|
6852
7271
|
});
|
|
6853
7272
|
req.setTimeout(timeoutMs, () => {
|
|
@@ -6948,7 +7367,7 @@ function isSafeVersionSpecifier(version) {
|
|
|
6948
7367
|
}
|
|
6949
7368
|
|
|
6950
7369
|
// src/cli/update-command.ts
|
|
6951
|
-
var
|
|
7370
|
+
var log5 = createLogger("ctxo:update");
|
|
6952
7371
|
var CTXO_CLI_PACKAGE = "@ctxo/cli";
|
|
6953
7372
|
function formatText(report) {
|
|
6954
7373
|
const lines = [];
|
|
@@ -7112,16 +7531,16 @@ var UpdateCommand = class {
|
|
|
7112
7531
|
return this.projectHasCtxoDep() ? "execute" : "print";
|
|
7113
7532
|
}
|
|
7114
7533
|
projectHasCtxoDep() {
|
|
7115
|
-
const pkg =
|
|
7534
|
+
const pkg = join31(this.projectRoot, "package.json");
|
|
7116
7535
|
if (!existsSync27(pkg)) return false;
|
|
7117
7536
|
try {
|
|
7118
7537
|
const json = JSON.parse(readFileSync16(pkg, "utf-8"));
|
|
7119
7538
|
const merged = { ...json.dependencies ?? {}, ...json.devDependencies ?? {} };
|
|
7120
7539
|
return Object.keys(merged).some(
|
|
7121
|
-
(name) => name === CTXO_CLI_PACKAGE || name.startsWith("@ctxo/lang-") || name.startsWith("ctxo-lang-")
|
|
7540
|
+
(name) => name === CTXO_CLI_PACKAGE || name === DOMAIN_MODULE_PACKAGE || name.startsWith("@ctxo/lang-") || name.startsWith("ctxo-lang-")
|
|
7122
7541
|
);
|
|
7123
7542
|
} catch (err) {
|
|
7124
|
-
|
|
7543
|
+
log5.error(`failed to read project package.json: ${err.message}`);
|
|
7125
7544
|
return false;
|
|
7126
7545
|
}
|
|
7127
7546
|
}
|
|
@@ -7140,9 +7559,12 @@ var UpdateCommand = class {
|
|
|
7140
7559
|
async function defaultDiscoverInstalled(projectRoot) {
|
|
7141
7560
|
const out = [{ name: CTXO_CLI_PACKAGE, version: getVersion() }];
|
|
7142
7561
|
const manifestPath = loadManifestPath(projectRoot);
|
|
7143
|
-
if (
|
|
7144
|
-
|
|
7145
|
-
|
|
7562
|
+
if (manifestPath) {
|
|
7563
|
+
const { plugins } = await discoverPlugins({ manifestPath });
|
|
7564
|
+
for (const p of plugins) out.push({ name: p.specifier, version: p.plugin.version });
|
|
7565
|
+
}
|
|
7566
|
+
const domain = resolvePackageCandidates(DOMAIN_MODULE_PACKAGE, projectRoot)[0];
|
|
7567
|
+
if (domain !== void 0) out.push({ name: DOMAIN_MODULE_PACKAGE, version: domain.version });
|
|
7146
7568
|
return out;
|
|
7147
7569
|
}
|
|
7148
7570
|
function isLockedCI2(env) {
|
|
@@ -7150,11 +7572,11 @@ function isLockedCI2(env) {
|
|
|
7150
7572
|
}
|
|
7151
7573
|
|
|
7152
7574
|
// src/cli/blast-radius-command.ts
|
|
7153
|
-
import { join as
|
|
7575
|
+
import { join as join32 } from "path";
|
|
7154
7576
|
var BlastRadiusCommand = class {
|
|
7155
7577
|
ctxoRoot;
|
|
7156
7578
|
constructor(projectRoot) {
|
|
7157
|
-
this.ctxoRoot =
|
|
7579
|
+
this.ctxoRoot = join32(projectRoot, ".ctxo");
|
|
7158
7580
|
}
|
|
7159
7581
|
run(options) {
|
|
7160
7582
|
const { graph } = loadGraph(this.ctxoRoot);
|
|
@@ -7187,12 +7609,12 @@ var BlastRadiusCommand = class {
|
|
|
7187
7609
|
};
|
|
7188
7610
|
|
|
7189
7611
|
// src/cli/contexts-command.ts
|
|
7190
|
-
import { join as
|
|
7612
|
+
import { join as join33 } from "path";
|
|
7191
7613
|
var NO_VIEW_HINT = "No contexts view. Run ctxo index (without --skip-community).";
|
|
7192
7614
|
var ContextsCommand = class {
|
|
7193
7615
|
ctxoRoot;
|
|
7194
7616
|
constructor(projectRoot) {
|
|
7195
|
-
this.ctxoRoot =
|
|
7617
|
+
this.ctxoRoot = join33(projectRoot, ".ctxo");
|
|
7196
7618
|
}
|
|
7197
7619
|
run(options) {
|
|
7198
7620
|
const view = new ContextsViewReader(this.ctxoRoot).readContextsView();
|
|
@@ -7232,7 +7654,7 @@ var ContextsCommand = class {
|
|
|
7232
7654
|
};
|
|
7233
7655
|
|
|
7234
7656
|
// src/cli/gate-hook-command.ts
|
|
7235
|
-
import { join as
|
|
7657
|
+
import { join as join35, relative as relative6, sep as sep3 } from "path";
|
|
7236
7658
|
import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
|
|
7237
7659
|
|
|
7238
7660
|
// src/core/gate/gate-config.ts
|
|
@@ -7301,8 +7723,8 @@ function transcriptHasBlastCheck(lines, symbolId) {
|
|
|
7301
7723
|
|
|
7302
7724
|
// src/adapters/gate/gate-session-store.ts
|
|
7303
7725
|
import { existsSync as existsSync28, mkdirSync as mkdirSync7, readFileSync as readFileSync17, writeFileSync as writeFileSync6 } from "fs";
|
|
7304
|
-
import { join as
|
|
7305
|
-
var
|
|
7726
|
+
import { join as join34 } from "path";
|
|
7727
|
+
var log6 = createLogger("ctxo:gate-store");
|
|
7306
7728
|
var GateSessionStore = class {
|
|
7307
7729
|
constructor(cacheDir) {
|
|
7308
7730
|
this.cacheDir = cacheDir;
|
|
@@ -7310,7 +7732,7 @@ var GateSessionStore = class {
|
|
|
7310
7732
|
cacheDir;
|
|
7311
7733
|
file(sessionId) {
|
|
7312
7734
|
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7313
|
-
return
|
|
7735
|
+
return join34(this.cacheDir, "gate", `${safe}.json`);
|
|
7314
7736
|
}
|
|
7315
7737
|
read(sessionId) {
|
|
7316
7738
|
const f = this.file(sessionId);
|
|
@@ -7318,7 +7740,7 @@ var GateSessionStore = class {
|
|
|
7318
7740
|
try {
|
|
7319
7741
|
return new Set(JSON.parse(readFileSync17(f, "utf-8")));
|
|
7320
7742
|
} catch (err) {
|
|
7321
|
-
|
|
7743
|
+
log6.error(`${err.message}`);
|
|
7322
7744
|
return /* @__PURE__ */ new Set();
|
|
7323
7745
|
}
|
|
7324
7746
|
}
|
|
@@ -7330,10 +7752,10 @@ var GateSessionStore = class {
|
|
|
7330
7752
|
const set = this.read(sessionId);
|
|
7331
7753
|
set.add(symbolId);
|
|
7332
7754
|
const f = this.file(sessionId);
|
|
7333
|
-
mkdirSync7(
|
|
7755
|
+
mkdirSync7(join34(this.cacheDir, "gate"), { recursive: true });
|
|
7334
7756
|
writeFileSync6(f, JSON.stringify([...set]), "utf-8");
|
|
7335
7757
|
} catch (err) {
|
|
7336
|
-
|
|
7758
|
+
log6.error(`${err.message}`);
|
|
7337
7759
|
}
|
|
7338
7760
|
}
|
|
7339
7761
|
};
|
|
@@ -7364,13 +7786,13 @@ ${list}` : "",
|
|
|
7364
7786
|
}
|
|
7365
7787
|
|
|
7366
7788
|
// src/cli/gate-hook-command.ts
|
|
7367
|
-
var
|
|
7789
|
+
var log7 = createLogger("ctxo:gate-hook");
|
|
7368
7790
|
var RANK_ALL = 1e5;
|
|
7369
7791
|
var GateHookCommand = class {
|
|
7370
7792
|
constructor(projectRoot) {
|
|
7371
7793
|
this.projectRoot = projectRoot;
|
|
7372
|
-
this.ctxoRoot =
|
|
7373
|
-
this.cacheDir =
|
|
7794
|
+
this.ctxoRoot = join35(projectRoot, ".ctxo");
|
|
7795
|
+
this.cacheDir = join35(this.ctxoRoot, ".cache");
|
|
7374
7796
|
}
|
|
7375
7797
|
projectRoot;
|
|
7376
7798
|
ctxoRoot;
|
|
@@ -7386,7 +7808,7 @@ var GateHookCommand = class {
|
|
|
7386
7808
|
const thresholds = resolveGateThresholds(loaded.config.gate);
|
|
7387
7809
|
if (!thresholds.enabled) return { block: false };
|
|
7388
7810
|
const relPath = relative6(this.projectRoot, filePath).split(sep3).join("/");
|
|
7389
|
-
const indexFile =
|
|
7811
|
+
const indexFile = join35(this.ctxoRoot, "index", `${relPath}.json`);
|
|
7390
7812
|
if (!existsSync29(indexFile)) return { block: false };
|
|
7391
7813
|
const fileIndex = new JsonIndexReader(this.ctxoRoot).readSingle(indexFile);
|
|
7392
7814
|
if (!fileIndex) return { block: false };
|
|
@@ -7420,7 +7842,7 @@ var GateHookCommand = class {
|
|
|
7420
7842
|
const top = blast.impactedSymbols.filter((s) => s.confidence !== "potential").map((s) => s.symbolId);
|
|
7421
7843
|
return { block: true, reason: formatBlockReason(name, riskCount, top) };
|
|
7422
7844
|
} catch (err) {
|
|
7423
|
-
|
|
7845
|
+
log7.error(`${err.message}`);
|
|
7424
7846
|
return { block: false };
|
|
7425
7847
|
}
|
|
7426
7848
|
}
|
|
@@ -7432,7 +7854,7 @@ var GateHookCommand = class {
|
|
|
7432
7854
|
if (decision.block) this.emitBlock(decision.reason ?? "ctxo guard");
|
|
7433
7855
|
}
|
|
7434
7856
|
} catch (err) {
|
|
7435
|
-
|
|
7857
|
+
log7.error(`${err.message}`);
|
|
7436
7858
|
}
|
|
7437
7859
|
process.exit(0);
|
|
7438
7860
|
}
|
|
@@ -7448,7 +7870,7 @@ var GateHookCommand = class {
|
|
|
7448
7870
|
process.exit(0);
|
|
7449
7871
|
}
|
|
7450
7872
|
readStdin() {
|
|
7451
|
-
return new Promise((
|
|
7873
|
+
return new Promise((resolve5) => {
|
|
7452
7874
|
let data = "";
|
|
7453
7875
|
process.stdin.setEncoding("utf-8");
|
|
7454
7876
|
process.stdin.on("data", (c) => {
|
|
@@ -7456,23 +7878,23 @@ var GateHookCommand = class {
|
|
|
7456
7878
|
});
|
|
7457
7879
|
process.stdin.on("end", () => {
|
|
7458
7880
|
try {
|
|
7459
|
-
|
|
7881
|
+
resolve5(JSON.parse(data));
|
|
7460
7882
|
} catch {
|
|
7461
|
-
|
|
7883
|
+
resolve5(null);
|
|
7462
7884
|
}
|
|
7463
7885
|
});
|
|
7464
|
-
process.stdin.on("error", () =>
|
|
7886
|
+
process.stdin.on("error", () => resolve5(null));
|
|
7465
7887
|
});
|
|
7466
7888
|
}
|
|
7467
7889
|
};
|
|
7468
7890
|
|
|
7469
7891
|
// src/cli/gate-command.ts
|
|
7470
|
-
import { join as
|
|
7892
|
+
import { join as join36 } from "path";
|
|
7471
7893
|
var RANK_ALL2 = 1e5;
|
|
7472
7894
|
var GateCommand = class {
|
|
7473
7895
|
ctxoRoot;
|
|
7474
7896
|
constructor(projectRoot) {
|
|
7475
|
-
this.ctxoRoot =
|
|
7897
|
+
this.ctxoRoot = join36(projectRoot, ".ctxo");
|
|
7476
7898
|
}
|
|
7477
7899
|
preview(options = {}) {
|
|
7478
7900
|
const gate = loadConfig(this.ctxoRoot).config.gate;
|
|
@@ -7545,43 +7967,50 @@ function readInitPartsFlag(args, flag) {
|
|
|
7545
7967
|
}
|
|
7546
7968
|
return { present, parts };
|
|
7547
7969
|
}
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7970
|
+
var BOUNDARIES_ERROR = "[ctxo] --boundaries requires by-rung or report";
|
|
7971
|
+
function readBoundariesFlag(args) {
|
|
7972
|
+
let mode;
|
|
7973
|
+
for (let i = 1; i < args.length; i++) {
|
|
7974
|
+
const arg = args[i];
|
|
7975
|
+
let value;
|
|
7976
|
+
if (arg === "--boundaries") {
|
|
7977
|
+
value = args[i + 1];
|
|
7978
|
+
i += 1;
|
|
7979
|
+
} else if (arg.startsWith("--boundaries=")) {
|
|
7980
|
+
value = arg.slice("--boundaries=".length);
|
|
7981
|
+
} else {
|
|
7982
|
+
continue;
|
|
7559
7983
|
}
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
dir = parent;
|
|
7984
|
+
if (value !== "by-rung" && value !== "report") return { error: BOUNDARIES_ERROR };
|
|
7985
|
+
mode = value;
|
|
7563
7986
|
}
|
|
7564
|
-
return
|
|
7565
|
-
}
|
|
7566
|
-
function locateGitRoot(startDir) {
|
|
7567
|
-
let dir = startDir;
|
|
7568
|
-
for (let i = 0; i < 12; i++) {
|
|
7569
|
-
if (existsSync30(join36(dir, ".git"))) return dir;
|
|
7570
|
-
const parent = dirname10(dir);
|
|
7571
|
-
if (parent === dir) break;
|
|
7572
|
-
dir = parent;
|
|
7573
|
-
}
|
|
7574
|
-
return null;
|
|
7987
|
+
return mode !== void 0 ? { mode } : {};
|
|
7575
7988
|
}
|
|
7576
7989
|
var CliRouter = class {
|
|
7577
7990
|
projectRoot;
|
|
7578
|
-
|
|
7991
|
+
cliAnchor;
|
|
7992
|
+
modules;
|
|
7993
|
+
constructor(projectRoot, options = {}) {
|
|
7579
7994
|
this.projectRoot = locateGitRoot(projectRoot) ?? projectRoot;
|
|
7995
|
+
this.cliAnchor = options.cliAnchor;
|
|
7996
|
+
}
|
|
7997
|
+
/**
|
|
7998
|
+
* The opt-in modules (@ctxo/domain), loaded on first use and only by the routes that need them: the
|
|
7999
|
+
* help, module and unknown commands, `index` and `doctor` (`version` reports them through its own
|
|
8000
|
+
* loader call, which shares this load). Never rejects; a failed load means no module.
|
|
8001
|
+
*/
|
|
8002
|
+
loadModules() {
|
|
8003
|
+
this.modules ??= loadModulesFor(this.projectRoot, "cli", { cliAnchor: this.cliAnchor });
|
|
8004
|
+
return this.modules;
|
|
7580
8005
|
}
|
|
7581
8006
|
async route(args) {
|
|
7582
8007
|
const command = args[0];
|
|
8008
|
+
if (command !== void 0 && isKnownModuleCommand(command)) {
|
|
8009
|
+
await this.dispatchModuleCommand(command, args);
|
|
8010
|
+
return;
|
|
8011
|
+
}
|
|
7583
8012
|
if (!command || args.some((arg) => arg === "--help" || arg === "-h")) {
|
|
7584
|
-
this.printHelp();
|
|
8013
|
+
await this.printHelp();
|
|
7585
8014
|
return;
|
|
7586
8015
|
}
|
|
7587
8016
|
if (command === "--version" || command === "-v" || command === "-V") {
|
|
@@ -7662,6 +8091,7 @@ var CliRouter = class {
|
|
|
7662
8091
|
process.exit(1);
|
|
7663
8092
|
return;
|
|
7664
8093
|
}
|
|
8094
|
+
const modules = await this.loadModules();
|
|
7665
8095
|
await new IndexCommand(this.projectRoot).run({
|
|
7666
8096
|
files: filesArg.length > 0 ? filesArg : void 0,
|
|
7667
8097
|
commit: commitArg,
|
|
@@ -7671,7 +8101,8 @@ var CliRouter = class {
|
|
|
7671
8101
|
maxHistory: maxHistoryArg,
|
|
7672
8102
|
cochangeCommits: cochangeCommitsArg,
|
|
7673
8103
|
cochangeSince: cochangeSinceArg,
|
|
7674
|
-
installMissing
|
|
8104
|
+
installMissing,
|
|
8105
|
+
modules
|
|
7675
8106
|
});
|
|
7676
8107
|
break;
|
|
7677
8108
|
}
|
|
@@ -7695,16 +8126,18 @@ var CliRouter = class {
|
|
|
7695
8126
|
const dryRun = args.includes("--dry-run");
|
|
7696
8127
|
const noInstall = args.includes("--no-install");
|
|
7697
8128
|
const noGuard = args.includes("--no-guard");
|
|
8129
|
+
const domain = args.includes("--domain");
|
|
7698
8130
|
const tools = toolsArg ? toolsArg.split(",").map((t) => t.trim()) : void 0;
|
|
7699
8131
|
const only = readInitPartsFlag(args, "--only");
|
|
7700
8132
|
const skip = readInitPartsFlag(args, "--skip");
|
|
7701
|
-
const
|
|
8133
|
+
const boundaries = readBoundariesFlag(args);
|
|
8134
|
+
const partsError = only.error ?? skip.error ?? (only.present && (skip.present || rulesOnly) ? "[ctxo] --only cannot be combined with --skip or --rules" : void 0) ?? boundaries.error;
|
|
7702
8135
|
if (partsError) {
|
|
7703
8136
|
console.error(partsError);
|
|
7704
8137
|
process.exit(1);
|
|
7705
8138
|
return;
|
|
7706
8139
|
}
|
|
7707
|
-
await new InitCommand(this.projectRoot).run({
|
|
8140
|
+
await new InitCommand(this.projectRoot, { cliAnchor: this.cliAnchor }).run({
|
|
7708
8141
|
tools,
|
|
7709
8142
|
yes: yesArg,
|
|
7710
8143
|
rulesOnly,
|
|
@@ -7712,7 +8145,9 @@ var CliRouter = class {
|
|
|
7712
8145
|
noInstall,
|
|
7713
8146
|
only: only.present ? only.parts : void 0,
|
|
7714
8147
|
skip: skip.present ? skip.parts : void 0,
|
|
7715
|
-
noGuard
|
|
8148
|
+
noGuard,
|
|
8149
|
+
domain,
|
|
8150
|
+
...boundaries.mode !== void 0 ? { boundaries: boundaries.mode } : {}
|
|
7716
8151
|
});
|
|
7717
8152
|
break;
|
|
7718
8153
|
}
|
|
@@ -7730,12 +8165,14 @@ var CliRouter = class {
|
|
|
7730
8165
|
const fixArg = args.includes("--fix");
|
|
7731
8166
|
const dryRunArg = args.includes("--dry-run");
|
|
7732
8167
|
const yesArg = args.includes("--yes") || args.includes("-y");
|
|
8168
|
+
const modules = await this.loadModules();
|
|
7733
8169
|
await new DoctorCommand(this.projectRoot).run({
|
|
7734
8170
|
json: jsonArg,
|
|
7735
8171
|
quiet: quietArg,
|
|
7736
8172
|
fix: fixArg,
|
|
7737
8173
|
dryRun: dryRunArg,
|
|
7738
|
-
yes: yesArg
|
|
8174
|
+
yes: yesArg,
|
|
8175
|
+
modules
|
|
7739
8176
|
});
|
|
7740
8177
|
break;
|
|
7741
8178
|
}
|
|
@@ -7864,13 +8301,51 @@ var CliRouter = class {
|
|
|
7864
8301
|
break;
|
|
7865
8302
|
}
|
|
7866
8303
|
default:
|
|
8304
|
+
if (await this.dispatchModuleCommand(command, args)) return;
|
|
7867
8305
|
console.error(`[ctxo] Unknown command: "${command}". Run "ctxo --help" for usage.`);
|
|
7868
8306
|
process.exit(1);
|
|
7869
8307
|
return;
|
|
7870
8308
|
}
|
|
7871
8309
|
}
|
|
7872
|
-
|
|
8310
|
+
/**
|
|
8311
|
+
* Runs a module command and sets `process.exitCode` from it. A known module command name without its
|
|
8312
|
+
* module prints the install hint and exits 1 (the hook command exits 0 silently, as on the fast route).
|
|
8313
|
+
* Resolves to false when no module registers `command` and the CLI does not know the name, so the caller
|
|
8314
|
+
* reports an unknown command.
|
|
8315
|
+
*/
|
|
8316
|
+
async dispatchModuleCommand(command, args) {
|
|
8317
|
+
if (FAST_ROUTE_COMMANDS.has(command)) {
|
|
8318
|
+
process.exitCode = await runFastRoute(args, this.projectRoot, { cliAnchor: this.cliAnchor });
|
|
8319
|
+
return true;
|
|
8320
|
+
}
|
|
8321
|
+
let code;
|
|
8322
|
+
try {
|
|
8323
|
+
code = await runModuleCommand(command, args.slice(1), {
|
|
8324
|
+
projectRoot: this.projectRoot,
|
|
8325
|
+
modules: await this.loadModules()
|
|
8326
|
+
});
|
|
8327
|
+
} catch (err) {
|
|
8328
|
+
console.error(`[ctxo] "ctxo ${command}" failed: ${err.message}`);
|
|
8329
|
+
process.exitCode = 1;
|
|
8330
|
+
return true;
|
|
8331
|
+
}
|
|
8332
|
+
if (code !== void 0) {
|
|
8333
|
+
process.exitCode = typeof code === "number" && Number.isInteger(code) ? code : 1;
|
|
8334
|
+
return true;
|
|
8335
|
+
}
|
|
8336
|
+
if (isKnownModuleCommand(command)) {
|
|
8337
|
+
console.error(`[ctxo] "ctxo ${command}" needs @ctxo/domain. Install it with "ctxo install domain".`);
|
|
8338
|
+
process.exitCode = 1;
|
|
8339
|
+
return true;
|
|
8340
|
+
}
|
|
8341
|
+
return false;
|
|
8342
|
+
}
|
|
8343
|
+
async printHelp() {
|
|
7873
8344
|
const v = getVersion();
|
|
8345
|
+
const moduleLines = moduleHelpLines(await this.loadModules());
|
|
8346
|
+
const moduleBlock = moduleLines.length > 0 ? `
|
|
8347
|
+
|
|
8348
|
+
${moduleLines.join("\n")}` : "";
|
|
7874
8349
|
console.error(`
|
|
7875
8350
|
ctxo v${v} \u2014 MCP server for dependency-aware codebase context
|
|
7876
8351
|
|
|
@@ -7891,11 +8366,13 @@ Usage:
|
|
|
7891
8366
|
ctxo status Show index manifest
|
|
7892
8367
|
ctxo init Interactive setup (rules, skills, guard, MCP, git hooks, config); the post-commit hook runs ctxo index --commit HEAD
|
|
7893
8368
|
ctxo init --tools a,b -y Non-interactive (claude-code,cursor,...); -y without --tools selects every tool
|
|
7894
|
-
ctxo init --only <parts> Install only these parts: rules,skills,guard,mcp,git-hooks,config
|
|
8369
|
+
ctxo init --only <parts> Install only these parts: rules,skills,guard,mcp,git-hooks,config,domain
|
|
7895
8370
|
ctxo init --skip <parts> Install everything except these parts
|
|
7896
8371
|
ctxo init --no-guard Does not install the Claude Code safe-edit guard
|
|
7897
8372
|
ctxo init --rules Deprecated: same as --skip git-hooks,guard
|
|
7898
8373
|
ctxo init --dry-run List every file init would create or change
|
|
8374
|
+
ctxo init --domain Also set up the domain layer: registry, fold guard, skill (@ctxo/domain)
|
|
8375
|
+
ctxo init --boundaries <by-rung|report> Boundary mode for --domain, without the prompt
|
|
7899
8376
|
ctxo stats Show usage statistics (--json, --days N, --clear)
|
|
7900
8377
|
ctxo doctor Health check all subsystems (--json, --quiet)
|
|
7901
8378
|
ctxo doctor --fix Apply fixes in dependency order: config, git hooks, skills, the safe-edit guard, rules blocks, plugins, index (--dry-run, --yes)
|
|
@@ -7908,6 +8385,7 @@ Usage:
|
|
|
7908
8385
|
ctxo install ts --global Install globally
|
|
7909
8386
|
ctxo install py --dry-run Show command without running
|
|
7910
8387
|
ctxo install sql SQL plugin: .sql definitions, data edges from C#, TypeScript, Java and Go call sites
|
|
8388
|
+
ctxo install domain Install @ctxo/domain at this CLI's version (globally without package.json)
|
|
7911
8389
|
ctxo update Check + apply updates for ctxo + plugins
|
|
7912
8390
|
ctxo update --check Check only, no install
|
|
7913
8391
|
ctxo update --print Print install command, never execute
|
|
@@ -7925,12 +8403,10 @@ Usage:
|
|
|
7925
8403
|
ctxo --version --json Machine-readable version payload
|
|
7926
8404
|
ctxo version Subcommand \u2014 verbose by default
|
|
7927
8405
|
ctxo --help Show this help message (also ctxo <command> --help; nothing is run)
|
|
7928
|
-
`.trim());
|
|
8406
|
+
`.trim() + moduleBlock);
|
|
7929
8407
|
}
|
|
7930
8408
|
};
|
|
7931
8409
|
export {
|
|
7932
|
-
CliRouter
|
|
7933
|
-
getVersion,
|
|
7934
|
-
locateGitRoot
|
|
8410
|
+
CliRouter
|
|
7935
8411
|
};
|
|
7936
|
-
//# sourceMappingURL=cli-router-
|
|
8412
|
+
//# sourceMappingURL=cli-router-W56INOQ3.js.map
|