@mutmutco/hub 4.0.11 → 4.0.13

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.
Files changed (2) hide show
  1. package/dist/index.cjs +84 -2
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  ownVersion: () => ownVersion,
36
36
  probeHermesInstallation: () => probeHermesInstallation,
37
37
  progressiveRow: () => progressiveRow,
38
+ reconcileCursorUserHooks: () => reconcileCursorUserHooks,
38
39
  reportFooterLines: () => reportFooterLines,
39
40
  retryHint: () => retryHint,
40
41
  schedulerStatus: () => schedulerStatus,
@@ -1038,6 +1039,8 @@ var import_node_os4 = require("node:os");
1038
1039
  var import_node_path8 = require("node:path");
1039
1040
  var PACKAGE = "@mutmutco/cursor-plugin";
1040
1041
  var MANIFEST = ".cursor-plugin/plugin.json";
1042
+ var USER_HOOKS_FILE = "hooks.json";
1043
+ var PLUGIN_HOOKS_REL = ["hooks", "cursor-hooks.json"];
1041
1044
  var KEEP_GENERATIONS = 2;
1042
1045
  var STRANDED_TMP_AGE_MS = 24 * 60 * 6e4;
1043
1046
  var TREE_FILES = [MANIFEST, "skills/mmi/SKILL.md", "hooks/cursor-hooks.json", "scripts/hook-run.mjs", "scripts/hook-policy.mjs"];
@@ -1100,6 +1103,84 @@ function cursorHostEvidence(env) {
1100
1103
  }
1101
1104
  return null;
1102
1105
  }
1106
+ function resolvedPluginRoot(root) {
1107
+ return (0, import_node_path8.join)(root).replace(/\\/g, "/");
1108
+ }
1109
+ function isMmiEntry(entry, pluginRoot) {
1110
+ return typeof entry === "object" && entry !== null && typeof entry.command === "string" && entry.command.includes(resolvedPluginRoot(pluginRoot));
1111
+ }
1112
+ function resolveCommand(command, pluginRoot) {
1113
+ return command.replaceAll("${CURSOR_PLUGIN_ROOT}", resolvedPluginRoot(pluginRoot));
1114
+ }
1115
+ function desiredUserHooks(pluginRoot) {
1116
+ try {
1117
+ const doc = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(pluginRoot, ...PLUGIN_HOOKS_REL), "utf8"));
1118
+ const hooks = doc.hooks;
1119
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) return null;
1120
+ const desired = /* @__PURE__ */ new Map();
1121
+ for (const [event, entries] of Object.entries(hooks)) {
1122
+ if (!Array.isArray(entries)) return null;
1123
+ desired.set(event, entries.map((entry) => {
1124
+ const command = typeof entry.command === "string" ? entry.command : "";
1125
+ return { ...entry, command: resolveCommand(command, pluginRoot) };
1126
+ }));
1127
+ }
1128
+ return desired;
1129
+ } catch {
1130
+ return null;
1131
+ }
1132
+ }
1133
+ function mergeUserHooks(existing, desired, pluginRoot) {
1134
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) return null;
1135
+ const merged = { ...existing };
1136
+ const hooks = merged.hooks ?? {};
1137
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) return null;
1138
+ const nextHooks = {};
1139
+ for (const [event, entries] of Object.entries(hooks)) {
1140
+ if (!Array.isArray(entries)) return null;
1141
+ const kept = entries.filter((entry) => !isMmiEntry(entry, pluginRoot));
1142
+ nextHooks[event] = [...kept, ...desired.get(event) ?? []];
1143
+ }
1144
+ for (const [event, entries] of desired) {
1145
+ if (!(event in hooks)) nextHooks[event] = entries;
1146
+ }
1147
+ merged.hooks = nextHooks;
1148
+ return merged;
1149
+ }
1150
+ function reconcileCursorUserHooks(pluginRoot, homeDir) {
1151
+ const desired = desiredUserHooks(pluginRoot);
1152
+ if (!desired) return "source-unreadable";
1153
+ const path = (0, import_node_path8.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1154
+ if (!(0, import_node_fs10.existsSync)(path)) {
1155
+ (0, import_node_fs10.writeFileSync)(path, JSON.stringify({ version: 1, hooks: Object.fromEntries(desired) }, null, 2) + "\n", "utf8");
1156
+ return "updated";
1157
+ }
1158
+ let existing;
1159
+ try {
1160
+ existing = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
1161
+ } catch {
1162
+ return "user-conflict";
1163
+ }
1164
+ const merged = mergeUserHooks(existing, desired, pluginRoot);
1165
+ if (!merged) return "user-conflict";
1166
+ if (JSON.stringify(merged) === JSON.stringify(existing)) return "current";
1167
+ const tmp = `${path}.tmp-${process.pid}`;
1168
+ (0, import_node_fs10.writeFileSync)(tmp, JSON.stringify(merged, null, 2) + "\n", "utf8");
1169
+ (0, import_node_fs10.renameSync)(tmp, path);
1170
+ return "updated";
1171
+ }
1172
+ function userHooksDetail(pluginRoot, homeDir) {
1173
+ switch (reconcileCursorUserHooks(pluginRoot, homeDir)) {
1174
+ case "updated":
1175
+ return "; user hooks bridged into ~/.cursor/hooks.json";
1176
+ case "user-conflict":
1177
+ return `; USER HOOKS CONFLICT at ${(0, import_node_path8.join)(homeDir, ".cursor", USER_HOOKS_FILE)} \u2014 Cursor gates are NOT bridged; fix or remove that file and reconcile`;
1178
+ case "source-unreadable":
1179
+ return "; user hooks source unreadable \u2014 Cursor gates are NOT bridged";
1180
+ default:
1181
+ return "";
1182
+ }
1183
+ }
1103
1184
  function writeRollbackPointer(root, entry) {
1104
1185
  const path = (0, import_node_path8.join)(root, "quarantine", "rollback.json");
1105
1186
  const tmp = `${path}.tmp-${process.pid}`;
@@ -1188,7 +1269,7 @@ function cursorArm(options) {
1188
1269
  return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
1189
1270
  }
1190
1271
  if (installed === target && treeHealthy2(live)) {
1191
- return result(installed, target, "ok", `already at target ${target}`);
1272
+ return result(installed, target, "ok", `already at target ${target}${userHooksDetail(live, (0, import_node_os4.homedir)())}`);
1192
1273
  }
1193
1274
  if ((0, import_node_fs10.existsSync)(live) && !mmiOwned(live)) {
1194
1275
  return result(installed, target, "defer", `${live} carries no MMI ownership marker (plugin manifest name, or a ${PACKAGE} package.json) \u2014 refusing to displace an unmanaged directory (mmi-cli plugin heal adopts it)`);
@@ -1256,7 +1337,7 @@ function cursorArm(options) {
1256
1337
  writeRollbackPointer(root, { installed: target, previous: installed, generation: displaced ? quarantined : null });
1257
1338
  const pruned = pruneQuarantine(root, displaced ? quarantined : null);
1258
1339
  const previous = displaced ? `previous generation quarantined at ${quarantined}` : "no previous generation to quarantine";
1259
- return result(installed, target, "ok", `switched ${installed ?? "absent"} -> ${target}; ${previous}${pruned ? `; pruned ${pruned} older generation${pruned === 1 ? "" : "s"}` : ""}`);
1340
+ return result(installed, target, "ok", `switched ${installed ?? "absent"} -> ${target}; ${previous}${pruned ? `; pruned ${pruned} older generation${pruned === 1 ? "" : "s"}` : ""}${userHooksDetail(live, (0, import_node_os4.homedir)())}`);
1260
1341
  }
1261
1342
 
1262
1343
  // src/arm-jervcode.ts
@@ -2909,6 +2990,7 @@ if (typeof require !== "undefined" && require.main === module) {
2909
2990
  ownVersion,
2910
2991
  probeHermesInstallation,
2911
2992
  progressiveRow,
2993
+ reconcileCursorUserHooks,
2912
2994
  reportFooterLines,
2913
2995
  retryHint,
2914
2996
  schedulerStatus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "4.0.11",
3
+ "version": "4.0.13",
4
4
  "description": "Install and maintain the MMI CLI and every present host surface from one release-gated Hub command.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",