@danielblomma/cortex-mcp 2.4.0 → 2.4.2
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/CHANGELOG.md +226 -0
- package/README.md +24 -3
- package/bin/cli/arguments.mjs +60 -0
- package/bin/cli/context-passthrough.mjs +129 -0
- package/bin/cli/daemon.mjs +157 -0
- package/bin/cli/enterprise.mjs +516 -0
- package/bin/cli/help.mjs +88 -0
- package/bin/cli/hooks.mjs +199 -0
- package/bin/cli/mcp-command.mjs +87 -0
- package/bin/cli/paths.mjs +14 -0
- package/bin/cli/process.mjs +49 -0
- package/bin/cli/project-commands.mjs +237 -0
- package/bin/cli/project-runtime.mjs +34 -0
- package/bin/cli/query-command.mjs +18 -0
- package/bin/cli/router.mjs +66 -0
- package/bin/cli/run-command.mjs +44 -0
- package/bin/cli/scaffold-ownership.mjs +936 -0
- package/bin/cli/scaffold.mjs +687 -0
- package/bin/cli/stage-command.mjs +9 -0
- package/bin/cli/telemetry-command.mjs +18 -0
- package/bin/cli/trusted-runtime.mjs +18 -0
- package/bin/cortex.mjs +12 -1607
- package/bin/daemon-control.mjs +162 -0
- package/mcp-registry-submission.json +1 -1
- package/package.json +8 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/ownership/baseline-v2.4.1.json +22 -0
- package/scaffold/ownership/current.json +4 -0
- package/scaffold/ownership/v1.json +456 -0
- package/scaffold/scripts/ingest-parsers.mjs +1 -387
- package/scaffold/scripts/ingest-worker.mjs +4 -1
- package/scaffold/scripts/ingest.mjs +5 -3899
- package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
- package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
- package/scaffold/scripts/lib/ingest/config.mjs +120 -0
- package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
- package/scaffold/scripts/lib/ingest/files.mjs +387 -0
- package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
- package/scaffold/scripts/lib/ingest/io.mjs +78 -0
- package/scaffold/scripts/lib/ingest/main.mjs +76 -0
- package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
- package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
- package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
- package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
- package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
- package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
- package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
CONTEXT_RUNTIME_REL,
|
|
6
|
+
CONTEXT_SCRIPTS_REL,
|
|
7
|
+
MCP_PROJECT_REL,
|
|
8
|
+
PACKAGE_ROOT,
|
|
9
|
+
SCAFFOLD_ROOT,
|
|
10
|
+
} from "./paths.mjs";
|
|
11
|
+
import {
|
|
12
|
+
runCommand,
|
|
13
|
+
runCommandResult,
|
|
14
|
+
toErrorMessage,
|
|
15
|
+
} from "./process.mjs";
|
|
16
|
+
import {
|
|
17
|
+
expandLegacyFiles,
|
|
18
|
+
installManagedScaffold,
|
|
19
|
+
loadCurrentOwnershipManifest,
|
|
20
|
+
loadPreStateOwnershipBaselines,
|
|
21
|
+
} from "./scaffold-ownership.mjs";
|
|
22
|
+
|
|
23
|
+
const GITIGNORE_LINES = [
|
|
24
|
+
"",
|
|
25
|
+
"# Cortex local storage",
|
|
26
|
+
".context/*",
|
|
27
|
+
"!.context/config.yaml",
|
|
28
|
+
"!.context/rules.yaml",
|
|
29
|
+
"!.context/ontology.cypher",
|
|
30
|
+
".npm-cache/",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const DEFAULT_SOURCE_PATHS = ["."];
|
|
34
|
+
|
|
35
|
+
export function ensureScaffoldExists() {
|
|
36
|
+
if (!fs.existsSync(SCAFFOLD_ROOT)) {
|
|
37
|
+
throw new Error(`Scaffold not found at ${SCAFFOLD_ROOT}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function yamlScalar(value) {
|
|
42
|
+
return /^[A-Za-z0-9._/-]+$/.test(value) ? value : JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function slugifyRepoId(value) {
|
|
46
|
+
const dashed = String(value || "")
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
49
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
50
|
+
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
51
|
+
.replace(/^-+|-+$/g, "")
|
|
52
|
+
.toLowerCase();
|
|
53
|
+
return dashed || "cortex";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function detectInitialSourcePaths(_targetDir) {
|
|
57
|
+
return [...DEFAULT_SOURCE_PATHS];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildInitialConfig(targetDir) {
|
|
61
|
+
const repoId = slugifyRepoId(path.basename(path.resolve(targetDir)));
|
|
62
|
+
const sourcePaths = detectInitialSourcePaths(targetDir);
|
|
63
|
+
return [
|
|
64
|
+
`repo_id: ${yamlScalar(repoId)}`,
|
|
65
|
+
"source_paths:",
|
|
66
|
+
...sourcePaths.map((sourcePath) => ` - ${yamlScalar(sourcePath)}`),
|
|
67
|
+
"truth_order:",
|
|
68
|
+
" - ADR",
|
|
69
|
+
" - RULE",
|
|
70
|
+
" - CODE",
|
|
71
|
+
" - WIKI",
|
|
72
|
+
"# Tuned together with the midrank-percentile graph_score in searchResults.ts.",
|
|
73
|
+
"ranking:",
|
|
74
|
+
" semantic: 0.40",
|
|
75
|
+
" graph: 0.25",
|
|
76
|
+
" trust: 0.20",
|
|
77
|
+
" recency: 0.15",
|
|
78
|
+
"runtime:",
|
|
79
|
+
" top_k: 5",
|
|
80
|
+
" include_uncertainties: true",
|
|
81
|
+
"",
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function initializeScaffold(targetDir, force) {
|
|
86
|
+
const configPath = path.join(targetDir, ".context", "config.yaml");
|
|
87
|
+
const hasExistingConfig = fs.existsSync(configPath);
|
|
88
|
+
const generatedConfig = hasExistingConfig ? null : buildInitialConfig(targetDir);
|
|
89
|
+
installScaffold(targetDir, force);
|
|
90
|
+
if (!hasExistingConfig && generatedConfig) {
|
|
91
|
+
writeTextFile(targetDir, configPath, generatedConfig);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hardenEnterpriseConfigPermissions(targetDir) {
|
|
96
|
+
const contextDir = path.join(targetDir, ".context");
|
|
97
|
+
let contextStat;
|
|
98
|
+
try {
|
|
99
|
+
contextStat = fs.lstatSync(contextDir);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err && typeof err === "object" && err.code === "ENOENT") return;
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
if (contextStat.isSymbolicLink()) {
|
|
105
|
+
throw new Error(`Refusing symlinked Cortex context directory: ${contextDir}`);
|
|
106
|
+
}
|
|
107
|
+
if (!contextStat.isDirectory()) {
|
|
108
|
+
throw new Error(`Cortex context path is not a directory: ${contextDir}`);
|
|
109
|
+
}
|
|
110
|
+
const configs = [];
|
|
111
|
+
for (const filename of ["enterprise.yml", "enterprise.yaml"]) {
|
|
112
|
+
const configPath = path.join(contextDir, filename);
|
|
113
|
+
let stat;
|
|
114
|
+
try {
|
|
115
|
+
stat = fs.lstatSync(configPath);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err && typeof err === "object" && err.code === "ENOENT") continue;
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
if (stat.isSymbolicLink()) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Refusing symlinked Enterprise configuration: ${configPath}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (!stat.isFile()) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Enterprise configuration is not a regular file: ${configPath}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (stat.nlink > 1) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Refusing multiply linked Enterprise configuration: ${configPath}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
configs.push({ path: configPath, stat });
|
|
136
|
+
}
|
|
137
|
+
for (const config of configs) {
|
|
138
|
+
const { path: configPath, stat } = config;
|
|
139
|
+
const currentStat = fs.lstatSync(configPath);
|
|
140
|
+
if (
|
|
141
|
+
!currentStat.isFile() ||
|
|
142
|
+
currentStat.isSymbolicLink() ||
|
|
143
|
+
currentStat.nlink > 1 ||
|
|
144
|
+
currentStat.dev !== stat.dev ||
|
|
145
|
+
currentStat.ino !== stat.ino
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Enterprise configuration changed during permission hardening: ${configPath}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
fs.chmodSync(configPath, 0o600);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ensurePathWritable(targetPath, force) {
|
|
156
|
+
if (!fs.existsSync(targetPath)) return;
|
|
157
|
+
if (!force) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Refusing to overwrite existing path: ${targetPath}\nRun with --force to overwrite scaffold files.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mergeGitignore(targetDir) {
|
|
165
|
+
const gitignorePath = resolveSafeTextTarget(
|
|
166
|
+
targetDir,
|
|
167
|
+
path.join(targetDir, ".gitignore"),
|
|
168
|
+
);
|
|
169
|
+
let gitignoreStat = null;
|
|
170
|
+
try {
|
|
171
|
+
gitignoreStat = fs.lstatSync(gitignorePath);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (gitignoreStat?.isSymbolicLink()) {
|
|
178
|
+
throw new Error(`Refusing symlinked .gitignore: ${gitignorePath}`);
|
|
179
|
+
}
|
|
180
|
+
if (gitignoreStat !== null && !gitignoreStat.isFile()) {
|
|
181
|
+
throw new Error(`.gitignore is not a regular file: ${gitignorePath}`);
|
|
182
|
+
}
|
|
183
|
+
const current =
|
|
184
|
+
gitignoreStat === null ? "" : fs.readFileSync(gitignorePath, "utf8");
|
|
185
|
+
const merged =
|
|
186
|
+
current +
|
|
187
|
+
GITIGNORE_LINES.filter((line) => !current.includes(line)).join("\n") +
|
|
188
|
+
"\n";
|
|
189
|
+
writeResolvedTextFile(gitignorePath, merged);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function migrateLegacyMcpLocation(targetDir) {
|
|
193
|
+
const legacyMcp = path.join(targetDir, "mcp");
|
|
194
|
+
const contextDir = path.join(targetDir, ".context");
|
|
195
|
+
const newMcp = path.join(targetDir, MCP_PROJECT_REL);
|
|
196
|
+
let legacyStat;
|
|
197
|
+
try {
|
|
198
|
+
legacyStat = fs.lstatSync(legacyMcp);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error && typeof error === "object" && error.code === "ENOENT") return;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (legacyStat.isSymbolicLink() || !legacyStat.isDirectory()) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Refusing unsafe legacy Cortex runtime path: ${legacyMcp}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
let contextStat = null;
|
|
209
|
+
try {
|
|
210
|
+
contextStat = fs.lstatSync(contextDir);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (contextStat?.isSymbolicLink() || (contextStat && !contextStat.isDirectory())) {
|
|
217
|
+
throw new Error(`Refusing unsafe Cortex context path: ${contextDir}`);
|
|
218
|
+
}
|
|
219
|
+
let newMcpStat = null;
|
|
220
|
+
try {
|
|
221
|
+
newMcpStat = fs.lstatSync(newMcp);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (newMcpStat !== null) {
|
|
228
|
+
if (newMcpStat.isSymbolicLink()) {
|
|
229
|
+
throw new Error(`Refusing symlinked Cortex runtime path: ${newMcp}`);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
234
|
+
fs.renameSync(legacyMcp, newMcp);
|
|
235
|
+
console.log(
|
|
236
|
+
"[cortex] migrated legacy mcp/ → .context/mcp/ to keep project root clean. " +
|
|
237
|
+
"Re-run 'cortex connect' if Claude/Codex MCP registrations need to be refreshed.",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function lstatLegacyFile(scriptsDir, relativePath) {
|
|
242
|
+
let currentPath = scriptsDir;
|
|
243
|
+
const segments = relativePath.split("/");
|
|
244
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
245
|
+
currentPath = path.join(currentPath, segments[index]);
|
|
246
|
+
let stat;
|
|
247
|
+
try {
|
|
248
|
+
stat = fs.lstatSync(currentPath);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
if (stat.isSymbolicLink()) return null;
|
|
256
|
+
if (index < segments.length - 1 && !stat.isDirectory()) return null;
|
|
257
|
+
if (index === segments.length - 1) {
|
|
258
|
+
return stat.isFile() ? { path: currentPath, stat } : null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function removeEmptyLegacyDirectories(scriptsDir, removedFiles) {
|
|
265
|
+
const directories = new Set();
|
|
266
|
+
for (const filePath of removedFiles) {
|
|
267
|
+
let directory = path.dirname(filePath);
|
|
268
|
+
while (
|
|
269
|
+
directory === scriptsDir ||
|
|
270
|
+
directory.startsWith(`${scriptsDir}${path.sep}`)
|
|
271
|
+
) {
|
|
272
|
+
directories.add(directory);
|
|
273
|
+
if (directory === scriptsDir) break;
|
|
274
|
+
directory = path.dirname(directory);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const deepestFirst = [...directories].sort(
|
|
278
|
+
(left, right) => right.split(path.sep).length - left.split(path.sep).length,
|
|
279
|
+
);
|
|
280
|
+
for (const directory of deepestFirst) {
|
|
281
|
+
try {
|
|
282
|
+
const stat = fs.lstatSync(directory);
|
|
283
|
+
if (
|
|
284
|
+
!stat.isSymbolicLink() &&
|
|
285
|
+
stat.isDirectory() &&
|
|
286
|
+
fs.readdirSync(directory).length === 0
|
|
287
|
+
) {
|
|
288
|
+
fs.rmdirSync(directory);
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function removeLegacyCortexScripts(
|
|
299
|
+
targetDir,
|
|
300
|
+
ownershipManifest,
|
|
301
|
+
preStateOwnership,
|
|
302
|
+
) {
|
|
303
|
+
if (path.resolve(targetDir) === PACKAGE_ROOT) return;
|
|
304
|
+
|
|
305
|
+
const scriptsDir = path.join(targetDir, "scripts");
|
|
306
|
+
const prepared = [];
|
|
307
|
+
for (const entry of expandLegacyFiles(ownershipManifest).sort((left, right) =>
|
|
308
|
+
left.target.localeCompare(right.target),
|
|
309
|
+
)) {
|
|
310
|
+
const sourcePath = path.join(
|
|
311
|
+
SCAFFOLD_ROOT,
|
|
312
|
+
...entry.source.split("/"),
|
|
313
|
+
);
|
|
314
|
+
let sourceStat;
|
|
315
|
+
try {
|
|
316
|
+
sourceStat = fs.lstatSync(sourcePath);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (
|
|
319
|
+
entry.optional &&
|
|
320
|
+
error &&
|
|
321
|
+
typeof error === "object" &&
|
|
322
|
+
error.code === "ENOENT"
|
|
323
|
+
) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
329
|
+
throw new Error(`Unsafe legacy migration source: ${sourcePath}`);
|
|
330
|
+
}
|
|
331
|
+
const target = lstatLegacyFile(targetDir, entry.target);
|
|
332
|
+
if (target === null) continue;
|
|
333
|
+
const packageHash = sha256File(sourcePath);
|
|
334
|
+
const targetHash = sha256File(target.path);
|
|
335
|
+
const matchesBaseline =
|
|
336
|
+
preStateOwnership.legacyHashes
|
|
337
|
+
.get(entry.target)
|
|
338
|
+
?.has(targetHash) === true;
|
|
339
|
+
if (targetHash !== packageHash && !matchesBaseline) continue;
|
|
340
|
+
prepared.push({
|
|
341
|
+
...entry,
|
|
342
|
+
path: target.path,
|
|
343
|
+
ownershipHash: targetHash,
|
|
344
|
+
dev: target.stat.dev,
|
|
345
|
+
ino: target.stat.ino,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const removedFiles = [];
|
|
349
|
+
for (const entry of prepared) {
|
|
350
|
+
const target = lstatLegacyFile(targetDir, entry.target);
|
|
351
|
+
if (
|
|
352
|
+
target === null ||
|
|
353
|
+
target.stat.dev !== entry.dev ||
|
|
354
|
+
target.stat.ino !== entry.ino ||
|
|
355
|
+
sha256File(target.path) !== entry.ownershipHash
|
|
356
|
+
) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`Refusing to remove legacy scaffold file changed during cleanup: ${entry.target}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
fs.unlinkSync(target.path);
|
|
362
|
+
removedFiles.push(target.path);
|
|
363
|
+
}
|
|
364
|
+
removeEmptyLegacyDirectories(scriptsDir, removedFiles);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function sha256File(filePath) {
|
|
368
|
+
return crypto
|
|
369
|
+
.createHash("sha256")
|
|
370
|
+
.update(fs.readFileSync(filePath))
|
|
371
|
+
.digest("hex");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function installScaffold(targetDir, force) {
|
|
375
|
+
const ownershipManifest = loadCurrentOwnershipManifest(PACKAGE_ROOT);
|
|
376
|
+
const preStateOwnership = loadPreStateOwnershipBaselines(
|
|
377
|
+
PACKAGE_ROOT,
|
|
378
|
+
ownershipManifest,
|
|
379
|
+
);
|
|
380
|
+
migrateLegacyMcpLocation(targetDir);
|
|
381
|
+
|
|
382
|
+
const copyTargets = [
|
|
383
|
+
path.join(targetDir, ".context"),
|
|
384
|
+
path.join(targetDir, CONTEXT_SCRIPTS_REL),
|
|
385
|
+
path.join(targetDir, MCP_PROJECT_REL),
|
|
386
|
+
path.join(targetDir, ".githooks"),
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
for (const targetPath of copyTargets) {
|
|
390
|
+
ensurePathWritable(targetPath, force);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
installManagedScaffold(PACKAGE_ROOT, targetDir, { force });
|
|
394
|
+
|
|
395
|
+
removeLegacyCortexScripts(
|
|
396
|
+
targetDir,
|
|
397
|
+
ownershipManifest,
|
|
398
|
+
preStateOwnership,
|
|
399
|
+
);
|
|
400
|
+
mergeGitignore(targetDir);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function resolveSafeTextTarget(targetRoot, targetPath) {
|
|
404
|
+
const resolvedRoot = path.resolve(targetRoot);
|
|
405
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
406
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
407
|
+
if (
|
|
408
|
+
relative.length === 0 ||
|
|
409
|
+
relative === ".." ||
|
|
410
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
411
|
+
path.isAbsolute(relative)
|
|
412
|
+
) {
|
|
413
|
+
throw new Error(`Text-file target is outside the project: ${targetPath}`);
|
|
414
|
+
}
|
|
415
|
+
const rootReal = fs.realpathSync(resolvedRoot);
|
|
416
|
+
const segments = relative.split(path.sep);
|
|
417
|
+
let currentPath = rootReal;
|
|
418
|
+
for (const segment of segments.slice(0, -1)) {
|
|
419
|
+
currentPath = path.join(currentPath, segment);
|
|
420
|
+
let stat = null;
|
|
421
|
+
try {
|
|
422
|
+
stat = fs.lstatSync(currentPath);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (stat === null) {
|
|
429
|
+
fs.mkdirSync(currentPath);
|
|
430
|
+
stat = fs.lstatSync(currentPath);
|
|
431
|
+
}
|
|
432
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
433
|
+
throw new Error(`Refusing unsafe text-file ancestor: ${currentPath}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return path.join(rootReal, ...segments);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function writeResolvedTextFile(targetPath, content) {
|
|
440
|
+
const targetDirectory = path.dirname(targetPath);
|
|
441
|
+
let targetStat = null;
|
|
442
|
+
try {
|
|
443
|
+
targetStat = fs.lstatSync(targetPath);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (
|
|
450
|
+
targetStat?.isSymbolicLink() ||
|
|
451
|
+
(targetStat !== null && !targetStat.isFile())
|
|
452
|
+
) {
|
|
453
|
+
throw new Error(`Refusing unsafe text-file target: ${targetPath}`);
|
|
454
|
+
}
|
|
455
|
+
const temporaryPath = path.join(
|
|
456
|
+
targetDirectory,
|
|
457
|
+
`.${path.basename(targetPath)}.tmp-${process.pid}-${crypto.randomUUID()}`,
|
|
458
|
+
);
|
|
459
|
+
try {
|
|
460
|
+
fs.writeFileSync(temporaryPath, content, {
|
|
461
|
+
encoding: "utf8",
|
|
462
|
+
flag: "wx",
|
|
463
|
+
mode: targetStat?.mode ?? 0o644,
|
|
464
|
+
});
|
|
465
|
+
let currentStat = null;
|
|
466
|
+
try {
|
|
467
|
+
currentStat = fs.lstatSync(targetPath);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (
|
|
474
|
+
(targetStat === null && currentStat !== null) ||
|
|
475
|
+
(targetStat !== null &&
|
|
476
|
+
(currentStat === null ||
|
|
477
|
+
currentStat.isSymbolicLink() ||
|
|
478
|
+
!currentStat.isFile() ||
|
|
479
|
+
currentStat.dev !== targetStat.dev ||
|
|
480
|
+
currentStat.ino !== targetStat.ino))
|
|
481
|
+
) {
|
|
482
|
+
throw new Error(`Text-file target changed during update: ${targetPath}`);
|
|
483
|
+
}
|
|
484
|
+
fs.renameSync(temporaryPath, targetPath);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
try {
|
|
487
|
+
fs.unlinkSync(temporaryPath);
|
|
488
|
+
} catch {
|
|
489
|
+
// Best effort cleanup for an incomplete atomic replacement.
|
|
490
|
+
}
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function writeTextFile(targetRoot, targetPath, content) {
|
|
496
|
+
writeResolvedTextFile(
|
|
497
|
+
resolveSafeTextTarget(targetRoot, targetPath),
|
|
498
|
+
content,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function upsertTextFile(targetRoot, targetPath, content) {
|
|
503
|
+
const resolvedTarget = resolveSafeTextTarget(targetRoot, targetPath);
|
|
504
|
+
if (fs.existsSync(resolvedTarget)) {
|
|
505
|
+
const existing = fs.readFileSync(resolvedTarget, "utf8");
|
|
506
|
+
if (existing === content) return false;
|
|
507
|
+
}
|
|
508
|
+
writeResolvedTextFile(resolvedTarget, content);
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function escapeRegex(value) {
|
|
513
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function upsertSectionByMarkers(
|
|
517
|
+
targetRoot,
|
|
518
|
+
targetPath,
|
|
519
|
+
startMarker,
|
|
520
|
+
endMarker,
|
|
521
|
+
sectionContent,
|
|
522
|
+
) {
|
|
523
|
+
const resolvedTarget = resolveSafeTextTarget(targetRoot, targetPath);
|
|
524
|
+
const block = `${startMarker}\n${sectionContent.trimEnd()}\n${endMarker}`;
|
|
525
|
+
const existing = fs.existsSync(resolvedTarget)
|
|
526
|
+
? fs.readFileSync(resolvedTarget, "utf8")
|
|
527
|
+
: "";
|
|
528
|
+
const hasMarkers =
|
|
529
|
+
existing.includes(startMarker) && existing.includes(endMarker);
|
|
530
|
+
|
|
531
|
+
if (hasMarkers) {
|
|
532
|
+
const pattern = new RegExp(
|
|
533
|
+
`${escapeRegex(startMarker)}[\\s\\S]*?${escapeRegex(endMarker)}`,
|
|
534
|
+
);
|
|
535
|
+
const replaced = existing.replace(pattern, block);
|
|
536
|
+
if (replaced === existing) return false;
|
|
537
|
+
writeResolvedTextFile(
|
|
538
|
+
resolvedTarget,
|
|
539
|
+
replaced.endsWith("\n") ? replaced : `${replaced}\n`,
|
|
540
|
+
);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let next = existing;
|
|
545
|
+
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
546
|
+
if (next.trim().length > 0 && !next.endsWith("\n\n")) next += "\n";
|
|
547
|
+
next += `${block}\n`;
|
|
548
|
+
writeResolvedTextFile(resolvedTarget, next);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function installClaudeCommands(targetDir) {
|
|
553
|
+
const commandSpecs = [
|
|
554
|
+
{
|
|
555
|
+
file: "context-update.md",
|
|
556
|
+
content: `---
|
|
557
|
+
description: "Refresh Cortex context for changed files"
|
|
558
|
+
---
|
|
559
|
+
Execute: cortex update
|
|
560
|
+
`,
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
const commandsDir = path.join(targetDir, ".claude", "commands");
|
|
565
|
+
let changed = 0;
|
|
566
|
+
for (const spec of commandSpecs) {
|
|
567
|
+
const targetPath = path.join(commandsDir, spec.file);
|
|
568
|
+
if (upsertTextFile(targetDir, targetPath, spec.content)) changed += 1;
|
|
569
|
+
}
|
|
570
|
+
return { total: commandSpecs.length, changed };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function installCodexAgentsSection(targetDir) {
|
|
574
|
+
const agentsPath = path.join(targetDir, "AGENTS.md");
|
|
575
|
+
const startMarker = "<!-- cortex:auto:start -->";
|
|
576
|
+
const endMarker = "<!-- cortex:auto:end -->";
|
|
577
|
+
const section = `## Cortex Auto Workflow
|
|
578
|
+
- Use the \`using-cortex\` skill if available; otherwise follow the commands below.
|
|
579
|
+
- Search before answering code questions: \`cortex search "<query>" --json\`; never answer from memory.
|
|
580
|
+
- Check \`cortex rules --json\` before suggesting changes and \`cortex impact "<query>" --json\` before refactors.
|
|
581
|
+
- Review changed files with \`cortex pattern-evidence <file> --json\` before finalizing.
|
|
582
|
+
- Run \`cortex update\` before completing substantial code changes.
|
|
583
|
+
- If background sync is enabled, check with \`cortex watch status\`.`;
|
|
584
|
+
const changed = upsertSectionByMarkers(
|
|
585
|
+
targetDir,
|
|
586
|
+
agentsPath,
|
|
587
|
+
startMarker,
|
|
588
|
+
endMarker,
|
|
589
|
+
section,
|
|
590
|
+
);
|
|
591
|
+
return { path: agentsPath, changed };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function installAssistantHelpers(targetDir) {
|
|
595
|
+
const claude = installClaudeCommands(targetDir);
|
|
596
|
+
const codex = installCodexAgentsSection(targetDir);
|
|
597
|
+
return { claude, codex };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export async function maybeInstallGitHooks(targetDir) {
|
|
601
|
+
const installScript = path.join(
|
|
602
|
+
targetDir,
|
|
603
|
+
CONTEXT_SCRIPTS_REL,
|
|
604
|
+
"install-git-hooks.sh",
|
|
605
|
+
);
|
|
606
|
+
if (!fs.existsSync(installScript)) return false;
|
|
607
|
+
|
|
608
|
+
const gitRepo = await runCommandResult(
|
|
609
|
+
"git",
|
|
610
|
+
["rev-parse", "--show-toplevel"],
|
|
611
|
+
targetDir,
|
|
612
|
+
"ignore",
|
|
613
|
+
);
|
|
614
|
+
if (!gitRepo.ok) {
|
|
615
|
+
console.log("[cortex] git hooks skipped (not a Git repository)");
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
await runCommand("bash", [installScript], targetDir);
|
|
621
|
+
return true;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.log(
|
|
624
|
+
`[cortex] failed to install git hooks: ${toErrorMessage(error)}`,
|
|
625
|
+
);
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function ensureProjectInitialized(targetDir) {
|
|
631
|
+
const runtimePackageJson = path.join(
|
|
632
|
+
targetDir,
|
|
633
|
+
CONTEXT_RUNTIME_REL,
|
|
634
|
+
"package.json",
|
|
635
|
+
);
|
|
636
|
+
if (!fs.existsSync(runtimePackageJson)) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`Missing ${runtimePackageJson}. Run 'cortex init --bootstrap' first.`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function isTruthyEnv(value) {
|
|
644
|
+
if (typeof value !== "string") return false;
|
|
645
|
+
const normalized = value.trim().toLowerCase();
|
|
646
|
+
return (
|
|
647
|
+
normalized === "1" ||
|
|
648
|
+
normalized === "true" ||
|
|
649
|
+
normalized === "yes" ||
|
|
650
|
+
normalized === "on"
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function canAutoInitialize(targetDir) {
|
|
655
|
+
const scaffoldPaths = [".context", ".githooks"].map((entry) =>
|
|
656
|
+
path.join(targetDir, entry),
|
|
657
|
+
);
|
|
658
|
+
return scaffoldPaths.every((entryPath) => !fs.existsSync(entryPath));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function isScaffoldOutOfDate(targetDir) {
|
|
662
|
+
const contextScript = path.join(
|
|
663
|
+
targetDir,
|
|
664
|
+
CONTEXT_SCRIPTS_REL,
|
|
665
|
+
"context.sh",
|
|
666
|
+
);
|
|
667
|
+
const legacyContextScript = path.join(targetDir, "scripts", "context.sh");
|
|
668
|
+
if (!fs.existsSync(contextScript)) {
|
|
669
|
+
return fs.existsSync(legacyContextScript);
|
|
670
|
+
}
|
|
671
|
+
const doctorScript = path.join(
|
|
672
|
+
targetDir,
|
|
673
|
+
CONTEXT_SCRIPTS_REL,
|
|
674
|
+
"doctor.sh",
|
|
675
|
+
);
|
|
676
|
+
if (!fs.existsSync(doctorScript)) return true;
|
|
677
|
+
if (fs.existsSync(path.join(targetDir, "mcp", "package.json"))) return true;
|
|
678
|
+
const mcpPackage = path.join(targetDir, MCP_PROJECT_REL, "package.json");
|
|
679
|
+
if (!fs.existsSync(mcpPackage)) return true;
|
|
680
|
+
try {
|
|
681
|
+
const contents = fs.readFileSync(contextScript, "utf8");
|
|
682
|
+
if (!/\bdoctor\)\s*\n/.test(contents)) return true;
|
|
683
|
+
} catch {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadProjectCliModule } from "./project-runtime.mjs";
|
|
3
|
+
|
|
4
|
+
export async function runStageCommandShim(args) {
|
|
5
|
+
const target = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
6
|
+
process.env.CORTEX_PROJECT_ROOT = path.resolve(target);
|
|
7
|
+
const mod = await loadProjectCliModule("stage");
|
|
8
|
+
await mod.runStageCommand(args);
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { loadProjectCliModule } from "./project-runtime.mjs";
|
|
2
|
+
|
|
3
|
+
export async function runTelemetryCommand(args) {
|
|
4
|
+
const sub = args[0] || "help";
|
|
5
|
+
if (sub === "test") {
|
|
6
|
+
const mod = await loadProjectCliModule("telemetry-test");
|
|
7
|
+
const code = await mod.runTelemetryTest();
|
|
8
|
+
process.exit(code);
|
|
9
|
+
}
|
|
10
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
11
|
+
console.log("Usage:");
|
|
12
|
+
console.log(
|
|
13
|
+
" cortex telemetry test Smoke-test the push pipeline end-to-end",
|
|
14
|
+
);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Unknown telemetry subcommand: ${sub}`);
|
|
18
|
+
}
|