@cg3/equip 0.2.22 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +26 -10
- package/bin/equip.js +159 -68
- package/demo/README.md +1 -1
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cli.d.ts +22 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +148 -0
- package/dist/lib/cli.js.map +1 -0
- package/dist/lib/commands/doctor.d.ts +2 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -0
- package/dist/lib/commands/doctor.js +162 -0
- package/dist/lib/commands/doctor.js.map +1 -0
- package/dist/lib/commands/status.d.ts +2 -0
- package/dist/lib/commands/status.d.ts.map +1 -0
- package/dist/lib/commands/status.js +134 -0
- package/dist/lib/commands/status.js.map +1 -0
- package/dist/lib/commands/update.d.ts +2 -0
- package/dist/lib/commands/update.d.ts.map +1 -0
- package/dist/lib/commands/update.js +93 -0
- package/dist/lib/commands/update.js.map +1 -0
- package/dist/lib/detect.d.ts +12 -0
- package/dist/lib/detect.d.ts.map +1 -0
- package/dist/lib/detect.js +109 -0
- package/dist/lib/detect.js.map +1 -0
- package/dist/lib/hooks.d.ts +40 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/hooks.js +226 -0
- package/dist/lib/hooks.js.map +1 -0
- package/dist/lib/mcp.d.ts +73 -0
- package/dist/lib/mcp.d.ts.map +1 -0
- package/dist/lib/mcp.js +418 -0
- package/dist/lib/mcp.js.map +1 -0
- package/dist/lib/platforms.d.ts +67 -0
- package/dist/lib/platforms.d.ts.map +1 -0
- package/dist/lib/platforms.js +353 -0
- package/dist/lib/platforms.js.map +1 -0
- package/dist/lib/rules.d.ts +35 -0
- package/dist/lib/rules.d.ts.map +1 -0
- package/dist/lib/rules.js +161 -0
- package/dist/lib/rules.js.map +1 -0
- package/dist/lib/state.d.ts +33 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +130 -0
- package/dist/lib/state.js.map +1 -0
- package/package.json +19 -13
- package/registry.json +9 -0
- package/index.js +0 -244
- package/lib/cli.js +0 -99
- package/lib/detect.js +0 -242
- package/lib/hooks.js +0 -238
- package/lib/mcp.js +0 -503
- package/lib/platforms.js +0 -184
- package/lib/rules.js +0 -170
package/lib/detect.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
// Platform detection — discovers installed AI coding tools.
|
|
2
|
-
// Zero dependencies.
|
|
3
|
-
|
|
4
|
-
"use strict";
|
|
5
|
-
|
|
6
|
-
const fs = require("fs");
|
|
7
|
-
const path = require("path");
|
|
8
|
-
const os = require("os");
|
|
9
|
-
const { execSync } = require("child_process");
|
|
10
|
-
const { getVsCodeMcpPath, getVsCodeUserDir, getClineConfigPath, getRooConfigPath, getCodexConfigPath, getGeminiSettingsPath, getJunieMcpPath, getCopilotJetBrainsMcpPath, getCopilotCliMcpPath } = require("./platforms");
|
|
11
|
-
const { readMcpEntry } = require("./mcp");
|
|
12
|
-
|
|
13
|
-
// ─── Helpers ─────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
function whichSync(cmd) {
|
|
16
|
-
try {
|
|
17
|
-
const r = execSync(process.platform === "win32" ? `where ${cmd} 2>nul` : `which ${cmd} 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
|
|
18
|
-
return r.trim().split(/\r?\n/)[0] || null;
|
|
19
|
-
} catch { return null; }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function dirExists(p) {
|
|
23
|
-
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function fileExists(p) {
|
|
27
|
-
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function cliVersion(cmd, regex) {
|
|
31
|
-
try {
|
|
32
|
-
const out = execSync(`${cmd} --version 2>&1`, { encoding: "utf-8", timeout: 5000 });
|
|
33
|
-
const m = out.match(regex || /(\d+\.\d+[\.\d]*)/);
|
|
34
|
-
return m ? m[1] : "unknown";
|
|
35
|
-
} catch { return null; }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getClaudeCodeVersion() {
|
|
39
|
-
try {
|
|
40
|
-
const out = execSync("claude --version 2>&1", { encoding: "utf-8", timeout: 5000 });
|
|
41
|
-
const m = out.match(/(\d+\.\d+[\.\d]*)/);
|
|
42
|
-
return m ? m[1] : "unknown";
|
|
43
|
-
} catch { return null; }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ─── Detection ──────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Detect installed AI coding platforms.
|
|
50
|
-
* @param {string} [serverName] - MCP server name to check for existing config (default: null)
|
|
51
|
-
* @returns {Array<object>} Array of platform objects
|
|
52
|
-
*/
|
|
53
|
-
function detectPlatforms(serverName) {
|
|
54
|
-
const home = os.homedir();
|
|
55
|
-
const platforms = [];
|
|
56
|
-
|
|
57
|
-
// Claude Code
|
|
58
|
-
const claudeVersion = whichSync("claude") ? getClaudeCodeVersion() : null;
|
|
59
|
-
if (claudeVersion || dirExists(path.join(home, ".claude"))) {
|
|
60
|
-
const configPath = path.join(home, ".claude.json");
|
|
61
|
-
const rulesPath = path.join(home, ".claude", "CLAUDE.md");
|
|
62
|
-
platforms.push({
|
|
63
|
-
platform: "claude-code",
|
|
64
|
-
version: claudeVersion || "unknown",
|
|
65
|
-
configPath,
|
|
66
|
-
rulesPath,
|
|
67
|
-
existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
|
|
68
|
-
hasCli: !!whichSync("claude"),
|
|
69
|
-
rootKey: "mcpServers",
|
|
70
|
-
configFormat: "json",
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Cursor
|
|
75
|
-
const cursorDir = path.join(home, ".cursor");
|
|
76
|
-
if (whichSync("cursor") || dirExists(cursorDir)) {
|
|
77
|
-
const configPath = path.join(cursorDir, "mcp.json");
|
|
78
|
-
platforms.push({
|
|
79
|
-
platform: "cursor",
|
|
80
|
-
version: cliVersion("cursor") || "unknown",
|
|
81
|
-
configPath,
|
|
82
|
-
rulesPath: null, // Cursor: clipboard only
|
|
83
|
-
existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
|
|
84
|
-
hasCli: !!whichSync("cursor"),
|
|
85
|
-
rootKey: "mcpServers",
|
|
86
|
-
configFormat: "json",
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Windsurf
|
|
91
|
-
const windsurfDir = path.join(home, ".codeium", "windsurf");
|
|
92
|
-
if (dirExists(windsurfDir)) {
|
|
93
|
-
const configPath = path.join(windsurfDir, "mcp_config.json");
|
|
94
|
-
const rulesPath = path.join(windsurfDir, "memories", "global_rules.md");
|
|
95
|
-
platforms.push({
|
|
96
|
-
platform: "windsurf",
|
|
97
|
-
version: "unknown",
|
|
98
|
-
configPath,
|
|
99
|
-
rulesPath,
|
|
100
|
-
existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
|
|
101
|
-
hasCli: false,
|
|
102
|
-
rootKey: "mcpServers",
|
|
103
|
-
configFormat: "json",
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// VS Code (Copilot)
|
|
108
|
-
const vscodeMcpPath = getVsCodeMcpPath();
|
|
109
|
-
if (whichSync("code") || fileExists(vscodeMcpPath) || dirExists(getVsCodeUserDir())) {
|
|
110
|
-
platforms.push({
|
|
111
|
-
platform: "vscode",
|
|
112
|
-
version: cliVersion("code") || "unknown",
|
|
113
|
-
configPath: vscodeMcpPath,
|
|
114
|
-
rulesPath: null, // VS Code: clipboard only
|
|
115
|
-
existingMcp: serverName ? readMcpEntry(vscodeMcpPath, "servers", serverName) : null,
|
|
116
|
-
hasCli: !!whichSync("code"),
|
|
117
|
-
rootKey: "servers",
|
|
118
|
-
configFormat: "json",
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Cline (VS Code extension)
|
|
123
|
-
const clineConfigPath = getClineConfigPath();
|
|
124
|
-
if (fileExists(clineConfigPath) || dirExists(path.dirname(clineConfigPath))) {
|
|
125
|
-
const home_ = os.homedir();
|
|
126
|
-
platforms.push({
|
|
127
|
-
platform: "cline",
|
|
128
|
-
version: "unknown",
|
|
129
|
-
configPath: clineConfigPath,
|
|
130
|
-
rulesPath: path.join(home_, "Documents", "Cline", "Rules"),
|
|
131
|
-
existingMcp: serverName ? readMcpEntry(clineConfigPath, "mcpServers", serverName) : null,
|
|
132
|
-
hasCli: false,
|
|
133
|
-
rootKey: "mcpServers",
|
|
134
|
-
configFormat: "json",
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Roo Code (VS Code extension)
|
|
139
|
-
const rooConfigPath = getRooConfigPath();
|
|
140
|
-
if (fileExists(rooConfigPath) || dirExists(path.dirname(rooConfigPath))) {
|
|
141
|
-
platforms.push({
|
|
142
|
-
platform: "roo-code",
|
|
143
|
-
version: "unknown",
|
|
144
|
-
configPath: rooConfigPath,
|
|
145
|
-
rulesPath: path.join(os.homedir(), ".roo", "rules"),
|
|
146
|
-
existingMcp: serverName ? readMcpEntry(rooConfigPath, "mcpServers", serverName) : null,
|
|
147
|
-
hasCli: false,
|
|
148
|
-
rootKey: "mcpServers",
|
|
149
|
-
configFormat: "json",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Codex (OpenAI CLI)
|
|
154
|
-
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
155
|
-
const codexConfigPath = getCodexConfigPath();
|
|
156
|
-
if (whichSync("codex") || dirExists(codexHome)) {
|
|
157
|
-
platforms.push({
|
|
158
|
-
platform: "codex",
|
|
159
|
-
version: cliVersion("codex") || "unknown",
|
|
160
|
-
configPath: codexConfigPath,
|
|
161
|
-
rulesPath: path.join(codexHome, "AGENTS.md"),
|
|
162
|
-
existingMcp: serverName ? readMcpEntry(codexConfigPath, "mcp_servers", serverName, "toml") : null,
|
|
163
|
-
hasCli: !!whichSync("codex"),
|
|
164
|
-
rootKey: "mcp_servers",
|
|
165
|
-
configFormat: "toml",
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Gemini CLI (Google)
|
|
170
|
-
const geminiDir = path.join(home, ".gemini");
|
|
171
|
-
const geminiSettingsPath = getGeminiSettingsPath();
|
|
172
|
-
if (whichSync("gemini") || dirExists(geminiDir)) {
|
|
173
|
-
platforms.push({
|
|
174
|
-
platform: "gemini-cli",
|
|
175
|
-
version: cliVersion("gemini") || "unknown",
|
|
176
|
-
configPath: geminiSettingsPath,
|
|
177
|
-
rulesPath: path.join(geminiDir, "GEMINI.md"),
|
|
178
|
-
existingMcp: serverName ? readMcpEntry(geminiSettingsPath, "mcpServers", serverName) : null,
|
|
179
|
-
hasCli: !!whichSync("gemini"),
|
|
180
|
-
rootKey: "mcpServers",
|
|
181
|
-
configFormat: "json",
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Junie (JetBrains CLI)
|
|
186
|
-
const junieDir = path.join(home, ".junie");
|
|
187
|
-
const junieMcpPath = getJunieMcpPath();
|
|
188
|
-
if (whichSync("junie") || dirExists(junieDir)) {
|
|
189
|
-
platforms.push({
|
|
190
|
-
platform: "junie",
|
|
191
|
-
version: cliVersion("junie") || "unknown",
|
|
192
|
-
configPath: junieMcpPath,
|
|
193
|
-
rulesPath: null,
|
|
194
|
-
existingMcp: serverName ? readMcpEntry(junieMcpPath, "mcpServers", serverName) : null,
|
|
195
|
-
hasCli: !!whichSync("junie"),
|
|
196
|
-
rootKey: "mcpServers",
|
|
197
|
-
configFormat: "json",
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// GitHub Copilot in JetBrains IDEs
|
|
202
|
-
const copilotJbPath = getCopilotJetBrainsMcpPath();
|
|
203
|
-
const copilotJbDir = path.dirname(path.dirname(copilotJbPath)); // github-copilot dir
|
|
204
|
-
if (dirExists(copilotJbDir)) {
|
|
205
|
-
platforms.push({
|
|
206
|
-
platform: "copilot-jetbrains",
|
|
207
|
-
version: "unknown",
|
|
208
|
-
configPath: copilotJbPath,
|
|
209
|
-
rulesPath: null,
|
|
210
|
-
existingMcp: serverName ? readMcpEntry(copilotJbPath, "mcpServers", serverName) : null,
|
|
211
|
-
hasCli: false,
|
|
212
|
-
rootKey: "mcpServers",
|
|
213
|
-
configFormat: "json",
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// GitHub Copilot CLI
|
|
218
|
-
const copilotCliDir = path.join(home, ".copilot");
|
|
219
|
-
const copilotCliMcpPath = getCopilotCliMcpPath();
|
|
220
|
-
if (whichSync("copilot") || dirExists(copilotCliDir)) {
|
|
221
|
-
platforms.push({
|
|
222
|
-
platform: "copilot-cli",
|
|
223
|
-
version: cliVersion("copilot") || "unknown",
|
|
224
|
-
configPath: copilotCliMcpPath,
|
|
225
|
-
rulesPath: null,
|
|
226
|
-
existingMcp: serverName ? readMcpEntry(copilotCliMcpPath, "mcpServers", serverName) : null,
|
|
227
|
-
hasCli: !!whichSync("copilot"),
|
|
228
|
-
rootKey: "mcpServers",
|
|
229
|
-
configFormat: "json",
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return platforms;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
module.exports = {
|
|
237
|
-
detectPlatforms,
|
|
238
|
-
whichSync,
|
|
239
|
-
dirExists,
|
|
240
|
-
fileExists,
|
|
241
|
-
cliVersion,
|
|
242
|
-
};
|
package/lib/hooks.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
// Hook installation for platforms that support lifecycle hooks.
|
|
2
|
-
// Equip provides the infrastructure; consumers provide hook definitions.
|
|
3
|
-
// Zero dependencies.
|
|
4
|
-
|
|
5
|
-
"use strict";
|
|
6
|
-
|
|
7
|
-
const fs = require("fs");
|
|
8
|
-
const path = require("path");
|
|
9
|
-
const os = require("os");
|
|
10
|
-
|
|
11
|
-
// ─── Platform Hook Capabilities ──────────────────────────────
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Which platforms support hooks and what events they handle.
|
|
15
|
-
* Returns null if the platform doesn't support hooks.
|
|
16
|
-
*/
|
|
17
|
-
function getHookCapabilities(platformId) {
|
|
18
|
-
const caps = {
|
|
19
|
-
"claude-code": {
|
|
20
|
-
settingsPath: () => path.join(os.homedir(), ".claude", "settings.json"),
|
|
21
|
-
events: ["PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop",
|
|
22
|
-
"SessionStart", "SessionEnd", "UserPromptSubmit", "Notification",
|
|
23
|
-
"SubagentStart", "SubagentStop", "PreCompact", "TaskCompleted"],
|
|
24
|
-
format: "claude-code",
|
|
25
|
-
},
|
|
26
|
-
// Future: cursor, etc.
|
|
27
|
-
};
|
|
28
|
-
return caps[platformId] || null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ─── Hook Config Generation ─────────────────────────────────
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Build platform-specific hooks config from consumer-defined hook definitions.
|
|
35
|
-
* @param {Array} hookDefs - Array of { event, matcher?, script, name }
|
|
36
|
-
* @param {string} hookDir - Absolute path to directory containing hook scripts
|
|
37
|
-
* @param {string} platformId - Platform id
|
|
38
|
-
* @returns {object|null} Hooks config in the platform's format
|
|
39
|
-
*/
|
|
40
|
-
function buildHooksConfig(hookDefs, hookDir, platformId) {
|
|
41
|
-
const caps = getHookCapabilities(platformId);
|
|
42
|
-
if (!caps || !hookDefs || hookDefs.length === 0) return null;
|
|
43
|
-
|
|
44
|
-
if (caps.format === "claude-code") {
|
|
45
|
-
const config = {};
|
|
46
|
-
|
|
47
|
-
for (const def of hookDefs) {
|
|
48
|
-
if (!caps.events.includes(def.event)) continue;
|
|
49
|
-
|
|
50
|
-
const entry = {
|
|
51
|
-
hooks: [{
|
|
52
|
-
type: "command",
|
|
53
|
-
command: `node "${path.join(hookDir, def.name + ".js")}"`,
|
|
54
|
-
}],
|
|
55
|
-
};
|
|
56
|
-
if (def.matcher) entry.matcher = def.matcher;
|
|
57
|
-
|
|
58
|
-
if (!config[def.event]) config[def.event] = [];
|
|
59
|
-
config[def.event].push(entry);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return Object.keys(config).length > 0 ? config : null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ─── Installation ────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Install hook scripts to disk and register them in platform settings.
|
|
72
|
-
* @param {object} platform - Platform object from detect()
|
|
73
|
-
* @param {Array} hookDefs - Array of { event, matcher?, script, name }
|
|
74
|
-
* @param {object} [options] - { hookDir, dryRun, marker }
|
|
75
|
-
* @returns {{ installed: boolean, scripts: string[], hookDir: string } | null}
|
|
76
|
-
*/
|
|
77
|
-
function installHooks(platform, hookDefs, options = {}) {
|
|
78
|
-
const caps = getHookCapabilities(platform.platform);
|
|
79
|
-
if (!caps || !hookDefs || hookDefs.length === 0) return null;
|
|
80
|
-
|
|
81
|
-
if (!options.hookDir) throw new Error("hookDir is required");
|
|
82
|
-
const hookDir = options.hookDir;
|
|
83
|
-
const dryRun = options.dryRun || false;
|
|
84
|
-
|
|
85
|
-
// 1. Write hook scripts
|
|
86
|
-
const installedScripts = [];
|
|
87
|
-
|
|
88
|
-
if (!dryRun) {
|
|
89
|
-
fs.mkdirSync(hookDir, { recursive: true });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
for (const def of hookDefs) {
|
|
93
|
-
if (!caps.events.includes(def.event)) continue;
|
|
94
|
-
const filePath = path.join(hookDir, def.name + ".js");
|
|
95
|
-
if (!dryRun) {
|
|
96
|
-
fs.writeFileSync(filePath, def.script, { mode: 0o755 });
|
|
97
|
-
}
|
|
98
|
-
installedScripts.push(def.name + ".js");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (installedScripts.length === 0) return null;
|
|
102
|
-
|
|
103
|
-
// 2. Register hooks in platform settings
|
|
104
|
-
const hooksConfig = buildHooksConfig(hookDefs, hookDir, platform.platform);
|
|
105
|
-
if (!hooksConfig) return null;
|
|
106
|
-
|
|
107
|
-
if (!dryRun) {
|
|
108
|
-
const settingsPath = caps.settingsPath();
|
|
109
|
-
let settings = {};
|
|
110
|
-
try {
|
|
111
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
112
|
-
} catch { /* file doesn't exist yet */ }
|
|
113
|
-
|
|
114
|
-
// Merge hooks — preserve existing non-marker hooks
|
|
115
|
-
if (!settings.hooks) settings.hooks = {};
|
|
116
|
-
|
|
117
|
-
for (const [event, hookGroups] of Object.entries(hooksConfig)) {
|
|
118
|
-
if (!settings.hooks[event]) {
|
|
119
|
-
settings.hooks[event] = hookGroups;
|
|
120
|
-
} else {
|
|
121
|
-
// Remove existing hooks from this marker, then add new ones
|
|
122
|
-
const hookDirNorm = hookDir.replace(/\\/g, "/");
|
|
123
|
-
settings.hooks[event] = settings.hooks[event].filter(
|
|
124
|
-
group => !group.hooks?.some(h => h.command && h.command.replace(/\\/g, "/").includes(hookDirNorm))
|
|
125
|
-
);
|
|
126
|
-
settings.hooks[event].push(...hookGroups);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
131
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { installed: true, scripts: installedScripts, hookDir };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Uninstall hook scripts and remove from platform settings.
|
|
139
|
-
* @param {object} platform - Platform object
|
|
140
|
-
* @param {Array} hookDefs - Array of { event, matcher?, script, name } (need names to know what to remove)
|
|
141
|
-
* @param {object} [options] - { hookDir, dryRun }
|
|
142
|
-
* @returns {boolean} Whether anything was removed
|
|
143
|
-
*/
|
|
144
|
-
function uninstallHooks(platform, hookDefs, options = {}) {
|
|
145
|
-
const caps = getHookCapabilities(platform.platform);
|
|
146
|
-
if (!caps || !hookDefs || hookDefs.length === 0) return false;
|
|
147
|
-
|
|
148
|
-
if (!options.hookDir) throw new Error("hookDir is required");
|
|
149
|
-
const hookDir = options.hookDir;
|
|
150
|
-
const dryRun = options.dryRun || false;
|
|
151
|
-
let removed = false;
|
|
152
|
-
|
|
153
|
-
// 1. Remove hook scripts
|
|
154
|
-
for (const def of hookDefs) {
|
|
155
|
-
const filePath = path.join(hookDir, def.name + ".js");
|
|
156
|
-
try {
|
|
157
|
-
if (fs.statSync(filePath).isFile()) {
|
|
158
|
-
if (!dryRun) fs.unlinkSync(filePath);
|
|
159
|
-
removed = true;
|
|
160
|
-
}
|
|
161
|
-
} catch { /* doesn't exist */ }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Clean up empty hooks dir
|
|
165
|
-
if (!dryRun) {
|
|
166
|
-
try { fs.rmdirSync(hookDir); } catch { /* not empty or doesn't exist */ }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 2. Remove from platform settings
|
|
170
|
-
if (!dryRun) {
|
|
171
|
-
const settingsPath = caps.settingsPath();
|
|
172
|
-
try {
|
|
173
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
174
|
-
if (settings.hooks) {
|
|
175
|
-
let changed = false;
|
|
176
|
-
const hookDirNorm = hookDir.replace(/\\/g, "/");
|
|
177
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
178
|
-
const before = settings.hooks[event].length;
|
|
179
|
-
settings.hooks[event] = settings.hooks[event].filter(
|
|
180
|
-
group => !group.hooks?.some(h => h.command && h.command.replace(/\\/g, "/").includes(hookDirNorm))
|
|
181
|
-
);
|
|
182
|
-
if (settings.hooks[event].length === 0) {
|
|
183
|
-
delete settings.hooks[event];
|
|
184
|
-
}
|
|
185
|
-
if (settings.hooks[event]?.length !== before) changed = true;
|
|
186
|
-
}
|
|
187
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
188
|
-
if (changed) {
|
|
189
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
190
|
-
removed = true;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} catch { /* file doesn't exist */ }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return removed;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Check if hooks are installed for a platform.
|
|
201
|
-
* @param {object} platform - Platform object
|
|
202
|
-
* @param {Array} hookDefs - Array of { event, matcher?, script, name }
|
|
203
|
-
* @param {object} [options] - { hookDir }
|
|
204
|
-
* @returns {boolean}
|
|
205
|
-
*/
|
|
206
|
-
function hasHooks(platform, hookDefs, options = {}) {
|
|
207
|
-
const caps = getHookCapabilities(platform.platform);
|
|
208
|
-
if (!caps || !hookDefs || hookDefs.length === 0) return false;
|
|
209
|
-
|
|
210
|
-
if (!options.hookDir) throw new Error("hookDir is required");
|
|
211
|
-
const hookDir = options.hookDir;
|
|
212
|
-
|
|
213
|
-
// Check scripts exist
|
|
214
|
-
for (const def of hookDefs) {
|
|
215
|
-
try {
|
|
216
|
-
if (!fs.statSync(path.join(hookDir, def.name + ".js")).isFile()) return false;
|
|
217
|
-
} catch { return false; }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check settings registration
|
|
221
|
-
try {
|
|
222
|
-
const settings = JSON.parse(fs.readFileSync(caps.settingsPath(), "utf-8"));
|
|
223
|
-
if (!settings.hooks) return false;
|
|
224
|
-
const hookDirNorm = hookDir.replace(/\\/g, "/");
|
|
225
|
-
const hasRegistered = Object.values(settings.hooks).some(groups =>
|
|
226
|
-
groups.some(g => g.hooks?.some(h => h.command && h.command.replace(/\\/g, "/").includes(hookDirNorm)))
|
|
227
|
-
);
|
|
228
|
-
return hasRegistered;
|
|
229
|
-
} catch { return false; }
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
module.exports = {
|
|
233
|
-
getHookCapabilities,
|
|
234
|
-
buildHooksConfig,
|
|
235
|
-
installHooks,
|
|
236
|
-
uninstallHooks,
|
|
237
|
-
hasHooks,
|
|
238
|
-
};
|