coder-config 0.40.1
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 +21 -0
- package/README.md +553 -0
- package/cli.js +431 -0
- package/config-loader.js +294 -0
- package/hooks/activity-track.sh +56 -0
- package/hooks/codex-workstream.sh +44 -0
- package/hooks/gemini-workstream.sh +44 -0
- package/hooks/workstream-inject.sh +20 -0
- package/lib/activity.js +283 -0
- package/lib/apply.js +344 -0
- package/lib/cli.js +267 -0
- package/lib/config.js +171 -0
- package/lib/constants.js +55 -0
- package/lib/env.js +114 -0
- package/lib/index.js +47 -0
- package/lib/init.js +122 -0
- package/lib/mcps.js +139 -0
- package/lib/memory.js +201 -0
- package/lib/projects.js +138 -0
- package/lib/registry.js +83 -0
- package/lib/utils.js +129 -0
- package/lib/workstreams.js +652 -0
- package/package.json +80 -0
- package/scripts/capture-screenshots.js +142 -0
- package/scripts/postinstall.js +122 -0
- package/scripts/release.sh +71 -0
- package/scripts/sync-version.js +77 -0
- package/scripts/tauri-prepare.js +328 -0
- package/shared/mcp-registry.json +76 -0
- package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
- package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
- package/ui/dist/icons/icon-192.svg +16 -0
- package/ui/dist/icons/icon-512.svg +16 -0
- package/ui/dist/index.html +39 -0
- package/ui/dist/manifest.json +25 -0
- package/ui/dist/sw.js +24 -0
- package/ui/dist/tutorial/claude-settings.png +0 -0
- package/ui/dist/tutorial/header.png +0 -0
- package/ui/dist/tutorial/mcp-registry.png +0 -0
- package/ui/dist/tutorial/memory-view.png +0 -0
- package/ui/dist/tutorial/permissions.png +0 -0
- package/ui/dist/tutorial/plugins-view.png +0 -0
- package/ui/dist/tutorial/project-explorer.png +0 -0
- package/ui/dist/tutorial/projects-view.png +0 -0
- package/ui/dist/tutorial/sidebar.png +0 -0
- package/ui/dist/tutorial/tutorial-view.png +0 -0
- package/ui/dist/tutorial/workstreams-view.png +0 -0
- package/ui/routes/activity.js +58 -0
- package/ui/routes/commands.js +74 -0
- package/ui/routes/configs.js +329 -0
- package/ui/routes/env.js +40 -0
- package/ui/routes/file-explorer.js +668 -0
- package/ui/routes/index.js +41 -0
- package/ui/routes/mcp-discovery.js +235 -0
- package/ui/routes/memory.js +385 -0
- package/ui/routes/package.json +3 -0
- package/ui/routes/plugins.js +466 -0
- package/ui/routes/projects.js +198 -0
- package/ui/routes/registry.js +30 -0
- package/ui/routes/rules.js +74 -0
- package/ui/routes/search.js +125 -0
- package/ui/routes/settings.js +381 -0
- package/ui/routes/subprojects.js +208 -0
- package/ui/routes/tool-sync.js +127 -0
- package/ui/routes/updates.js +339 -0
- package/ui/routes/workstreams.js +224 -0
- package/ui/server.cjs +773 -0
- package/ui/terminal-server.cjs +160 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default marketplace to auto-install
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_MARKETPLACE = 'regression-io/claude-config-plugins';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get plugins directory path
|
|
17
|
+
*/
|
|
18
|
+
function getPluginsDir() {
|
|
19
|
+
return path.join(os.homedir(), '.claude', 'plugins');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensure the default marketplace is installed
|
|
24
|
+
* Called automatically on first plugin view load
|
|
25
|
+
*/
|
|
26
|
+
let defaultMarketplaceChecked = false;
|
|
27
|
+
async function ensureDefaultMarketplace() {
|
|
28
|
+
if (defaultMarketplaceChecked) return;
|
|
29
|
+
defaultMarketplaceChecked = true;
|
|
30
|
+
|
|
31
|
+
const pluginsDir = getPluginsDir();
|
|
32
|
+
const marketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
33
|
+
|
|
34
|
+
// Check if any marketplaces exist
|
|
35
|
+
let hasDefaultMarketplace = false;
|
|
36
|
+
if (fs.existsSync(marketplacesPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const known = JSON.parse(fs.readFileSync(marketplacesPath, 'utf8'));
|
|
39
|
+
// Check if our default marketplace is already installed
|
|
40
|
+
hasDefaultMarketplace = Object.values(known).some(
|
|
41
|
+
m => m.source && m.source.includes('claude-config-plugins')
|
|
42
|
+
);
|
|
43
|
+
} catch (e) {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Auto-install if not present
|
|
47
|
+
if (!hasDefaultMarketplace) {
|
|
48
|
+
console.log(`Auto-installing default marketplace: ${DEFAULT_MARKETPLACE}`);
|
|
49
|
+
try {
|
|
50
|
+
await addMarketplaceInternal(DEFAULT_MARKETPLACE);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.warn('Failed to auto-install default marketplace:', e.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Internal marketplace add (no repo parameter needed)
|
|
59
|
+
*/
|
|
60
|
+
function addMarketplaceInternal(repo) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const proc = spawn('claude', ['plugin', 'marketplace', 'add', repo], {
|
|
63
|
+
cwd: os.homedir(),
|
|
64
|
+
env: process.env,
|
|
65
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let stderr = '';
|
|
69
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
70
|
+
|
|
71
|
+
proc.on('close', (code) => {
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
resolve();
|
|
74
|
+
} else {
|
|
75
|
+
reject(new Error(stderr || 'Failed to add marketplace'));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
proc.on('error', reject);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all plugins
|
|
85
|
+
*/
|
|
86
|
+
function getPlugins(manager) {
|
|
87
|
+
const pluginsDir = getPluginsDir();
|
|
88
|
+
const installedPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
89
|
+
const marketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
90
|
+
|
|
91
|
+
// Load installed plugins
|
|
92
|
+
let installed = {};
|
|
93
|
+
if (fs.existsSync(installedPath)) {
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(fs.readFileSync(installedPath, 'utf8'));
|
|
96
|
+
installed = data.plugins || {};
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Load marketplaces and their plugins
|
|
101
|
+
const marketplaces = [];
|
|
102
|
+
const allPlugins = [];
|
|
103
|
+
const categories = new Set();
|
|
104
|
+
|
|
105
|
+
if (fs.existsSync(marketplacesPath)) {
|
|
106
|
+
try {
|
|
107
|
+
const known = JSON.parse(fs.readFileSync(marketplacesPath, 'utf8'));
|
|
108
|
+
for (const [name, info] of Object.entries(known)) {
|
|
109
|
+
const marketplace = {
|
|
110
|
+
name,
|
|
111
|
+
source: info.source,
|
|
112
|
+
installLocation: info.installLocation,
|
|
113
|
+
lastUpdated: info.lastUpdated,
|
|
114
|
+
plugins: [],
|
|
115
|
+
externalPlugins: []
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Track plugin names from manifest to avoid duplicates
|
|
119
|
+
const manifestPluginNames = new Set();
|
|
120
|
+
|
|
121
|
+
// Load marketplace manifest for plugins
|
|
122
|
+
const manifestPath = path.join(info.installLocation, '.claude-plugin', 'marketplace.json');
|
|
123
|
+
if (fs.existsSync(manifestPath)) {
|
|
124
|
+
try {
|
|
125
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
126
|
+
marketplace.description = manifest.description;
|
|
127
|
+
marketplace.owner = manifest.owner;
|
|
128
|
+
marketplace.plugins = (manifest.plugins || []).map(p => {
|
|
129
|
+
if (p.category) categories.add(p.category);
|
|
130
|
+
manifestPluginNames.add(p.name);
|
|
131
|
+
const isExternal = p.source?.includes('external_plugins');
|
|
132
|
+
const plugin = {
|
|
133
|
+
...p,
|
|
134
|
+
marketplace: name,
|
|
135
|
+
sourceType: isExternal ? 'external' : 'internal',
|
|
136
|
+
installed: !!installed[`${p.name}@${name}`],
|
|
137
|
+
installedInfo: installed[`${p.name}@${name}`]?.[0] || null
|
|
138
|
+
};
|
|
139
|
+
allPlugins.push(plugin);
|
|
140
|
+
return plugin;
|
|
141
|
+
});
|
|
142
|
+
} catch (e) {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Load external plugins by scanning external_plugins directory
|
|
146
|
+
const externalDir = path.join(info.installLocation, 'external_plugins');
|
|
147
|
+
if (fs.existsSync(externalDir)) {
|
|
148
|
+
try {
|
|
149
|
+
const externals = fs.readdirSync(externalDir, { withFileTypes: true })
|
|
150
|
+
.filter(d => d.isDirectory())
|
|
151
|
+
.map(d => d.name);
|
|
152
|
+
|
|
153
|
+
for (const pluginName of externals) {
|
|
154
|
+
if (manifestPluginNames.has(pluginName)) continue;
|
|
155
|
+
|
|
156
|
+
const pluginManifestPath = path.join(externalDir, pluginName, '.claude-plugin', 'plugin.json');
|
|
157
|
+
if (fs.existsSync(pluginManifestPath)) {
|
|
158
|
+
try {
|
|
159
|
+
const pluginManifest = JSON.parse(fs.readFileSync(pluginManifestPath, 'utf8'));
|
|
160
|
+
if (manifestPluginNames.has(pluginManifest.name)) continue;
|
|
161
|
+
|
|
162
|
+
if (pluginManifest.category) categories.add(pluginManifest.category);
|
|
163
|
+
const plugin = {
|
|
164
|
+
name: pluginManifest.name || pluginName,
|
|
165
|
+
description: pluginManifest.description || '',
|
|
166
|
+
version: pluginManifest.version || '1.0.0',
|
|
167
|
+
author: pluginManifest.author,
|
|
168
|
+
category: pluginManifest.category || 'external',
|
|
169
|
+
homepage: pluginManifest.homepage,
|
|
170
|
+
mcpServers: pluginManifest.mcpServers,
|
|
171
|
+
lspServers: pluginManifest.lspServers,
|
|
172
|
+
commands: pluginManifest.commands,
|
|
173
|
+
marketplace: name,
|
|
174
|
+
sourceType: 'external',
|
|
175
|
+
installed: !!installed[`${pluginManifest.name || pluginName}@${name}`],
|
|
176
|
+
installedInfo: installed[`${pluginManifest.name || pluginName}@${name}`]?.[0] || null
|
|
177
|
+
};
|
|
178
|
+
marketplace.externalPlugins.push(plugin);
|
|
179
|
+
allPlugins.push(plugin);
|
|
180
|
+
} catch (e) {}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
marketplaces.push(marketplace);
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
installed,
|
|
193
|
+
marketplaces,
|
|
194
|
+
allPlugins,
|
|
195
|
+
categories: Array.from(categories).sort(),
|
|
196
|
+
pluginsDir
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get marketplaces
|
|
202
|
+
*/
|
|
203
|
+
function getMarketplaces() {
|
|
204
|
+
const pluginsDir = getPluginsDir();
|
|
205
|
+
const marketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
206
|
+
|
|
207
|
+
if (fs.existsSync(marketplacesPath)) {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(fs.readFileSync(marketplacesPath, 'utf8'));
|
|
210
|
+
} catch (e) {}
|
|
211
|
+
}
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Install a plugin
|
|
217
|
+
*/
|
|
218
|
+
async function installPlugin(pluginId, marketplace, scope = 'user', projectDir = null) {
|
|
219
|
+
const args = ['plugin', 'install', `${pluginId}@${marketplace}`];
|
|
220
|
+
if (scope && scope !== 'user') {
|
|
221
|
+
args.push('--scope', scope);
|
|
222
|
+
}
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
const proc = spawn('claude', args, {
|
|
225
|
+
cwd: projectDir || os.homedir(),
|
|
226
|
+
env: process.env,
|
|
227
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
let stdout = '';
|
|
231
|
+
let stderr = '';
|
|
232
|
+
|
|
233
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
234
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
235
|
+
|
|
236
|
+
proc.on('close', (code) => {
|
|
237
|
+
if (code === 0) {
|
|
238
|
+
resolve({ success: true, message: stdout || 'Plugin installed' });
|
|
239
|
+
} else {
|
|
240
|
+
resolve({ success: false, error: stderr || stdout || 'Installation failed' });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
proc.on('error', (err) => {
|
|
245
|
+
resolve({ success: false, error: err.message });
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Uninstall a plugin
|
|
252
|
+
*/
|
|
253
|
+
async function uninstallPlugin(pluginId) {
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
const proc = spawn('claude', ['plugin', 'uninstall', pluginId], {
|
|
256
|
+
cwd: os.homedir(),
|
|
257
|
+
env: process.env,
|
|
258
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let stdout = '';
|
|
262
|
+
let stderr = '';
|
|
263
|
+
|
|
264
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
265
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
266
|
+
|
|
267
|
+
proc.on('close', (code) => {
|
|
268
|
+
if (code === 0) {
|
|
269
|
+
resolve({ success: true, message: stdout || 'Plugin uninstalled' });
|
|
270
|
+
} else {
|
|
271
|
+
resolve({ success: false, error: stderr || stdout || 'Uninstallation failed' });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
proc.on('error', (err) => {
|
|
276
|
+
resolve({ success: false, error: err.message });
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Add a marketplace
|
|
283
|
+
*/
|
|
284
|
+
async function addMarketplace(name, repo) {
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
const proc = spawn('claude', ['plugin', 'marketplace', 'add', repo], {
|
|
287
|
+
cwd: os.homedir(),
|
|
288
|
+
env: process.env,
|
|
289
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
let stdout = '';
|
|
293
|
+
let stderr = '';
|
|
294
|
+
|
|
295
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
296
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
297
|
+
|
|
298
|
+
proc.on('close', (code) => {
|
|
299
|
+
if (code === 0) {
|
|
300
|
+
resolve({ success: true, message: stdout || 'Marketplace added' });
|
|
301
|
+
} else {
|
|
302
|
+
resolve({ success: false, error: stderr || stdout || 'Failed to add marketplace' });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
proc.on('error', (err) => {
|
|
307
|
+
resolve({ success: false, error: err.message });
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Refresh a marketplace
|
|
314
|
+
*/
|
|
315
|
+
async function refreshMarketplace(name) {
|
|
316
|
+
return new Promise((resolve) => {
|
|
317
|
+
const proc = spawn('claude', ['plugin', 'marketplace', 'update', name], {
|
|
318
|
+
cwd: os.homedir(),
|
|
319
|
+
env: process.env,
|
|
320
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
let stdout = '';
|
|
324
|
+
let stderr = '';
|
|
325
|
+
|
|
326
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
327
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
328
|
+
|
|
329
|
+
proc.on('close', (code) => {
|
|
330
|
+
if (code === 0) {
|
|
331
|
+
resolve({ success: true, message: stdout || 'Marketplace refreshed' });
|
|
332
|
+
} else {
|
|
333
|
+
resolve({ success: false, error: stderr || stdout || 'Failed to refresh marketplace' });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
proc.on('error', (err) => {
|
|
338
|
+
resolve({ success: false, error: err.message });
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get enabled plugins for a directory (with hierarchy merging)
|
|
345
|
+
*/
|
|
346
|
+
function getEnabledPluginsForDir(manager, dir) {
|
|
347
|
+
const homeDir = os.homedir();
|
|
348
|
+
const configs = manager.findAllConfigs(dir);
|
|
349
|
+
|
|
350
|
+
// Merge enabledPlugins from all configs (child overrides parent)
|
|
351
|
+
const merged = {};
|
|
352
|
+
for (const { configPath } of configs) {
|
|
353
|
+
try {
|
|
354
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
355
|
+
if (config.enabledPlugins) {
|
|
356
|
+
Object.assign(merged, config.enabledPlugins);
|
|
357
|
+
}
|
|
358
|
+
} catch (e) {}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Build per-directory breakdown
|
|
362
|
+
const perDir = configs.map(({ dir: d, configPath }) => {
|
|
363
|
+
let enabledPlugins = {};
|
|
364
|
+
try {
|
|
365
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
366
|
+
enabledPlugins = config.enabledPlugins || {};
|
|
367
|
+
} catch (e) {}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
dir: d,
|
|
371
|
+
label: d === homeDir ? '~' : path.relative(dir, d) || '.',
|
|
372
|
+
enabledPlugins
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
merged,
|
|
378
|
+
perDir
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Set plugin enabled/disabled for a specific directory
|
|
384
|
+
*/
|
|
385
|
+
function setPluginEnabled(manager, dir, pluginId, enabled) {
|
|
386
|
+
const configPath = path.join(dir, '.claude', 'mcps.json');
|
|
387
|
+
const claudeDir = path.join(dir, '.claude');
|
|
388
|
+
|
|
389
|
+
// Ensure .claude directory exists
|
|
390
|
+
if (!fs.existsSync(claudeDir)) {
|
|
391
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Load existing config or create new
|
|
395
|
+
let config = { include: [], mcpServers: {}, enabledPlugins: {} };
|
|
396
|
+
if (fs.existsSync(configPath)) {
|
|
397
|
+
try {
|
|
398
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
399
|
+
if (!config.enabledPlugins) {
|
|
400
|
+
config.enabledPlugins = {};
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Set the plugin state
|
|
406
|
+
if (enabled === null || enabled === undefined) {
|
|
407
|
+
// Remove the override (inherit from parent)
|
|
408
|
+
delete config.enabledPlugins[pluginId];
|
|
409
|
+
} else {
|
|
410
|
+
config.enabledPlugins[pluginId] = enabled;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Clean up empty enabledPlugins
|
|
414
|
+
if (Object.keys(config.enabledPlugins).length === 0) {
|
|
415
|
+
delete config.enabledPlugins;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Save config
|
|
419
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
420
|
+
|
|
421
|
+
return { success: true, dir, pluginId, enabled };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get all plugins with their enabled state per directory
|
|
426
|
+
*/
|
|
427
|
+
function getPluginsWithEnabledState(manager, projectDir) {
|
|
428
|
+
const pluginsData = getPlugins(manager);
|
|
429
|
+
const enabledData = getEnabledPluginsForDir(manager, projectDir);
|
|
430
|
+
|
|
431
|
+
// Add enabled state to each plugin
|
|
432
|
+
const pluginsWithState = pluginsData.allPlugins.map(plugin => {
|
|
433
|
+
const pluginId = `${plugin.name}@${plugin.marketplace}`;
|
|
434
|
+
return {
|
|
435
|
+
...plugin,
|
|
436
|
+
enabledState: {
|
|
437
|
+
merged: enabledData.merged[pluginId] ?? null, // null means no explicit setting
|
|
438
|
+
perDir: enabledData.perDir.map(d => ({
|
|
439
|
+
dir: d.dir,
|
|
440
|
+
label: d.label,
|
|
441
|
+
enabled: d.enabledPlugins[pluginId] ?? null
|
|
442
|
+
}))
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
...pluginsData,
|
|
449
|
+
allPlugins: pluginsWithState,
|
|
450
|
+
enabledPlugins: enabledData
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
getPluginsDir,
|
|
456
|
+
getPlugins,
|
|
457
|
+
getMarketplaces,
|
|
458
|
+
installPlugin,
|
|
459
|
+
uninstallPlugin,
|
|
460
|
+
addMarketplace,
|
|
461
|
+
refreshMarketplace,
|
|
462
|
+
getEnabledPluginsForDir,
|
|
463
|
+
setPluginEnabled,
|
|
464
|
+
getPluginsWithEnabledState,
|
|
465
|
+
ensureDefaultMarketplace,
|
|
466
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects Registry Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get all registered projects with status info
|
|
12
|
+
*/
|
|
13
|
+
function getProjects(manager, projectDir) {
|
|
14
|
+
if (!manager) {
|
|
15
|
+
return { projects: [], activeProjectId: null, error: 'Manager not available' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const registry = manager.loadProjectsRegistry();
|
|
19
|
+
|
|
20
|
+
const projects = registry.projects.map(p => ({
|
|
21
|
+
...p,
|
|
22
|
+
exists: fs.existsSync(p.path),
|
|
23
|
+
hasClaudeConfig: fs.existsSync(path.join(p.path, '.claude')),
|
|
24
|
+
isActive: p.id === registry.activeProjectId
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
projects.sort((a, b) => {
|
|
28
|
+
if (a.isActive) return -1;
|
|
29
|
+
if (b.isActive) return 1;
|
|
30
|
+
if (a.lastOpened && b.lastOpened) {
|
|
31
|
+
return new Date(b.lastOpened) - new Date(a.lastOpened);
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
projects,
|
|
38
|
+
activeProjectId: registry.activeProjectId,
|
|
39
|
+
currentDir: projectDir
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get active project details
|
|
45
|
+
*/
|
|
46
|
+
function getActiveProject(manager, projectDir, getHierarchy, getSubprojects) {
|
|
47
|
+
if (!manager) return { error: 'Manager not available' };
|
|
48
|
+
|
|
49
|
+
const registry = manager.loadProjectsRegistry();
|
|
50
|
+
const activeProject = registry.projects.find(p => p.id === registry.activeProjectId);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
project: activeProject || null,
|
|
54
|
+
dir: projectDir,
|
|
55
|
+
hierarchy: getHierarchy(),
|
|
56
|
+
subprojects: getSubprojects()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a project to the registry
|
|
62
|
+
* @param {boolean} runClaudeInit - If true, run `claude /init` to create CLAUDE.md
|
|
63
|
+
*/
|
|
64
|
+
function addProject(manager, projectPath, name, setProjectDir, runClaudeInit = false) {
|
|
65
|
+
if (!manager) return { error: 'Manager not available' };
|
|
66
|
+
|
|
67
|
+
const absPath = path.resolve(projectPath.replace(/^~/, os.homedir()));
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(absPath)) {
|
|
70
|
+
return { error: 'Path not found', path: absPath };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const registry = manager.loadProjectsRegistry();
|
|
74
|
+
|
|
75
|
+
if (registry.projects.some(p => p.path === absPath)) {
|
|
76
|
+
return { error: 'Project already registered', path: absPath };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const claudeDir = path.join(absPath, '.claude');
|
|
80
|
+
const claudeMd = path.join(absPath, 'CLAUDE.md');
|
|
81
|
+
const mcpsFile = path.join(claudeDir, 'mcps.json');
|
|
82
|
+
let claudeInitRan = false;
|
|
83
|
+
let claudeInitError = null;
|
|
84
|
+
|
|
85
|
+
// Run claude /init if requested and CLAUDE.md doesn't exist
|
|
86
|
+
if (runClaudeInit && !fs.existsSync(claudeMd)) {
|
|
87
|
+
try {
|
|
88
|
+
execFileSync('claude', ['/init'], {
|
|
89
|
+
cwd: absPath,
|
|
90
|
+
stdio: 'pipe',
|
|
91
|
+
timeout: 30000
|
|
92
|
+
});
|
|
93
|
+
claudeInitRan = true;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// Claude Code not installed or init failed
|
|
96
|
+
claudeInitError = err.message;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ensure .claude/mcps.json exists (for claude-config to work)
|
|
101
|
+
if (!fs.existsSync(claudeDir)) {
|
|
102
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
if (!fs.existsSync(mcpsFile)) {
|
|
105
|
+
fs.writeFileSync(mcpsFile, JSON.stringify({ mcpServers: {} }, null, 2));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const project = {
|
|
109
|
+
id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
|
|
110
|
+
name: name || path.basename(absPath),
|
|
111
|
+
path: absPath,
|
|
112
|
+
addedAt: new Date().toISOString(),
|
|
113
|
+
lastOpened: null
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
registry.projects.push(project);
|
|
117
|
+
|
|
118
|
+
if (!registry.activeProjectId) {
|
|
119
|
+
registry.activeProjectId = project.id;
|
|
120
|
+
setProjectDir(absPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
manager.saveProjectsRegistry(registry);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
project,
|
|
128
|
+
claudeInitRan,
|
|
129
|
+
claudeInitError
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove a project from the registry
|
|
135
|
+
*/
|
|
136
|
+
function removeProject(manager, projectId, setProjectDir) {
|
|
137
|
+
if (!manager) return { error: 'Manager not available' };
|
|
138
|
+
|
|
139
|
+
const registry = manager.loadProjectsRegistry();
|
|
140
|
+
const idx = registry.projects.findIndex(p => p.id === projectId);
|
|
141
|
+
|
|
142
|
+
if (idx === -1) {
|
|
143
|
+
return { error: 'Project not found' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const removed = registry.projects.splice(idx, 1)[0];
|
|
147
|
+
|
|
148
|
+
if (registry.activeProjectId === projectId) {
|
|
149
|
+
registry.activeProjectId = registry.projects[0]?.id || null;
|
|
150
|
+
if (registry.projects[0]) {
|
|
151
|
+
setProjectDir(registry.projects[0].path);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
manager.saveProjectsRegistry(registry);
|
|
156
|
+
|
|
157
|
+
return { success: true, removed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set active project and switch server context
|
|
162
|
+
*/
|
|
163
|
+
function setActiveProject(manager, projectId, setProjectDir, getHierarchy, getSubprojects) {
|
|
164
|
+
if (!manager) return { error: 'Manager not available' };
|
|
165
|
+
|
|
166
|
+
const registry = manager.loadProjectsRegistry();
|
|
167
|
+
const project = registry.projects.find(p => p.id === projectId);
|
|
168
|
+
|
|
169
|
+
if (!project) {
|
|
170
|
+
return { error: 'Project not found' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!fs.existsSync(project.path)) {
|
|
174
|
+
return { error: 'Project path no longer exists', path: project.path };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
registry.activeProjectId = projectId;
|
|
178
|
+
project.lastOpened = new Date().toISOString();
|
|
179
|
+
manager.saveProjectsRegistry(registry);
|
|
180
|
+
|
|
181
|
+
setProjectDir(project.path);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
project,
|
|
186
|
+
dir: project.path,
|
|
187
|
+
hierarchy: getHierarchy(),
|
|
188
|
+
subprojects: getSubprojects()
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
getProjects,
|
|
194
|
+
getActiveProject,
|
|
195
|
+
addProject,
|
|
196
|
+
removeProject,
|
|
197
|
+
setActiveProject,
|
|
198
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Registry Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get MCP registry
|
|
10
|
+
*/
|
|
11
|
+
function getRegistry(manager) {
|
|
12
|
+
return manager.loadJson(manager.registryPath) || { mcpServers: {} };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Update MCP registry
|
|
17
|
+
*/
|
|
18
|
+
function updateRegistry(manager, body) {
|
|
19
|
+
try {
|
|
20
|
+
manager.saveJson(manager.registryPath, body);
|
|
21
|
+
return { success: true };
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return { error: e.message };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
getRegistry,
|
|
29
|
+
updateRegistry,
|
|
30
|
+
};
|