0xray 4.0.28 → 4.0.30
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/.gitignore.default +1 -0
- package/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/README.md +3 -1
- package/dist/.gitignore.default +1 -0
- package/dist/CHANGELOG.md +16 -0
- package/dist/README.md +3 -1
- package/dist/cli/index.js +52 -44
- package/dist/integrations/cursor/README.md +21 -2
- package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +61 -4
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/llms.txt +2 -0
- package/package.json +2 -2
- package/scripts/node/bridge-mcp-wiring.cjs +2 -1
- package/scripts/node/consumer-gitignore.cjs +1 -0
- package/scripts/node/consumer-install-smoke.mjs +9 -72
- package/scripts/node/install-bridges.cjs +1123 -14
- package/scripts/node/postinstall.cjs +6 -59
- package/scripts/node/setup.cjs +80 -71
- package/scripts/node/unwear-cursor-hooks.cjs +21 -0
- package/scripts/node/wear-cursor-hooks.cjs +17 -0
- package/scripts/node/wear-vendored-repertoire.cjs +35 -0
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/xray/features.json +1 -1
|
@@ -189,7 +189,7 @@ function writePluginMcpJson(pluginDir, targetDir, log, label) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
function deployProjectMcpJson(targetDir, log) {
|
|
192
|
-
deployPortableProjectMcpJson(targetDir);
|
|
192
|
+
if (!deployPortableProjectMcpJson(targetDir)) return;
|
|
193
193
|
log("mcp-config", "project .mcp.json deployed (7 xray servers, portable)", "info");
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -732,6 +732,19 @@ const CURSOR_HOOK_SCRIPTS = [
|
|
|
732
732
|
"before-shell-execution.sh",
|
|
733
733
|
];
|
|
734
734
|
|
|
735
|
+
/** Five Cursor hook events 0xray ships. preCompact is one of them.
|
|
736
|
+
* Dogfood `.cursor/hooks` also holds the shared runner and invoke-probe.sh;
|
|
737
|
+
* those are not hook events. */
|
|
738
|
+
const CURSOR_HOOK_EVENTS = [
|
|
739
|
+
["preToolUse", "pre-tool-use.sh"],
|
|
740
|
+
["preCompact", "pre-compact.sh"],
|
|
741
|
+
["afterFileEdit", "after-file-edit.sh"],
|
|
742
|
+
["beforeShellExecution", "before-shell-execution.sh"],
|
|
743
|
+
["beforeReadFile", "before-read-file.sh"],
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
const CURSOR_WEAR_BACKUP = "hooks.json.xray-before";
|
|
747
|
+
|
|
735
748
|
const CLOUD_SAFE_CURSOR_HOOKS = {
|
|
736
749
|
version: 1,
|
|
737
750
|
hooks: {
|
|
@@ -860,8 +873,8 @@ function fastenCursorHooksAtUnsafe(targetDir, packageRoot, log) {
|
|
|
860
873
|
if (fs.existsSync(dest)) {
|
|
861
874
|
try {
|
|
862
875
|
existing = JSON.parse(fs.readFileSync(dest, "utf8"));
|
|
863
|
-
} catch {
|
|
864
|
-
|
|
876
|
+
} catch (err) {
|
|
877
|
+
throw new Error(`cursor-wear: refusing to edit ${dest}: ${err.message}. Wrote nothing.`);
|
|
865
878
|
}
|
|
866
879
|
}
|
|
867
880
|
const template = resolveCursorHooksTemplate(packageRoot);
|
|
@@ -870,8 +883,13 @@ function fastenCursorHooksAtUnsafe(targetDir, packageRoot, log) {
|
|
|
870
883
|
return null;
|
|
871
884
|
}
|
|
872
885
|
const next = mergeCloudSafeCursorHooks(existing);
|
|
873
|
-
|
|
874
|
-
fs.
|
|
886
|
+
const body = `${JSON.stringify(next, null, 2)}\n`;
|
|
887
|
+
const previousText = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : null;
|
|
888
|
+
if (previousText !== body) {
|
|
889
|
+
saveCursorWearSnapshot(targetDir, previousText, body);
|
|
890
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
891
|
+
fs.writeFileSync(dest, body);
|
|
892
|
+
}
|
|
875
893
|
log("cursor-bridge", existing ? "hooks.json rewritten cloud-safe" : "hooks.json fastened", "info", {
|
|
876
894
|
path: dest,
|
|
877
895
|
scripts: copied,
|
|
@@ -978,27 +996,1086 @@ function reloadCursorHostHooks(log) {
|
|
|
978
996
|
}
|
|
979
997
|
}
|
|
980
998
|
|
|
999
|
+
function consumerDistHooksReady(packageRoot) {
|
|
1000
|
+
const dir = path.join(packageRoot, "dist", "integrations", "cursor", "hooks");
|
|
1001
|
+
if (!fs.existsSync(path.join(dir, "xray-cloud-hook.sh"))) return false;
|
|
1002
|
+
return CURSOR_HOOK_EVENTS.every(([, script]) => fs.existsSync(path.join(dir, script)));
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function installedHookCommand(fromDir, packageRoot, scriptName) {
|
|
1006
|
+
const abs = path.join(packageRoot, "dist", "integrations", "cursor", "hooks", scriptName);
|
|
1007
|
+
return path.relative(fromDir, abs).split(path.sep).join("/");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function installedCursorHookEntries(fromDir, packageRoot) {
|
|
1011
|
+
const hooks = {};
|
|
1012
|
+
for (const [event, script] of CURSOR_HOOK_EVENTS) {
|
|
1013
|
+
hooks[event] = [{ command: installedHookCommand(fromDir, packageRoot, script) }];
|
|
1014
|
+
}
|
|
1015
|
+
return hooks;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Ours only when the command points at the installed dist hook.
|
|
1020
|
+
* Legacy one-liners count only when they launch that same dist `.js`.
|
|
1021
|
+
* A user's `.cursor/hooks/pre-compact.sh` is not ours.
|
|
1022
|
+
*/
|
|
1023
|
+
function isXrayHookCommand(command) {
|
|
1024
|
+
const cmd = String(command || "").trim();
|
|
1025
|
+
if (!cmd) return false;
|
|
1026
|
+
if (/(?:^|[/\\])node_modules\/0xray\/dist\/integrations\/cursor\/hooks\/[\w.-]+\.sh$/.test(cmd)) {
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
return (
|
|
1030
|
+
/XRAY_AI_PATH=/.test(cmd) &&
|
|
1031
|
+
/node_modules\/0xray\/dist\/integrations\/cursor\/hooks\/[\w.-]+\.js/.test(cmd)
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function shellWithoutComments(text) {
|
|
1036
|
+
return String(text)
|
|
1037
|
+
.split("\n")
|
|
1038
|
+
.map((line) => {
|
|
1039
|
+
if (/^\s*#/.test(line)) return "";
|
|
1040
|
+
const hash = line.indexOf("#");
|
|
1041
|
+
return hash === -1 ? line : line.slice(0, hash);
|
|
1042
|
+
})
|
|
1043
|
+
.join("\n");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function commandAlreadyRunsXrayHook(command, targetDir) {
|
|
1047
|
+
if (isXrayHookCommand(command)) return true;
|
|
1048
|
+
const match = /^(?:\.\/)?\.cursor\/hooks\/([\w.-]+\.sh)$/.exec(String(command || "").trim());
|
|
1049
|
+
if (!match || !targetDir) return false;
|
|
1050
|
+
const abs = path.join(path.resolve(targetDir), ".cursor", "hooks", match[1]);
|
|
1051
|
+
try {
|
|
1052
|
+
return shellWithoutComments(fs.readFileSync(abs, "utf8")).includes("xray-cloud-hook.sh");
|
|
1053
|
+
} catch {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function scanJsonc(text) {
|
|
1059
|
+
let hasComments = false;
|
|
1060
|
+
let i = 0;
|
|
1061
|
+
let inString = false;
|
|
1062
|
+
let escape = false;
|
|
1063
|
+
while (i < text.length) {
|
|
1064
|
+
const c = text[i];
|
|
1065
|
+
const n = text[i + 1];
|
|
1066
|
+
if (inString) {
|
|
1067
|
+
if (escape) escape = false;
|
|
1068
|
+
else if (c === "\\") escape = true;
|
|
1069
|
+
else if (c === '"') inString = false;
|
|
1070
|
+
i += 1;
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
if (c === '"') {
|
|
1074
|
+
inString = true;
|
|
1075
|
+
i += 1;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (c === "/" && n === "/") {
|
|
1079
|
+
hasComments = true;
|
|
1080
|
+
i += 2;
|
|
1081
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (c === "/" && n === "*") {
|
|
1085
|
+
hasComments = true;
|
|
1086
|
+
const end = text.indexOf("*/", i + 2);
|
|
1087
|
+
if (end < 0) return { ok: false, reason: "unclosed block comment", hasComments: true };
|
|
1088
|
+
i = end + 2;
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
i += 1;
|
|
1092
|
+
}
|
|
1093
|
+
if (inString) return { ok: false, reason: "unclosed string", hasComments };
|
|
1094
|
+
return { ok: true, hasComments };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function stripJsonComments(text) {
|
|
1098
|
+
let out = "";
|
|
1099
|
+
let i = 0;
|
|
1100
|
+
let inString = false;
|
|
1101
|
+
let escape = false;
|
|
1102
|
+
while (i < text.length) {
|
|
1103
|
+
const c = text[i];
|
|
1104
|
+
const n = text[i + 1];
|
|
1105
|
+
if (inString) {
|
|
1106
|
+
out += c;
|
|
1107
|
+
if (escape) escape = false;
|
|
1108
|
+
else if (c === "\\") escape = true;
|
|
1109
|
+
else if (c === '"') inString = false;
|
|
1110
|
+
i += 1;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (c === '"') {
|
|
1114
|
+
inString = true;
|
|
1115
|
+
out += c;
|
|
1116
|
+
i += 1;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (c === "/" && n === "/") {
|
|
1120
|
+
i += 2;
|
|
1121
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
1122
|
+
out += " ";
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (c === "/" && n === "*") {
|
|
1126
|
+
const end = text.indexOf("*/", i + 2);
|
|
1127
|
+
if (end < 0) break;
|
|
1128
|
+
i = end + 2;
|
|
1129
|
+
out += " ";
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
out += c;
|
|
1133
|
+
i += 1;
|
|
1134
|
+
}
|
|
1135
|
+
return out;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function refuseHooksEdit(hooksPath, reason) {
|
|
1139
|
+
throw new Error(`cursor-wear: refusing to edit ${hooksPath}: ${reason}. Wrote nothing.`);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function assertUniqueJsonKeys(text) {
|
|
1143
|
+
const tokens = tokenizeJsonc(text);
|
|
1144
|
+
const visit = (idx) => {
|
|
1145
|
+
const token = tokens[idx];
|
|
1146
|
+
if (!token) return;
|
|
1147
|
+
if (token.type === "{") {
|
|
1148
|
+
const obj = objectPairs(tokens, idx);
|
|
1149
|
+
for (const pair of obj.pairs) visit(pair.valIdx);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (token.type === "[") {
|
|
1153
|
+
const close = matchingBracket(tokens, idx);
|
|
1154
|
+
for (const el of arrayElements(tokens, idx, close)) visit(el.startIdx);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
if (tokens.length) visit(0);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function parseHooksText(text, hooksPath) {
|
|
1161
|
+
const scan = scanJsonc(text);
|
|
1162
|
+
if (!scan.ok) refuseHooksEdit(hooksPath, scan.reason);
|
|
1163
|
+
try {
|
|
1164
|
+
assertUniqueJsonKeys(text);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
refuseHooksEdit(hooksPath, err.message || String(err));
|
|
1167
|
+
}
|
|
1168
|
+
let parsed;
|
|
1169
|
+
try {
|
|
1170
|
+
parsed = JSON.parse(stripJsonComments(text));
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
refuseHooksEdit(hooksPath, `hooks.json is not valid JSONC (${err.message})`);
|
|
1173
|
+
}
|
|
1174
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1175
|
+
refuseHooksEdit(hooksPath, "hooks.json must be a JSON object");
|
|
1176
|
+
}
|
|
1177
|
+
return { parsed, hasComments: scan.hasComments };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function tokenizeJsonc(text) {
|
|
1181
|
+
const tokens = [];
|
|
1182
|
+
let i = 0;
|
|
1183
|
+
while (i < text.length) {
|
|
1184
|
+
const c = text[i];
|
|
1185
|
+
if (/\s/.test(c)) {
|
|
1186
|
+
i += 1;
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
1190
|
+
i += 2;
|
|
1191
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
1195
|
+
const end = text.indexOf("*/", i + 2);
|
|
1196
|
+
if (end < 0) throw new Error("unclosed block comment");
|
|
1197
|
+
i = end + 2;
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
if (c === '"') {
|
|
1201
|
+
const start = i;
|
|
1202
|
+
i += 1;
|
|
1203
|
+
let escape = false;
|
|
1204
|
+
while (i < text.length) {
|
|
1205
|
+
if (escape) {
|
|
1206
|
+
escape = false;
|
|
1207
|
+
i += 1;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (text[i] === "\\") {
|
|
1211
|
+
escape = true;
|
|
1212
|
+
i += 1;
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
if (text[i] === '"') {
|
|
1216
|
+
i += 1;
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
i += 1;
|
|
1220
|
+
}
|
|
1221
|
+
tokens.push({ type: "string", start, end: i, value: JSON.parse(text.slice(start, i)) });
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
if ("{}[],:".includes(c)) {
|
|
1225
|
+
tokens.push({ type: c, start: i, end: i + 1 });
|
|
1226
|
+
i += 1;
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const atom = /^(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/.exec(text.slice(i));
|
|
1230
|
+
if (atom) {
|
|
1231
|
+
tokens.push({ type: "atom", start: i, end: i + atom[0].length, value: atom[0] });
|
|
1232
|
+
i += atom[0].length;
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
throw new Error(`unexpected token at ${i}`);
|
|
1236
|
+
}
|
|
1237
|
+
return tokens;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function matchingBracket(tokens, openIdx) {
|
|
1241
|
+
const open = tokens[openIdx] && tokens[openIdx].type;
|
|
1242
|
+
if (open !== "{" && open !== "[") return -1;
|
|
1243
|
+
const close = open === "{" ? "}" : "]";
|
|
1244
|
+
let depth = 0;
|
|
1245
|
+
for (let i = openIdx; i < tokens.length; i += 1) {
|
|
1246
|
+
const type = tokens[i].type;
|
|
1247
|
+
if (type === "{" || type === "[") depth += 1;
|
|
1248
|
+
else if (type === "}" || type === "]") {
|
|
1249
|
+
depth -= 1;
|
|
1250
|
+
if (depth === 0) return type === close ? i : -1;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return -1;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function objectPairs(tokens, openIdx) {
|
|
1257
|
+
const closeIdx = matchingBracket(tokens, openIdx);
|
|
1258
|
+
if (closeIdx < 0) throw new Error("unbalanced braces");
|
|
1259
|
+
const pairs = [];
|
|
1260
|
+
let i = openIdx + 1;
|
|
1261
|
+
while (i < closeIdx) {
|
|
1262
|
+
const keyTok = tokens[i];
|
|
1263
|
+
if (!keyTok || keyTok.type !== "string" || !tokens[i + 1] || tokens[i + 1].type !== ":") {
|
|
1264
|
+
throw new Error("unexpected object entry");
|
|
1265
|
+
}
|
|
1266
|
+
const valIdx = i + 2;
|
|
1267
|
+
const val = tokens[valIdx];
|
|
1268
|
+
if (!val) throw new Error("missing value");
|
|
1269
|
+
let valEnd = valIdx;
|
|
1270
|
+
if (val.type === "{" || val.type === "[") {
|
|
1271
|
+
valEnd = matchingBracket(tokens, valIdx);
|
|
1272
|
+
if (valEnd < 0) throw new Error("unbalanced braces");
|
|
1273
|
+
}
|
|
1274
|
+
if (pairs.some((pair) => pair.key === keyTok.value)) {
|
|
1275
|
+
throw new Error(`duplicate key ${JSON.stringify(keyTok.value)}`);
|
|
1276
|
+
}
|
|
1277
|
+
pairs.push({ key: keyTok.value, keyIdx: i, valIdx, valEnd });
|
|
1278
|
+
i = valEnd + 1;
|
|
1279
|
+
if (i < closeIdx && tokens[i] && tokens[i].type === ",") i += 1;
|
|
1280
|
+
}
|
|
1281
|
+
return { closeIdx, pairs };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function arrayElements(tokens, openIdx, closeIdx) {
|
|
1285
|
+
const elements = [];
|
|
1286
|
+
let i = openIdx + 1;
|
|
1287
|
+
while (i < closeIdx) {
|
|
1288
|
+
const token = tokens[i];
|
|
1289
|
+
if (!token || token.type === ",") {
|
|
1290
|
+
i += 1;
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
let end = i;
|
|
1294
|
+
if (token.type === "{" || token.type === "[") {
|
|
1295
|
+
end = matchingBracket(tokens, i);
|
|
1296
|
+
if (end < 0) throw new Error("unbalanced braces");
|
|
1297
|
+
}
|
|
1298
|
+
elements.push({ startIdx: i, endIdx: end });
|
|
1299
|
+
i = end + 1;
|
|
1300
|
+
}
|
|
1301
|
+
return elements;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function commandOfObject(tokens, startIdx) {
|
|
1305
|
+
const obj = objectPairs(tokens, startIdx);
|
|
1306
|
+
const cmd = obj.pairs.find((pair) => pair.key === "command");
|
|
1307
|
+
if (!cmd) return null;
|
|
1308
|
+
const val = tokens[cmd.valIdx];
|
|
1309
|
+
if (!val || val.type !== "string") return null;
|
|
1310
|
+
return { value: val.value, token: val };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function applyTextEdits(text, edits) {
|
|
1314
|
+
const ordered = edits.slice().sort((a, b) => b.start - a.start || b.end - a.end);
|
|
1315
|
+
let out = text;
|
|
1316
|
+
for (const edit of ordered) {
|
|
1317
|
+
out = out.slice(0, edit.start) + edit.insert + out.slice(edit.end);
|
|
1318
|
+
}
|
|
1319
|
+
return out;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function ownedDeletionEdits(tokens, elements) {
|
|
1323
|
+
const claimed = new Set();
|
|
1324
|
+
const edits = [];
|
|
1325
|
+
for (const el of elements) {
|
|
1326
|
+
const startTok = tokens[el.startIdx];
|
|
1327
|
+
const endTok = tokens[el.endIdx];
|
|
1328
|
+
let start = startTok.start;
|
|
1329
|
+
let end = endTok.end;
|
|
1330
|
+
const next = tokens[el.endIdx + 1];
|
|
1331
|
+
const prev = tokens[el.startIdx - 1];
|
|
1332
|
+
if (next && next.type === "," && !claimed.has(next.start)) {
|
|
1333
|
+
claimed.add(next.start);
|
|
1334
|
+
end = next.end;
|
|
1335
|
+
} else if (prev && prev.type === "," && !claimed.has(prev.start)) {
|
|
1336
|
+
claimed.add(prev.start);
|
|
1337
|
+
start = prev.start;
|
|
1338
|
+
}
|
|
1339
|
+
edits.push({ start, end, insert: "" });
|
|
1340
|
+
}
|
|
1341
|
+
return edits;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function renderEventEntry(command) {
|
|
1345
|
+
return `{ "command": ${JSON.stringify(command)} }`;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function editJsoncHooks(text, xrayByEvent, targetDir) {
|
|
1349
|
+
let tokens;
|
|
1350
|
+
try {
|
|
1351
|
+
tokens = tokenizeJsonc(text);
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
throw new Error(err.message || String(err));
|
|
1354
|
+
}
|
|
1355
|
+
if (!tokens.length || tokens[0].type !== "{") throw new Error("hooks.json must be a JSON object");
|
|
1356
|
+
const root = objectPairs(tokens, 0);
|
|
1357
|
+
const edits = [];
|
|
1358
|
+
const hooksPair = root.pairs.find((pair) => pair.key === "hooks");
|
|
1359
|
+
if (!hooksPair) {
|
|
1360
|
+
const inner = Object.entries(xrayByEvent)
|
|
1361
|
+
.map(([event, entries]) => `"${event}": [${renderEventEntry(entries[0].command)}]`)
|
|
1362
|
+
.join(", ");
|
|
1363
|
+
const needsComma = root.pairs.length > 0;
|
|
1364
|
+
edits.push({
|
|
1365
|
+
start: tokens[root.closeIdx].start,
|
|
1366
|
+
end: tokens[root.closeIdx].start,
|
|
1367
|
+
insert: `${needsComma ? ", " : ""}"hooks": { ${inner} }`,
|
|
1368
|
+
});
|
|
1369
|
+
return applyTextEdits(text, edits);
|
|
1370
|
+
}
|
|
1371
|
+
if (tokens[hooksPair.valIdx].type !== "{") throw new Error("hooks is not an object");
|
|
1372
|
+
const hooksObj = objectPairs(tokens, hooksPair.valIdx);
|
|
1373
|
+
const missing = [];
|
|
1374
|
+
for (const [event, entries] of Object.entries(xrayByEvent)) {
|
|
1375
|
+
const command = entries[0].command;
|
|
1376
|
+
const pair = hooksObj.pairs.find((item) => item.key === event);
|
|
1377
|
+
if (!pair) {
|
|
1378
|
+
missing.push([event, command]);
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
if (tokens[pair.valIdx].type !== "[") throw new Error(`${event} is not an array`);
|
|
1382
|
+
const elements = arrayElements(tokens, pair.valIdx, pair.valEnd);
|
|
1383
|
+
const owned = [];
|
|
1384
|
+
let coveredByRunner = false;
|
|
1385
|
+
for (const el of elements) {
|
|
1386
|
+
if (tokens[el.startIdx].type !== "{") continue;
|
|
1387
|
+
const cmd = commandOfObject(tokens, el.startIdx);
|
|
1388
|
+
if (!cmd) continue;
|
|
1389
|
+
if (isXrayHookCommand(cmd.value)) owned.push({ el, cmd });
|
|
1390
|
+
else if (commandAlreadyRunsXrayHook(cmd.value, targetDir)) coveredByRunner = true;
|
|
1391
|
+
}
|
|
1392
|
+
if (coveredByRunner) {
|
|
1393
|
+
if (owned.length > 0) edits.push(...ownedDeletionEdits(tokens, owned.map((item) => item.el)));
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (owned.length === 0) {
|
|
1397
|
+
edits.push({
|
|
1398
|
+
start: tokens[pair.valEnd].start,
|
|
1399
|
+
end: tokens[pair.valEnd].start,
|
|
1400
|
+
insert: `${elements.length > 0 ? ", " : ""}${renderEventEntry(command)}`,
|
|
1401
|
+
});
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
const first = owned[0];
|
|
1405
|
+
if (first.cmd.value !== command) {
|
|
1406
|
+
edits.push({
|
|
1407
|
+
start: first.cmd.token.start,
|
|
1408
|
+
end: first.cmd.token.end,
|
|
1409
|
+
insert: JSON.stringify(command),
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
if (owned.length > 1) {
|
|
1413
|
+
edits.push(...ownedDeletionEdits(tokens, owned.slice(1).map((item) => item.el)));
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (missing.length > 0) {
|
|
1417
|
+
const rendered = missing
|
|
1418
|
+
.map(([event, command]) => `"${event}": [${renderEventEntry(command)}]`)
|
|
1419
|
+
.join(", ");
|
|
1420
|
+
edits.push({
|
|
1421
|
+
start: tokens[hooksObj.closeIdx].start,
|
|
1422
|
+
end: tokens[hooksObj.closeIdx].start,
|
|
1423
|
+
insert: `${hooksObj.pairs.length > 0 ? ", " : ""}${rendered}`,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
return applyTextEdits(text, edits);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function renderWornHooks(originalText, hooksPath, xrayByEvent, targetDir) {
|
|
1430
|
+
parseHooksText(originalText, hooksPath);
|
|
1431
|
+
try {
|
|
1432
|
+
return editJsoncHooks(originalText, xrayByEvent, targetDir);
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
refuseHooksEdit(hooksPath, err.message || String(err));
|
|
1435
|
+
}
|
|
1436
|
+
return "";
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function stripOwnedHookEntries(text) {
|
|
1440
|
+
const tokens = tokenizeJsonc(text);
|
|
1441
|
+
if (!tokens.length || tokens[0].type !== "{") return text;
|
|
1442
|
+
const root = objectPairs(tokens, 0);
|
|
1443
|
+
const hooksPair = root.pairs.find((pair) => pair.key === "hooks");
|
|
1444
|
+
if (!hooksPair || tokens[hooksPair.valIdx].type !== "{") return text;
|
|
1445
|
+
const hooksObj = objectPairs(tokens, hooksPair.valIdx);
|
|
1446
|
+
const owned = [];
|
|
1447
|
+
for (const pair of hooksObj.pairs) {
|
|
1448
|
+
if (tokens[pair.valIdx].type !== "[") continue;
|
|
1449
|
+
const elements = arrayElements(tokens, pair.valIdx, pair.valEnd);
|
|
1450
|
+
for (const el of elements) {
|
|
1451
|
+
if (tokens[el.startIdx].type !== "{") continue;
|
|
1452
|
+
const cmd = commandOfObject(tokens, el.startIdx);
|
|
1453
|
+
if (cmd && isXrayHookCommand(cmd.value)) owned.push(el);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return stripDanglingCommas(stripEmptyShippedEvents(applyTextEdits(text, ownedDeletionEdits(tokens, owned))));
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function stripDanglingCommas(text) {
|
|
1460
|
+
const tokens = tokenizeJsonc(text);
|
|
1461
|
+
const edits = [];
|
|
1462
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1463
|
+
if (tokens[i].type !== ",") continue;
|
|
1464
|
+
const next = tokens[i + 1];
|
|
1465
|
+
if (next && (next.type === "}" || next.type === "]")) {
|
|
1466
|
+
edits.push({ start: tokens[i].start, end: next.start, insert: "" });
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return applyTextEdits(text, edits);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function stripEmptyShippedEvents(text) {
|
|
1473
|
+
const shipped = new Set(CURSOR_HOOK_EVENTS.map(([event]) => event));
|
|
1474
|
+
const tokens = tokenizeJsonc(text);
|
|
1475
|
+
if (!tokens.length || tokens[0].type !== "{") return text;
|
|
1476
|
+
const root = objectPairs(tokens, 0);
|
|
1477
|
+
const hooksPair = root.pairs.find((pair) => pair.key === "hooks");
|
|
1478
|
+
if (!hooksPair || tokens[hooksPair.valIdx].type !== "{") return text;
|
|
1479
|
+
const hooksObj = objectPairs(tokens, hooksPair.valIdx);
|
|
1480
|
+
const empty = [];
|
|
1481
|
+
for (const pair of hooksObj.pairs) {
|
|
1482
|
+
if (!shipped.has(pair.key) || tokens[pair.valIdx].type !== "[") continue;
|
|
1483
|
+
if (arrayElements(tokens, pair.valIdx, pair.valEnd).length === 0) {
|
|
1484
|
+
empty.push({ startIdx: pair.keyIdx, endIdx: pair.valEnd });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return applyTextEdits(text, ownedDeletionEdits(tokens, empty));
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Keep every non-0xray entry. Add or replace only 0xray's shipped commands.
|
|
1492
|
+
* A second pass on the result is stable (same keys, same commands).
|
|
1493
|
+
*/
|
|
1494
|
+
function mergeInstalledCursorHooks(existing, xrayByEvent) {
|
|
1495
|
+
const sourceHooks =
|
|
1496
|
+
existing && existing.hooks && typeof existing.hooks === "object" && !Array.isArray(existing.hooks)
|
|
1497
|
+
? existing.hooks
|
|
1498
|
+
: {};
|
|
1499
|
+
const names = Object.keys(sourceHooks);
|
|
1500
|
+
for (const name of Object.keys(xrayByEvent)) {
|
|
1501
|
+
if (!names.includes(name)) names.push(name);
|
|
1502
|
+
}
|
|
1503
|
+
const hooks = {};
|
|
1504
|
+
for (const name of names) {
|
|
1505
|
+
const current = Array.isArray(sourceHooks[name]) ? sourceHooks[name] : [];
|
|
1506
|
+
const shipped = xrayByEvent[name];
|
|
1507
|
+
if (!shipped) {
|
|
1508
|
+
hooks[name] = current.slice();
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const command = shipped[0].command;
|
|
1512
|
+
let replaced = false;
|
|
1513
|
+
const next = [];
|
|
1514
|
+
for (const entry of current) {
|
|
1515
|
+
const cmd = entry && typeof entry === "object" ? entry.command : "";
|
|
1516
|
+
if (isXrayHookCommand(cmd)) {
|
|
1517
|
+
if (!replaced) {
|
|
1518
|
+
next.push({ ...entry, command });
|
|
1519
|
+
replaced = true;
|
|
1520
|
+
}
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
next.push(entry);
|
|
1524
|
+
}
|
|
1525
|
+
if (!replaced) next.push({ command });
|
|
1526
|
+
hooks[name] = next;
|
|
1527
|
+
}
|
|
1528
|
+
const doc = {};
|
|
1529
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
1530
|
+
for (const key of Object.keys(existing)) {
|
|
1531
|
+
if (key !== "hooks") doc[key] = existing[key];
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (doc.version == null) doc.version = 1;
|
|
1535
|
+
doc.hooks = hooks;
|
|
1536
|
+
return doc;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function serializeHooksDoc(doc) {
|
|
1540
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function wearStateDir(targetDir) {
|
|
1544
|
+
return path.join(path.resolve(targetDir), ".xray", "state", "cursor-hook-wear");
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* The snapshot lives in the project, outside node_modules. Append its path to
|
|
1549
|
+
* $GIT_DIR/info/exclude when nothing already ignores it. exclude is local git
|
|
1550
|
+
* metadata, so a fresh repo's `git status` does not grow a new `.gitignore`.
|
|
1551
|
+
*/
|
|
1552
|
+
function ensureWearStateGitignored(targetDir) {
|
|
1553
|
+
const stateDir = wearStateDir(targetDir);
|
|
1554
|
+
const probe = path.join(stateDir, "meta.json");
|
|
1555
|
+
try {
|
|
1556
|
+
execFileSync("git", ["check-ignore", "-q", "--", probe], {
|
|
1557
|
+
cwd: targetDir,
|
|
1558
|
+
stdio: "ignore",
|
|
1559
|
+
});
|
|
1560
|
+
return;
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
const notARepo = !err || err.code === "ENOENT" || err.status === 128;
|
|
1563
|
+
const notIgnored = Boolean(err && err.status === 1);
|
|
1564
|
+
if (notARepo || !notIgnored) return;
|
|
1565
|
+
}
|
|
1566
|
+
let top = "";
|
|
1567
|
+
let excludeRel = "";
|
|
1568
|
+
try {
|
|
1569
|
+
top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1570
|
+
cwd: targetDir,
|
|
1571
|
+
encoding: "utf8",
|
|
1572
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1573
|
+
}).trim();
|
|
1574
|
+
excludeRel = execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], {
|
|
1575
|
+
cwd: targetDir,
|
|
1576
|
+
encoding: "utf8",
|
|
1577
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1578
|
+
}).trim();
|
|
1579
|
+
} catch {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (!top || !excludeRel) return;
|
|
1583
|
+
const rel = path.relative(top, stateDir).split(path.sep).join("/");
|
|
1584
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return;
|
|
1585
|
+
const pattern = `${rel}/`;
|
|
1586
|
+
const excludeFile = path.resolve(targetDir, excludeRel);
|
|
1587
|
+
try {
|
|
1588
|
+
fs.mkdirSync(path.dirname(excludeFile), { recursive: true });
|
|
1589
|
+
const existing = fs.existsSync(excludeFile) ? fs.readFileSync(excludeFile, "utf8") : "";
|
|
1590
|
+
if (existing.split("\n").some((line) => line.trim() === pattern)) return;
|
|
1591
|
+
const suffix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
1592
|
+
fs.appendFileSync(excludeFile, `${suffix}${pattern}\n`);
|
|
1593
|
+
} catch {
|
|
1594
|
+
// exclude not writable — snapshot is still under .xray/state/
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function wearStatePaths(targetDir) {
|
|
1599
|
+
const dir = wearStateDir(targetDir);
|
|
1600
|
+
return {
|
|
1601
|
+
dir,
|
|
1602
|
+
meta: path.join(dir, "meta.json"),
|
|
1603
|
+
backup: path.join(dir, CURSOR_WEAR_BACKUP),
|
|
1604
|
+
written: path.join(dir, "hooks.json.written"),
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function textHasDistHookEntries(text) {
|
|
1609
|
+
return /node_modules\/0xray\/dist\/integrations\/cursor\/hooks\/[\w.-]+\.sh/.test(String(text || ""));
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function userBytesBeforeWear(previousText) {
|
|
1613
|
+
if (previousText == null) return null;
|
|
1614
|
+
if (!textHasDistHookEntries(previousText)) return previousText;
|
|
1615
|
+
return stripOwnedHookEntries(previousText);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function classifyCursorWearGit(targetDir) {
|
|
1619
|
+
try {
|
|
1620
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
1621
|
+
} catch {
|
|
1622
|
+
return "missing";
|
|
1623
|
+
}
|
|
1624
|
+
let bare = "";
|
|
1625
|
+
try {
|
|
1626
|
+
bare = execFileSync("git", ["rev-parse", "--is-bare-repository"], {
|
|
1627
|
+
cwd: targetDir,
|
|
1628
|
+
encoding: "utf8",
|
|
1629
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1630
|
+
}).trim();
|
|
1631
|
+
} catch {
|
|
1632
|
+
return "not-work-tree";
|
|
1633
|
+
}
|
|
1634
|
+
if (bare === "true") return "bare";
|
|
1635
|
+
let inside = "";
|
|
1636
|
+
try {
|
|
1637
|
+
inside = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1638
|
+
cwd: targetDir,
|
|
1639
|
+
encoding: "utf8",
|
|
1640
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1641
|
+
}).trim();
|
|
1642
|
+
} catch {
|
|
1643
|
+
return "not-work-tree";
|
|
1644
|
+
}
|
|
1645
|
+
if (inside !== "true") return "not-work-tree";
|
|
1646
|
+
return "ok";
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function assertCursorWearGit(targetDir) {
|
|
1650
|
+
const kind = classifyCursorWearGit(targetDir);
|
|
1651
|
+
if (kind === "missing") throw new Error("cursor-wear: git is missing");
|
|
1652
|
+
if (kind === "bare") throw new Error("cursor-wear: bare repository");
|
|
1653
|
+
if (kind === "not-work-tree") throw new Error("cursor-wear: not a git work tree");
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const CURSOR_HOOKS_SKIPPED_LINE =
|
|
1657
|
+
"cursor-wear: hooks skipped because this folder is not a git checkout\n";
|
|
1658
|
+
|
|
1659
|
+
function syncSetupSkillsAndRootLinks(packageRoot, targetDir) {
|
|
1660
|
+
require("./setup.cjs").syncSetupSkillsAndRootLinks(packageRoot, targetDir);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function setupSkillMirrorNames(packageRoot) {
|
|
1664
|
+
const skillsSource = fs.existsSync(path.join(packageRoot, "src", "skills"))
|
|
1665
|
+
? path.join(packageRoot, "src", "skills")
|
|
1666
|
+
: path.join(packageRoot, "dist", "skills");
|
|
1667
|
+
if (!fs.existsSync(skillsSource)) return new Set();
|
|
1668
|
+
const names = new Set();
|
|
1669
|
+
for (const entry of fs.readdirSync(skillsSource, { withFileTypes: true })) {
|
|
1670
|
+
if (!entry.isDirectory()) continue;
|
|
1671
|
+
if (fs.existsSync(path.join(skillsSource, entry.name, "SKILL.md"))) names.add(entry.name);
|
|
1672
|
+
}
|
|
1673
|
+
return names;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/** The 4.0.28 skill mirror is not a consumer costume. Any other extra name still throws. */
|
|
1677
|
+
function isSetupSkillMirrorDump(err, packageRoot) {
|
|
1678
|
+
if (!err || err.code !== "FOUNDRY_COSTUME_DUMP") return false;
|
|
1679
|
+
if (!Array.isArray(err.extraSkills) || err.extraSkills.length === 0) return false;
|
|
1680
|
+
if (Array.isArray(err.extraAgents) && err.extraAgents.length > 0) return false;
|
|
1681
|
+
const mirrored = setupSkillMirrorNames(packageRoot);
|
|
1682
|
+
return err.extraSkills.every((name) => mirrored.has(name));
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function packageShipsConsumerSuit(packageRoot) {
|
|
1686
|
+
return fs.existsSync(path.join(packageRoot, "AGENTS-consumer.md"));
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* 4.0.28 consumer postinstall files: AGENTS.md, .gitignore, repertoire link,
|
|
1691
|
+
* .xray config, bridges, mill plant, then the setup.cjs skill mirror and
|
|
1692
|
+
* dist/scripts links. Does not install Cursor hooks or git hooks.
|
|
1693
|
+
* Existing user files follow the 4.0.28 guards inside those functions.
|
|
1694
|
+
*/
|
|
1695
|
+
function installConsumerProjectFiles(packageRoot, targetDir, log) {
|
|
1696
|
+
const write = typeof log === "function" ? log : () => {};
|
|
1697
|
+
if (!isConsumerInstall(packageRoot, targetDir) || !packageShipsConsumerSuit(packageRoot)) {
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
const { deployManagedAgents } = require("./postinstall.cjs");
|
|
1701
|
+
const { applyConsumerGitignore } = require("./consumer-gitignore.cjs");
|
|
1702
|
+
const { mintConsumerSuit } = require("../foundry/mint-suit.cjs");
|
|
1703
|
+
deployManagedAgents(packageRoot, targetDir, write);
|
|
1704
|
+
applyConsumerGitignore(targetDir, packageRoot);
|
|
1705
|
+
wearVendoredRepertoire(packageRoot, targetDir, write);
|
|
1706
|
+
deployXrayConfig(targetDir, packageRoot, write);
|
|
1707
|
+
deployProjectMcpJson(targetDir, write);
|
|
1708
|
+
mergeOpencodeJson(targetDir, packageRoot, write);
|
|
1709
|
+
installOpencodeBridge(targetDir, packageRoot, write);
|
|
1710
|
+
installGrokBridge(targetDir, packageRoot, write);
|
|
1711
|
+
installHermesBridge(targetDir, packageRoot, write);
|
|
1712
|
+
installOpenclawBridge(targetDir, packageRoot, write);
|
|
1713
|
+
try {
|
|
1714
|
+
mintConsumerSuit(packageRoot, targetDir, write);
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
if (!isSetupSkillMirrorDump(err, packageRoot)) throw err;
|
|
1717
|
+
}
|
|
1718
|
+
syncSetupSkillsAndRootLinks(packageRoot, targetDir);
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* 4.0.28 consumer postinstall, minus Cursor hooks and git hooks.
|
|
1724
|
+
* Used only when the folder is not a git work tree.
|
|
1725
|
+
*/
|
|
1726
|
+
function wearSuitSkippingCursorHooks(packageRoot, targetDir, log) {
|
|
1727
|
+
const write = typeof log === "function" ? log : () => {};
|
|
1728
|
+
if (!installConsumerProjectFiles(packageRoot, targetDir, write)) {
|
|
1729
|
+
if (!isConsumerInstall(packageRoot, targetDir)) {
|
|
1730
|
+
wearVendoredRepertoire(packageRoot, targetDir, write);
|
|
1731
|
+
}
|
|
1732
|
+
syncSetupSkillsAndRootLinks(packageRoot, targetDir);
|
|
1733
|
+
}
|
|
1734
|
+
process.stderr.write(CURSOR_HOOKS_SKIPPED_LINE);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function isGitTracked(targetDir, rel) {
|
|
1738
|
+
try {
|
|
1739
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], {
|
|
1740
|
+
cwd: targetDir,
|
|
1741
|
+
stdio: "ignore",
|
|
1742
|
+
});
|
|
1743
|
+
return true;
|
|
1744
|
+
} catch {
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function byteMismatch(expected, actual) {
|
|
1750
|
+
if (expected == null && actual == null) return "both missing";
|
|
1751
|
+
if (expected == null) return `file exists (${actual.length} bytes) but the snapshot has no written copy`;
|
|
1752
|
+
if (actual == null) return `file is missing; snapshot wrote ${expected.length} bytes`;
|
|
1753
|
+
const a = expected.toString("utf8");
|
|
1754
|
+
const b = actual.toString("utf8");
|
|
1755
|
+
let i = 0;
|
|
1756
|
+
const n = Math.min(a.length, b.length);
|
|
1757
|
+
while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i += 1;
|
|
1758
|
+
return `differs at byte ${i} (snapshot ${expected.length} bytes, file ${actual.length} bytes)`;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function removeWearExcludeLine(targetDir) {
|
|
1762
|
+
let top = "";
|
|
1763
|
+
let excludeRel = "";
|
|
1764
|
+
try {
|
|
1765
|
+
top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1766
|
+
cwd: targetDir,
|
|
1767
|
+
encoding: "utf8",
|
|
1768
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1769
|
+
}).trim();
|
|
1770
|
+
excludeRel = execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], {
|
|
1771
|
+
cwd: targetDir,
|
|
1772
|
+
encoding: "utf8",
|
|
1773
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1774
|
+
}).trim();
|
|
1775
|
+
} catch {
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (!top || !excludeRel) return;
|
|
1779
|
+
const rel = path.relative(top, wearStateDir(targetDir)).split(path.sep).join("/");
|
|
1780
|
+
if (!rel || rel.startsWith("..")) return;
|
|
1781
|
+
const pattern = `${rel}/`;
|
|
1782
|
+
const excludeFile = path.resolve(targetDir, excludeRel);
|
|
1783
|
+
if (!fs.existsSync(excludeFile)) return;
|
|
1784
|
+
const lines = fs.readFileSync(excludeFile, "utf8").split("\n");
|
|
1785
|
+
const next = lines.filter((line) => line.trim() !== pattern);
|
|
1786
|
+
if (next.length === lines.length) return;
|
|
1787
|
+
fs.writeFileSync(excludeFile, next.join("\n"));
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function saveCursorWearSnapshot(targetDir, previousText, writtenBody) {
|
|
1791
|
+
ensureWearStateGitignored(targetDir);
|
|
1792
|
+
const paths = wearStatePaths(targetDir);
|
|
1793
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
1794
|
+
let existed;
|
|
1795
|
+
let cursorDirExisted;
|
|
1796
|
+
let also = [];
|
|
1797
|
+
if (fs.existsSync(paths.meta)) {
|
|
1798
|
+
const prev = JSON.parse(fs.readFileSync(paths.meta, "utf8"));
|
|
1799
|
+
existed = Boolean(prev.existed);
|
|
1800
|
+
cursorDirExisted = Boolean(prev.cursorDirExisted);
|
|
1801
|
+
also = Array.isArray(prev.also) ? prev.also : [];
|
|
1802
|
+
} else {
|
|
1803
|
+
existed = previousText != null;
|
|
1804
|
+
cursorDirExisted = fs.existsSync(path.join(targetDir, ".cursor"));
|
|
1805
|
+
}
|
|
1806
|
+
const backup = existed ? userBytesBeforeWear(previousText) : null;
|
|
1807
|
+
if (backup != null) fs.writeFileSync(paths.backup, backup);
|
|
1808
|
+
else if (fs.existsSync(paths.backup)) fs.unlinkSync(paths.backup);
|
|
1809
|
+
const meta = { version: 1, existed, cursorDirExisted, also };
|
|
1810
|
+
fs.writeFileSync(paths.written, writtenBody);
|
|
1811
|
+
fs.writeFileSync(paths.meta, `${JSON.stringify(meta, null, 2)}\n`);
|
|
1812
|
+
return { paths, meta };
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function saveWearState(paths, meta) {
|
|
1816
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
1817
|
+
fs.writeFileSync(paths.meta, `${JSON.stringify(meta, null, 2)}\n`);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function resolveGitToplevel(targetDir) {
|
|
1821
|
+
try {
|
|
1822
|
+
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1823
|
+
cwd: targetDir,
|
|
1824
|
+
encoding: "utf8",
|
|
1825
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1826
|
+
}).trim();
|
|
1827
|
+
if (!top) return null;
|
|
1828
|
+
const resolved = path.resolve(top);
|
|
1829
|
+
if (resolved === path.resolve(targetDir)) return null;
|
|
1830
|
+
return resolved;
|
|
1831
|
+
} catch {
|
|
1832
|
+
return null;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function linkedWearRoots(targetDir) {
|
|
1837
|
+
const roots = [];
|
|
1838
|
+
const add = (root) => {
|
|
1839
|
+
if (!root) return;
|
|
1840
|
+
const resolved = path.resolve(root);
|
|
1841
|
+
if (resolved === path.resolve(targetDir)) return;
|
|
1842
|
+
if (roots.some((item) => path.resolve(item) === resolved)) return;
|
|
1843
|
+
roots.push(resolved);
|
|
1844
|
+
};
|
|
1845
|
+
add(resolveCursorWorkspaceRoot(targetDir));
|
|
1846
|
+
add(resolveGitToplevel(targetDir));
|
|
1847
|
+
roots.sort();
|
|
1848
|
+
return roots;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function removeDirIfEmpty(dir) {
|
|
1852
|
+
try {
|
|
1853
|
+
fs.rmdirSync(dir);
|
|
1854
|
+
} catch {
|
|
1855
|
+
// still holds user files
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function assertWearSnapshotReusable(resolvedTarget, hooksPath) {
|
|
1860
|
+
const paths = wearStatePaths(resolvedTarget);
|
|
1861
|
+
if (!fs.existsSync(paths.meta)) return;
|
|
1862
|
+
const current = fs.existsSync(hooksPath) ? fs.readFileSync(hooksPath) : null;
|
|
1863
|
+
const written = fs.existsSync(paths.written) ? fs.readFileSync(paths.written) : null;
|
|
1864
|
+
const same =
|
|
1865
|
+
(current == null && written == null) ||
|
|
1866
|
+
(current != null && written != null && current.equals(written));
|
|
1867
|
+
if (same) return;
|
|
1868
|
+
throw new Error(`cursor-wear: ${hooksPath} differs from the last wear; refusing to overwrite`);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Merge the five shipped Cursor hooks into the target project's
|
|
1873
|
+
* `.cursor/hooks.json`. Writes only `targetDir`. An event whose command
|
|
1874
|
+
* already runs xray-cloud-hook.sh is left alone, so a committed suited
|
|
1875
|
+
* hooks.json is not given a second copy. Pass `{ outerRoots: true }` to also
|
|
1876
|
+
* write outer paths; each one is printed.
|
|
1877
|
+
* The snapshot is `<project>/.xray/state/cursor-hook-wear/` (gitignored, not packed).
|
|
1878
|
+
* An already-worn file is never stored as the pre-wear backup.
|
|
1879
|
+
* A second wear reuses the snapshot only when hooks.json still matches the last write.
|
|
1880
|
+
*/
|
|
1881
|
+
function wearCursorHooks(targetDir, packageRoot, log, opts) {
|
|
1882
|
+
const write = typeof log === "function" ? log : () => {};
|
|
1883
|
+
const options = opts || {};
|
|
1884
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
1885
|
+
const resolvedPackage = path.resolve(packageRoot);
|
|
1886
|
+
if (classifyCursorWearGit(resolvedTarget) === "not-work-tree") {
|
|
1887
|
+
wearSuitSkippingCursorHooks(resolvedPackage, resolvedTarget, write);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
assertCursorWearGit(resolvedTarget);
|
|
1891
|
+
if (options.suit !== false) {
|
|
1892
|
+
if (!installConsumerProjectFiles(resolvedPackage, resolvedTarget, write)) {
|
|
1893
|
+
syncSetupSkillsAndRootLinks(resolvedPackage, resolvedTarget);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (!isConsumerInstall(resolvedPackage, resolvedTarget) || !consumerDistHooksReady(resolvedPackage)) {
|
|
1897
|
+
return fastenCursorHooksAt(resolvedTarget, resolvedPackage, write);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
const hooksPath = path.join(resolvedTarget, ".cursor", "hooks.json");
|
|
1901
|
+
assertWearSnapshotReusable(resolvedTarget, hooksPath);
|
|
1902
|
+
const xrayByEvent = installedCursorHookEntries(resolvedTarget, resolvedPackage);
|
|
1903
|
+
const originalText = fs.existsSync(hooksPath) ? fs.readFileSync(hooksPath, "utf8") : null;
|
|
1904
|
+
const body =
|
|
1905
|
+
originalText == null
|
|
1906
|
+
? serializeHooksDoc(mergeInstalledCursorHooks(null, xrayByEvent))
|
|
1907
|
+
: renderWornHooks(originalText, hooksPath, xrayByEvent, resolvedTarget);
|
|
1908
|
+
const changed = originalText == null || body !== originalText;
|
|
1909
|
+
const statePaths = wearStatePaths(resolvedTarget);
|
|
1910
|
+
const alreadyWorn = textHasDistHookEntries(originalText || "");
|
|
1911
|
+
let snap = null;
|
|
1912
|
+
if (changed || alreadyWorn || fs.existsSync(statePaths.meta)) {
|
|
1913
|
+
snap = saveCursorWearSnapshot(resolvedTarget, originalText, body);
|
|
1914
|
+
}
|
|
1915
|
+
if (changed) {
|
|
1916
|
+
fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
|
|
1917
|
+
fs.writeFileSync(hooksPath, body);
|
|
1918
|
+
} else if (!snap) {
|
|
1919
|
+
write("cursor-bridge", "hooks.json already runs these hooks", "info", { path: hooksPath });
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
if (options.outerRoots === true) {
|
|
1923
|
+
if (!snap) snap = saveCursorWearSnapshot(resolvedTarget, originalText, body);
|
|
1924
|
+
snap.meta.also = linkedWearRoots(resolvedTarget);
|
|
1925
|
+
saveWearState(snap.paths, snap.meta);
|
|
1926
|
+
for (const extra of snap.meta.also) {
|
|
1927
|
+
wearCursorHooks(extra, resolvedPackage, write, { outerRoots: false, suit: false });
|
|
1928
|
+
process.stdout.write(`cursor-wear: wrote outer hooks at ${extra}\n`);
|
|
1929
|
+
}
|
|
1930
|
+
saveWearState(snap.paths, snap.meta);
|
|
1931
|
+
}
|
|
1932
|
+
if (options.suit !== false && packageShipsConsumerSuit(resolvedPackage)) {
|
|
1933
|
+
fastenCursorHookScripts(resolvedTarget, resolvedPackage, write);
|
|
1934
|
+
}
|
|
1935
|
+
write("cursor-bridge", "hooks.json wired to installed dist", "info", { path: hooksPath });
|
|
1936
|
+
return hooksPath;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Restore `.cursor/hooks.json` from the project snapshot only when the file
|
|
1941
|
+
* still matches what wear wrote and the restored bytes match that snapshot.
|
|
1942
|
+
* A mismatch or a file that cannot be parsed leaves the snapshot in place.
|
|
1943
|
+
*/
|
|
1944
|
+
function unwearCursorHooks(targetDir, log) {
|
|
1945
|
+
const write = typeof log === "function" ? log : () => {};
|
|
1946
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
1947
|
+
assertCursorWearGit(resolvedTarget);
|
|
1948
|
+
const paths = wearStatePaths(resolvedTarget);
|
|
1949
|
+
const cursor = path.join(resolvedTarget, ".cursor");
|
|
1950
|
+
const hooksPath = path.join(cursor, "hooks.json");
|
|
1951
|
+
if (!fs.existsSync(paths.meta)) {
|
|
1952
|
+
process.stderr.write(`cursor-wear: no snapshot at ${paths.dir}\n`);
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
let meta;
|
|
1956
|
+
try {
|
|
1957
|
+
meta = JSON.parse(fs.readFileSync(paths.meta, "utf8"));
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
process.stderr.write(
|
|
1960
|
+
`cursor-wear: snapshot does not match the current file (${err.message}); snapshot kept\n`,
|
|
1961
|
+
);
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
const current = fs.existsSync(hooksPath) ? fs.readFileSync(hooksPath) : null;
|
|
1965
|
+
const written = fs.existsSync(paths.written) ? fs.readFileSync(paths.written) : null;
|
|
1966
|
+
const matches =
|
|
1967
|
+
Boolean(current && written && current.equals(written)) || (current == null && written == null);
|
|
1968
|
+
if (!matches) {
|
|
1969
|
+
let detail = byteMismatch(written, current);
|
|
1970
|
+
if (current) {
|
|
1971
|
+
try {
|
|
1972
|
+
parseHooksText(current.toString("utf8"), hooksPath);
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
detail = `cannot be parsed (${err.message})`;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
process.stderr.write(`cursor-wear: ${hooksPath} ${detail}; snapshot kept\n`);
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
if (meta.existed) {
|
|
1981
|
+
if (!fs.existsSync(paths.backup)) {
|
|
1982
|
+
process.stderr.write(`cursor-wear: ${hooksPath} backup missing; snapshot kept\n`);
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
const backup = fs.readFileSync(paths.backup);
|
|
1986
|
+
fs.copyFileSync(paths.backup, hooksPath);
|
|
1987
|
+
if (!fs.readFileSync(hooksPath).equals(backup)) {
|
|
1988
|
+
process.stderr.write(
|
|
1989
|
+
`cursor-wear: ${hooksPath} restored bytes differ from the snapshot; snapshot kept\n`,
|
|
1990
|
+
);
|
|
1991
|
+
return false;
|
|
1992
|
+
}
|
|
1993
|
+
} else if (isGitTracked(resolvedTarget, ".cursor/hooks.json")) {
|
|
1994
|
+
process.stderr.write(`cursor-wear: ${hooksPath} is committed; refusing to delete it; snapshot kept\n`);
|
|
1995
|
+
return false;
|
|
1996
|
+
} else if (fs.existsSync(hooksPath)) {
|
|
1997
|
+
fs.unlinkSync(hooksPath);
|
|
1998
|
+
}
|
|
1999
|
+
const also = meta.also || [];
|
|
2000
|
+
fs.rmSync(paths.dir, { recursive: true, force: true });
|
|
2001
|
+
removeWearExcludeLine(resolvedTarget);
|
|
2002
|
+
const stateDir = path.join(resolvedTarget, ".xray", "state");
|
|
2003
|
+
removeDirIfEmpty(stateDir);
|
|
2004
|
+
removeDirIfEmpty(path.join(resolvedTarget, ".xray"));
|
|
2005
|
+
if (!meta.cursorDirExisted) removeDirIfEmpty(cursor);
|
|
2006
|
+
for (const extra of also) unwearCursorHooks(extra, write);
|
|
2007
|
+
write("cursor-bridge", "hooks.json restored", "info", { path: hooksPath });
|
|
2008
|
+
return true;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
981
2011
|
function installCursorBridge(targetDir, packageRoot, log) {
|
|
982
|
-
const
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
fastenCursorHooksAt(
|
|
2012
|
+
const consumerDist = isConsumerInstall(packageRoot, targetDir) && consumerDistHooksReady(packageRoot);
|
|
2013
|
+
const dest = consumerDist
|
|
2014
|
+
? wearCursorHooks(targetDir, packageRoot, log)
|
|
2015
|
+
: fastenCursorHooksAt(targetDir, packageRoot, log);
|
|
2016
|
+
if (!consumerDist) {
|
|
2017
|
+
const workspace = resolveCursorWorkspaceRoot(targetDir);
|
|
2018
|
+
if (workspace && path.resolve(workspace) !== path.resolve(targetDir)) {
|
|
2019
|
+
fastenCursorHooksAt(workspace, packageRoot, log);
|
|
2020
|
+
}
|
|
986
2021
|
}
|
|
987
2022
|
reloadCursorHostHooks(log);
|
|
988
2023
|
return dest;
|
|
989
2024
|
}
|
|
990
2025
|
|
|
991
|
-
function installGitHooks(packageRoot, log) {
|
|
2026
|
+
function installGitHooks(packageRoot, log, cwd) {
|
|
992
2027
|
const installHooks = path.join(packageRoot, "scripts", "hooks", "install-hooks.cjs");
|
|
993
2028
|
if (!fs.existsSync(installHooks)) return;
|
|
994
2029
|
try {
|
|
995
|
-
|
|
2030
|
+
execFileSync(process.execPath, [installHooks], { stdio: "pipe", cwd: cwd || process.cwd() });
|
|
996
2031
|
log("hooks", "pre-commit hook installed", "info");
|
|
997
2032
|
} catch {
|
|
998
2033
|
// non-git or hook failure — not blocking
|
|
999
2034
|
}
|
|
1000
2035
|
}
|
|
1001
2036
|
|
|
2037
|
+
function assertSetupGit(targetDir) {
|
|
2038
|
+
assertCursorWearGit(targetDir);
|
|
2039
|
+
let top = "";
|
|
2040
|
+
try {
|
|
2041
|
+
top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2042
|
+
cwd: targetDir,
|
|
2043
|
+
encoding: "utf8",
|
|
2044
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2045
|
+
}).trim();
|
|
2046
|
+
} catch {
|
|
2047
|
+
throw new Error("cursor-wear: not a git work tree");
|
|
2048
|
+
}
|
|
2049
|
+
if (path.resolve(top) !== path.resolve(targetDir)) {
|
|
2050
|
+
throw new Error("setup: work tree points elsewhere");
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function setupProjectBridges(opts) {
|
|
2055
|
+
const packageRoot = path.resolve(opts.packageRoot);
|
|
2056
|
+
const targetDir = path.resolve(opts.targetDir);
|
|
2057
|
+
const log =
|
|
2058
|
+
typeof opts.log === "function"
|
|
2059
|
+
? opts.log
|
|
2060
|
+
: () => {
|
|
2061
|
+
/* noop */
|
|
2062
|
+
};
|
|
2063
|
+
if (classifyCursorWearGit(targetDir) === "not-work-tree") {
|
|
2064
|
+
wearSuitSkippingCursorHooks(packageRoot, targetDir, log);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
assertSetupGit(targetDir);
|
|
2068
|
+
if (!installConsumerProjectFiles(packageRoot, targetDir, log)) {
|
|
2069
|
+
syncSetupSkillsAndRootLinks(packageRoot, targetDir);
|
|
2070
|
+
deployProjectMcpJson(targetDir, log);
|
|
2071
|
+
installOpencodeBridge(targetDir, packageRoot, log);
|
|
2072
|
+
installGrokBridge(targetDir, packageRoot, log);
|
|
2073
|
+
installHermesBridge(targetDir, packageRoot, log);
|
|
2074
|
+
installOpenclawBridge(targetDir, packageRoot, log);
|
|
2075
|
+
}
|
|
2076
|
+
if (opts.gitHooks === true) installGitHooks(packageRoot, log, targetDir);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1002
2079
|
/**
|
|
1003
2080
|
* Framework dogfood: do not deploy consumer copies over the package,
|
|
1004
2081
|
* but keep Grok discovery hooks hot and enable Repertoire when it resolves.
|
|
@@ -1031,8 +2108,9 @@ function installFrameworkDogfoodWear(packageRoot, log) {
|
|
|
1031
2108
|
function installAllBridges(opts) {
|
|
1032
2109
|
const packageRoot = path.resolve(opts.packageRoot);
|
|
1033
2110
|
const targetDir = path.resolve(opts.targetDir);
|
|
2111
|
+
const logFn = opts.log;
|
|
1034
2112
|
const log =
|
|
1035
|
-
|
|
2113
|
+
logFn ||
|
|
1036
2114
|
((_component, _action, _status, _details) => {
|
|
1037
2115
|
/* noop */
|
|
1038
2116
|
});
|
|
@@ -1055,7 +2133,6 @@ function installAllBridges(opts) {
|
|
|
1055
2133
|
installHermesBridge(targetDir, packageRoot, log);
|
|
1056
2134
|
installOpenclawBridge(targetDir, packageRoot, log);
|
|
1057
2135
|
installCursorBridge(targetDir, packageRoot, log);
|
|
1058
|
-
installGitHooks(packageRoot, log);
|
|
1059
2136
|
|
|
1060
2137
|
log("install-bridges", "4-platform + cursor wear complete", "success");
|
|
1061
2138
|
}
|
|
@@ -1093,5 +2170,37 @@ module.exports = {
|
|
|
1093
2170
|
mergeCloudSafeCursorHooks,
|
|
1094
2171
|
isEnvAssignmentCursorCommand,
|
|
1095
2172
|
CURSOR_HOOK_SCRIPTS,
|
|
2173
|
+
CURSOR_HOOK_EVENTS,
|
|
1096
2174
|
CLOUD_SAFE_CURSOR_HOOKS,
|
|
1097
|
-
|
|
2175
|
+
wearCursorHooks,
|
|
2176
|
+
unwearCursorHooks,
|
|
2177
|
+
setupProjectBridges,
|
|
2178
|
+
mergeInstalledCursorHooks,
|
|
2179
|
+
isXrayHookCommand,
|
|
2180
|
+
installedHookCommand,
|
|
2181
|
+
consumerDistHooksReady,
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
if (require.main === module) {
|
|
2185
|
+
if (process.argv[2] !== "setup") {
|
|
2186
|
+
process.stderr.write("setup: unknown command\n");
|
|
2187
|
+
process.exit(1);
|
|
2188
|
+
}
|
|
2189
|
+
const gitHooks = process.argv.includes("--git-hooks");
|
|
2190
|
+
try {
|
|
2191
|
+
setupProjectBridges({
|
|
2192
|
+
packageRoot: path.resolve(__dirname, "..", ".."),
|
|
2193
|
+
targetDir: path.resolve(process.cwd()),
|
|
2194
|
+
gitHooks,
|
|
2195
|
+
log: () => {},
|
|
2196
|
+
});
|
|
2197
|
+
process.stdout.write(
|
|
2198
|
+
gitHooks
|
|
2199
|
+
? "setup: wrote .mcp.json, chat bridges, and git hooks\n"
|
|
2200
|
+
: "setup: wrote .mcp.json and chat bridges\n",
|
|
2201
|
+
);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
process.stderr.write(`${err && err.message ? err.message : err}\n`);
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
}
|
|
2206
|
+
}
|