@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.
Files changed (57) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +26 -10
  3. package/bin/equip.js +159 -68
  4. package/demo/README.md +1 -1
  5. package/dist/index.d.ts +74 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +175 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lib/cli.d.ts +22 -0
  10. package/dist/lib/cli.d.ts.map +1 -0
  11. package/dist/lib/cli.js +148 -0
  12. package/dist/lib/cli.js.map +1 -0
  13. package/dist/lib/commands/doctor.d.ts +2 -0
  14. package/dist/lib/commands/doctor.d.ts.map +1 -0
  15. package/dist/lib/commands/doctor.js +162 -0
  16. package/dist/lib/commands/doctor.js.map +1 -0
  17. package/dist/lib/commands/status.d.ts +2 -0
  18. package/dist/lib/commands/status.d.ts.map +1 -0
  19. package/dist/lib/commands/status.js +134 -0
  20. package/dist/lib/commands/status.js.map +1 -0
  21. package/dist/lib/commands/update.d.ts +2 -0
  22. package/dist/lib/commands/update.d.ts.map +1 -0
  23. package/dist/lib/commands/update.js +93 -0
  24. package/dist/lib/commands/update.js.map +1 -0
  25. package/dist/lib/detect.d.ts +12 -0
  26. package/dist/lib/detect.d.ts.map +1 -0
  27. package/dist/lib/detect.js +109 -0
  28. package/dist/lib/detect.js.map +1 -0
  29. package/dist/lib/hooks.d.ts +40 -0
  30. package/dist/lib/hooks.d.ts.map +1 -0
  31. package/dist/lib/hooks.js +226 -0
  32. package/dist/lib/hooks.js.map +1 -0
  33. package/dist/lib/mcp.d.ts +73 -0
  34. package/dist/lib/mcp.d.ts.map +1 -0
  35. package/dist/lib/mcp.js +418 -0
  36. package/dist/lib/mcp.js.map +1 -0
  37. package/dist/lib/platforms.d.ts +67 -0
  38. package/dist/lib/platforms.d.ts.map +1 -0
  39. package/dist/lib/platforms.js +353 -0
  40. package/dist/lib/platforms.js.map +1 -0
  41. package/dist/lib/rules.d.ts +35 -0
  42. package/dist/lib/rules.d.ts.map +1 -0
  43. package/dist/lib/rules.js +161 -0
  44. package/dist/lib/rules.js.map +1 -0
  45. package/dist/lib/state.d.ts +33 -0
  46. package/dist/lib/state.d.ts.map +1 -0
  47. package/dist/lib/state.js +130 -0
  48. package/dist/lib/state.js.map +1 -0
  49. package/package.json +19 -13
  50. package/registry.json +9 -0
  51. package/index.js +0 -244
  52. package/lib/cli.js +0 -99
  53. package/lib/detect.js +0 -242
  54. package/lib/hooks.js +0 -238
  55. package/lib/mcp.js +0 -503
  56. package/lib/platforms.js +0 -184
  57. 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
- };