@phnx-labs/agents-cli 1.15.0 → 1.17.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/CHANGELOG.md +143 -39
- package/README.md +6 -6
- package/dist/commands/alias.js +2 -2
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +793 -83
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +70 -1
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +81 -39
- package/dist/commands/plugins.js +224 -17
- package/dist/commands/prune.js +29 -1
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +154 -20
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +22 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/view.js +78 -20
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +48 -36
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +32 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +41 -3
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +22 -6
- package/dist/lib/browser/drivers/ssh.js +9 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +158 -23
- package/dist/lib/browser/profiles.d.ts +10 -2
- package/dist/lib/browser/profiles.js +122 -37
- package/dist/lib/browser/service.d.ts +91 -13
- package/dist/lib/browser/service.js +767 -132
- package/dist/lib/browser/types.d.ts +91 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +69 -14
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -15
- package/dist/lib/commands.js +11 -7
- package/dist/lib/daemon.js +2 -3
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.js +2 -2
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +138 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1237 -22
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +8 -66
- package/dist/lib/permissions.js +18 -18
- package/dist/lib/plugins.d.ts +94 -24
- package/dist/lib/plugins.js +702 -123
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +109 -5
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +55 -29
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +4 -52
- package/dist/lib/shims.js +23 -15
- package/dist/lib/skills.js +6 -2
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +101 -16
- package/dist/lib/state.js +179 -31
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +75 -17
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.js +2 -2
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +158 -47
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +60 -59
- package/dist/commands/fork.d.ts +0 -10
- package/dist/commands/fork.js +0 -146
package/dist/lib/plugins.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plugin discovery, validation, and syncing.
|
|
3
3
|
*
|
|
4
|
-
* Plugins are bundles in ~/.agents/plugins/ that package skills, hooks,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Plugins are bundles in ~/.agents/.cache/plugins/ that package skills, hooks,
|
|
5
|
+
* commands, agents, bin scripts, MCP servers, and settings under a single
|
|
6
|
+
* manifest (plugin.json). This module discovers plugins, validates their
|
|
7
|
+
* manifests, and syncs their contents into agent version homes.
|
|
7
8
|
*/
|
|
8
9
|
import * as fs from 'fs';
|
|
9
10
|
import * as path from 'path';
|
|
10
|
-
import {
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { getPluginsDir, getTrashPluginsDir } from './state.js';
|
|
13
|
+
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
11
14
|
import { AGENTS, PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
12
15
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
13
16
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
17
|
+
const USER_CONFIG_FILE = '.user-config.json';
|
|
18
|
+
const SOURCE_FILE = '.source';
|
|
14
19
|
/**
|
|
15
|
-
* Discover all plugins in ~/.agents/plugins/.
|
|
20
|
+
* Discover all plugins in ~/.agents/.cache/plugins/.
|
|
16
21
|
* A valid plugin has a .claude-plugin/plugin.json manifest.
|
|
17
22
|
*/
|
|
18
23
|
export function discoverPlugins() {
|
|
@@ -29,17 +34,25 @@ export function discoverPlugins() {
|
|
|
29
34
|
const manifest = loadPluginManifest(pluginRoot);
|
|
30
35
|
if (!manifest)
|
|
31
36
|
continue;
|
|
32
|
-
plugins.push(
|
|
33
|
-
name: manifest.name,
|
|
34
|
-
root: pluginRoot,
|
|
35
|
-
manifest,
|
|
36
|
-
skills: discoverPluginSkills(pluginRoot),
|
|
37
|
-
hooks: discoverPluginHooks(pluginRoot),
|
|
38
|
-
scripts: discoverPluginScripts(pluginRoot),
|
|
39
|
-
});
|
|
37
|
+
plugins.push(buildDiscoveredPlugin(pluginRoot, manifest));
|
|
40
38
|
}
|
|
41
39
|
return plugins;
|
|
42
40
|
}
|
|
41
|
+
export function buildDiscoveredPlugin(pluginRoot, manifest) {
|
|
42
|
+
return {
|
|
43
|
+
name: manifest.name,
|
|
44
|
+
root: pluginRoot,
|
|
45
|
+
manifest,
|
|
46
|
+
skills: discoverPluginSkills(pluginRoot),
|
|
47
|
+
hooks: discoverPluginHooks(pluginRoot),
|
|
48
|
+
scripts: discoverPluginScripts(pluginRoot),
|
|
49
|
+
commands: discoverPluginCommands(pluginRoot),
|
|
50
|
+
agentDefs: discoverPluginAgentDefs(pluginRoot),
|
|
51
|
+
bin: discoverPluginBin(pluginRoot),
|
|
52
|
+
hasMcp: fs.existsSync(path.join(pluginRoot, '.mcp.json')),
|
|
53
|
+
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
43
56
|
/**
|
|
44
57
|
* Load a plugin manifest from a plugin directory.
|
|
45
58
|
*/
|
|
@@ -82,9 +95,7 @@ export function pluginSupportsAgent(plugin, agent) {
|
|
|
82
95
|
}
|
|
83
96
|
return true;
|
|
84
97
|
}
|
|
85
|
-
|
|
86
|
-
* Discover skill directories inside a plugin.
|
|
87
|
-
*/
|
|
98
|
+
// ─── Discovery helpers ────────────────────────────────────────────────────────
|
|
88
99
|
function discoverPluginSkills(pluginRoot) {
|
|
89
100
|
const skillsDir = path.join(pluginRoot, 'skills');
|
|
90
101
|
if (!fs.existsSync(skillsDir))
|
|
@@ -93,9 +104,6 @@ function discoverPluginSkills(pluginRoot) {
|
|
|
93
104
|
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
94
105
|
.map(d => d.name);
|
|
95
106
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Discover hook definitions inside a plugin.
|
|
98
|
-
*/
|
|
99
107
|
function discoverPluginHooks(pluginRoot) {
|
|
100
108
|
const hooksFile = path.join(pluginRoot, 'hooks', 'hooks.json');
|
|
101
109
|
if (!fs.existsSync(hooksFile))
|
|
@@ -108,85 +116,198 @@ function discoverPluginHooks(pluginRoot) {
|
|
|
108
116
|
return [];
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Discover scripts inside a plugin.
|
|
113
|
-
*/
|
|
114
119
|
function discoverPluginScripts(pluginRoot) {
|
|
115
120
|
const scriptsDir = path.join(pluginRoot, 'scripts');
|
|
116
121
|
if (!fs.existsSync(scriptsDir))
|
|
117
122
|
return [];
|
|
118
123
|
return fs.readdirSync(scriptsDir).filter(f => !f.startsWith('.'));
|
|
119
124
|
}
|
|
125
|
+
/** Discover command .md files inside a plugin's commands/ directory. */
|
|
126
|
+
export function discoverPluginCommands(pluginRoot) {
|
|
127
|
+
const commandsDir = path.join(pluginRoot, 'commands');
|
|
128
|
+
if (!fs.existsSync(commandsDir))
|
|
129
|
+
return [];
|
|
130
|
+
return fs.readdirSync(commandsDir)
|
|
131
|
+
.filter(f => f.endsWith('.md') && !f.startsWith('.'))
|
|
132
|
+
.map(f => f.slice(0, -3));
|
|
133
|
+
}
|
|
134
|
+
/** Discover agent definition .md files inside a plugin's agents/ directory. */
|
|
135
|
+
export function discoverPluginAgentDefs(pluginRoot) {
|
|
136
|
+
const agentsDir = path.join(pluginRoot, 'agents');
|
|
137
|
+
if (!fs.existsSync(agentsDir))
|
|
138
|
+
return [];
|
|
139
|
+
return fs.readdirSync(agentsDir)
|
|
140
|
+
.filter(f => f.endsWith('.md') && !f.startsWith('.'))
|
|
141
|
+
.map(f => f.slice(0, -3));
|
|
142
|
+
}
|
|
143
|
+
/** Discover executable files in a plugin's bin/ directory. */
|
|
144
|
+
export function discoverPluginBin(pluginRoot) {
|
|
145
|
+
const binDir = path.join(pluginRoot, 'bin');
|
|
146
|
+
if (!fs.existsSync(binDir))
|
|
147
|
+
return [];
|
|
148
|
+
return fs.readdirSync(binDir).filter(f => !f.startsWith('.'));
|
|
149
|
+
}
|
|
150
|
+
/** Return true if settings.json contains non-permission keys worth merging. */
|
|
151
|
+
function pluginHasNonPermissionSettings(pluginRoot) {
|
|
152
|
+
const settingsPath = path.join(pluginRoot, 'settings.json');
|
|
153
|
+
if (!fs.existsSync(settingsPath))
|
|
154
|
+
return false;
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
157
|
+
return Object.keys(parsed).some(k => k !== 'permissions');
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ─── Variable expansion ───────────────────────────────────────────────────────
|
|
120
164
|
/**
|
|
121
165
|
* Expand plugin variables in a string.
|
|
122
166
|
*
|
|
123
167
|
* Variables:
|
|
124
|
-
* ${CLAUDE_PLUGIN_ROOT}
|
|
125
|
-
* ${CLAUDE_PLUGIN_DATA}
|
|
168
|
+
* ${CLAUDE_PLUGIN_ROOT} -> absolute path to plugin directory
|
|
169
|
+
* ${CLAUDE_PLUGIN_DATA} -> per-version data directory for this plugin
|
|
170
|
+
* ${user_config.<key>} -> value from plugin's .user-config.json
|
|
126
171
|
*/
|
|
127
|
-
export function expandPluginVars(str, pluginRoot, pluginName, agentId, versionHome) {
|
|
172
|
+
export function expandPluginVars(str, pluginRoot, pluginName, agentId, versionHome, userConfig) {
|
|
128
173
|
const dataDir = path.join(versionHome, `.${agentId}`, 'plugin-data', pluginName);
|
|
129
|
-
|
|
174
|
+
let result = str
|
|
130
175
|
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginRoot)
|
|
131
176
|
.replace(/\$\{CLAUDE_PLUGIN_DATA\}/g, dataDir);
|
|
177
|
+
if (userConfig && Object.keys(userConfig).length > 0) {
|
|
178
|
+
result = result.replace(/\$\{user_config\.([^}]+)\}/g, (_, key) => {
|
|
179
|
+
return userConfig[key] ?? '';
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
132
183
|
}
|
|
184
|
+
// ─── userConfig storage ───────────────────────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* Load persisted user config for a plugin from .user-config.json.
|
|
187
|
+
*/
|
|
188
|
+
export function loadUserConfig(pluginName) {
|
|
189
|
+
const configPath = path.join(getPluginsDir(), pluginName, USER_CONFIG_FILE);
|
|
190
|
+
if (!fs.existsSync(configPath))
|
|
191
|
+
return {};
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Persist user config for a plugin to .user-config.json.
|
|
201
|
+
*/
|
|
202
|
+
export function saveUserConfig(pluginName, config) {
|
|
203
|
+
const configPath = path.join(getPluginsDir(), pluginName, USER_CONFIG_FILE);
|
|
204
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
205
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
206
|
+
}
|
|
207
|
+
// ─── Dependency checking ──────────────────────────────────────────────────────
|
|
208
|
+
/**
|
|
209
|
+
* Check plugin dependencies against installed plugins.
|
|
210
|
+
* Returns names of missing dependencies (warning only — not a hard error).
|
|
211
|
+
*/
|
|
212
|
+
export function checkPluginDependencies(manifest) {
|
|
213
|
+
if (!manifest.dependencies || manifest.dependencies.length === 0)
|
|
214
|
+
return [];
|
|
215
|
+
const installed = new Set(discoverPlugins().map(p => p.name));
|
|
216
|
+
return manifest.dependencies.filter(dep => !installed.has(dep));
|
|
217
|
+
}
|
|
218
|
+
// ─── Main sync entry point ────────────────────────────────────────────────────
|
|
133
219
|
/**
|
|
134
220
|
* Sync a plugin to a specific agent version's home directory.
|
|
135
221
|
*
|
|
136
222
|
* For Claude:
|
|
137
|
-
* 1. Copy plugin skills into version's skills dir (prefixed: pluginName
|
|
138
|
-
* 2.
|
|
139
|
-
* 3.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
223
|
+
* 1. Copy plugin skills into version's skills dir (prefixed: pluginName--skillName)
|
|
224
|
+
* 2. Copy plugin commands into version's commands dir (prefixed: pluginName--cmdName.md)
|
|
225
|
+
* 3. Copy plugin agent defs into version's agents dir (prefixed: pluginName--agentName.md)
|
|
226
|
+
* 4. Copy plugin bin/ into version home plugin-bin/<pluginName>/, note path in settings
|
|
227
|
+
* 5. Read hooks/hooks.json, expand vars, merge into settings.json hooks
|
|
228
|
+
* 6. Read .mcp.json, expand vars, merge mcpServers into settings.json
|
|
229
|
+
* 7. Read settings.json, merge non-permission keys non-destructively into settings.json
|
|
230
|
+
* 8. Read settings.json permissions, expand vars, merge into settings.json
|
|
143
231
|
*/
|
|
144
232
|
export function syncPluginToVersion(plugin, agent, versionHome) {
|
|
145
|
-
const result = {
|
|
233
|
+
const result = {
|
|
234
|
+
success: false,
|
|
235
|
+
skills: [],
|
|
236
|
+
commands: [],
|
|
237
|
+
agentDefs: [],
|
|
238
|
+
bin: [],
|
|
239
|
+
hooks: [],
|
|
240
|
+
permissions: false,
|
|
241
|
+
mcp: false,
|
|
242
|
+
settings: false,
|
|
243
|
+
};
|
|
146
244
|
if (!pluginSupportsAgent(plugin, agent)) {
|
|
147
245
|
return result;
|
|
148
246
|
}
|
|
247
|
+
const userConfig = loadUserConfig(plugin.name);
|
|
149
248
|
// 1. Sync skills
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
249
|
+
result.skills = syncPluginSkills(plugin, agent, versionHome, userConfig);
|
|
250
|
+
// 2. Sync commands (Claude + compatible agents only)
|
|
251
|
+
if (agent === 'claude' || agent === 'openclaw') {
|
|
252
|
+
result.commands = syncPluginCommands(plugin, agent, versionHome, userConfig);
|
|
253
|
+
}
|
|
254
|
+
// 3. Sync agent defs (Claude only)
|
|
255
|
+
if (agent === 'claude') {
|
|
256
|
+
result.agentDefs = syncPluginAgentDefs(plugin, agent, versionHome, userConfig);
|
|
257
|
+
}
|
|
258
|
+
// 4. Sync bin executables (Claude only for now)
|
|
153
259
|
if (agent === 'claude') {
|
|
154
|
-
|
|
155
|
-
result.hooks = hooksResult;
|
|
260
|
+
result.bin = syncPluginBin(plugin, agent, versionHome);
|
|
156
261
|
}
|
|
157
|
-
//
|
|
262
|
+
// 5. Sync hooks (Claude only - uses settings.json hook registration)
|
|
158
263
|
if (agent === 'claude') {
|
|
159
|
-
result.
|
|
264
|
+
result.hooks = syncPluginHooks(plugin, agent, versionHome, userConfig);
|
|
160
265
|
}
|
|
161
|
-
|
|
266
|
+
// 6. Sync MCP servers (Claude only)
|
|
267
|
+
if (agent === 'claude') {
|
|
268
|
+
result.mcp = syncPluginMcp(plugin, agent, versionHome, userConfig);
|
|
269
|
+
}
|
|
270
|
+
// 7. Merge non-permission settings keys non-destructively (Claude only)
|
|
271
|
+
if (agent === 'claude') {
|
|
272
|
+
result.settings = syncPluginSettings(plugin, agent, versionHome);
|
|
273
|
+
}
|
|
274
|
+
// 8. Sync permissions (Claude only - uses settings.json permissions)
|
|
275
|
+
if (agent === 'claude') {
|
|
276
|
+
result.permissions = syncPluginPermissions(plugin, agent, versionHome, userConfig);
|
|
277
|
+
}
|
|
278
|
+
result.success =
|
|
279
|
+
result.skills.length > 0 ||
|
|
280
|
+
result.commands.length > 0 ||
|
|
281
|
+
result.agentDefs.length > 0 ||
|
|
282
|
+
result.bin.length > 0 ||
|
|
283
|
+
result.hooks.length > 0 ||
|
|
284
|
+
result.mcp ||
|
|
285
|
+
result.settings ||
|
|
286
|
+
result.permissions;
|
|
162
287
|
return result;
|
|
163
288
|
}
|
|
289
|
+
// ─── Individual sync functions ────────────────────────────────────────────────
|
|
164
290
|
/**
|
|
165
291
|
* Copy plugin skills into the version's skills directory.
|
|
166
|
-
* Skills are prefixed with the plugin name: pluginName
|
|
292
|
+
* Skills are prefixed with the plugin name: pluginName--skillName
|
|
167
293
|
*/
|
|
168
|
-
function syncPluginSkills(plugin, agent, versionHome) {
|
|
294
|
+
function syncPluginSkills(plugin, agent, versionHome, userConfig) {
|
|
169
295
|
const synced = [];
|
|
170
296
|
const pluginSkillsDir = path.join(plugin.root, 'skills');
|
|
171
297
|
if (!fs.existsSync(pluginSkillsDir))
|
|
172
298
|
return synced;
|
|
173
|
-
const agentConfig = AGENTS[agent];
|
|
174
299
|
const targetSkillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
175
300
|
fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
176
301
|
for (const skillName of plugin.skills) {
|
|
177
302
|
const srcDir = path.join(pluginSkillsDir, skillName);
|
|
178
|
-
|
|
179
|
-
const prefixedName = `${plugin.name}:${skillName}`;
|
|
180
|
-
// Use colon-to-dash for filesystem (colons not allowed on some systems)
|
|
181
|
-
const fsSafeName = prefixedName.replace(/:/g, '--');
|
|
303
|
+
const fsSafeName = `${plugin.name}--${skillName}`;
|
|
182
304
|
const destDir = path.join(targetSkillsDir, fsSafeName);
|
|
183
305
|
try {
|
|
184
|
-
// Remove existing and copy fresh
|
|
185
306
|
if (fs.existsSync(destDir)) {
|
|
186
307
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
187
308
|
}
|
|
188
|
-
copyDirWithVarExpansion(srcDir, destDir, plugin.root, plugin.name, agent, versionHome);
|
|
189
|
-
synced.push(
|
|
309
|
+
copyDirWithVarExpansion(srcDir, destDir, plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
310
|
+
synced.push(`${plugin.name}:${skillName}`);
|
|
190
311
|
}
|
|
191
312
|
catch {
|
|
192
313
|
// Skip on error
|
|
@@ -194,12 +315,123 @@ function syncPluginSkills(plugin, agent, versionHome) {
|
|
|
194
315
|
}
|
|
195
316
|
return synced;
|
|
196
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Copy plugin commands into the version's commands directory.
|
|
320
|
+
* Commands are namespaced: pluginName--commandName.md
|
|
321
|
+
*/
|
|
322
|
+
function syncPluginCommands(plugin, agent, versionHome, userConfig) {
|
|
323
|
+
const synced = [];
|
|
324
|
+
const pluginCommandsDir = path.join(plugin.root, 'commands');
|
|
325
|
+
if (!fs.existsSync(pluginCommandsDir) || plugin.commands.length === 0)
|
|
326
|
+
return synced;
|
|
327
|
+
const agentConfig = AGENTS[agent];
|
|
328
|
+
const commandsTarget = path.join(versionHome, `.${agent}`, agentConfig.commandsSubdir);
|
|
329
|
+
fs.mkdirSync(commandsTarget, { recursive: true });
|
|
330
|
+
for (const cmdName of plugin.commands) {
|
|
331
|
+
const srcFile = path.join(pluginCommandsDir, `${cmdName}.md`);
|
|
332
|
+
if (!fs.existsSync(srcFile))
|
|
333
|
+
continue;
|
|
334
|
+
const destName = `${plugin.name}--${cmdName}.md`;
|
|
335
|
+
const destFile = path.join(commandsTarget, destName);
|
|
336
|
+
try {
|
|
337
|
+
let content = fs.readFileSync(srcFile, 'utf-8');
|
|
338
|
+
content = expandPluginVars(content, plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
339
|
+
fs.writeFileSync(destFile, content, 'utf-8');
|
|
340
|
+
synced.push(`${plugin.name}:${cmdName}`);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Skip on error
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return synced;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Copy plugin agent definitions into the version's agents directory.
|
|
350
|
+
* Agent defs are namespaced: pluginName--agentName.md
|
|
351
|
+
*/
|
|
352
|
+
function syncPluginAgentDefs(plugin, agent, versionHome, userConfig) {
|
|
353
|
+
const synced = [];
|
|
354
|
+
const pluginAgentsDir = path.join(plugin.root, 'agents');
|
|
355
|
+
if (!fs.existsSync(pluginAgentsDir) || plugin.agentDefs.length === 0)
|
|
356
|
+
return synced;
|
|
357
|
+
const agentsTarget = path.join(versionHome, `.${agent}`, 'agents');
|
|
358
|
+
fs.mkdirSync(agentsTarget, { recursive: true });
|
|
359
|
+
for (const agentDefName of plugin.agentDefs) {
|
|
360
|
+
const srcFile = path.join(pluginAgentsDir, `${agentDefName}.md`);
|
|
361
|
+
if (!fs.existsSync(srcFile))
|
|
362
|
+
continue;
|
|
363
|
+
const destName = `${plugin.name}--${agentDefName}.md`;
|
|
364
|
+
const destFile = path.join(agentsTarget, destName);
|
|
365
|
+
try {
|
|
366
|
+
let content = fs.readFileSync(srcFile, 'utf-8');
|
|
367
|
+
content = expandPluginVars(content, plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
368
|
+
fs.writeFileSync(destFile, content, 'utf-8');
|
|
369
|
+
synced.push(`${plugin.name}:${agentDefName}`);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
// Skip on error
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return synced;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Copy plugin bin executables into the version home plugin-bin/<pluginName>/ directory.
|
|
379
|
+
* Records the bin path in settings.json under pluginBinPaths so callers can add it to PATH.
|
|
380
|
+
*/
|
|
381
|
+
function syncPluginBin(plugin, agent, versionHome) {
|
|
382
|
+
const synced = [];
|
|
383
|
+
const pluginBinDir = path.join(plugin.root, 'bin');
|
|
384
|
+
if (!fs.existsSync(pluginBinDir) || plugin.bin.length === 0)
|
|
385
|
+
return synced;
|
|
386
|
+
const targetBinDir = path.join(versionHome, `.${agent}`, 'plugin-bin', plugin.name);
|
|
387
|
+
fs.mkdirSync(targetBinDir, { recursive: true });
|
|
388
|
+
for (const binFile of plugin.bin) {
|
|
389
|
+
const srcFile = path.join(pluginBinDir, binFile);
|
|
390
|
+
if (!fs.existsSync(srcFile))
|
|
391
|
+
continue;
|
|
392
|
+
const destFile = path.join(targetBinDir, binFile);
|
|
393
|
+
try {
|
|
394
|
+
fs.copyFileSync(srcFile, destFile);
|
|
395
|
+
const stat = fs.statSync(srcFile);
|
|
396
|
+
fs.chmodSync(destFile, stat.mode | 0o111);
|
|
397
|
+
synced.push(binFile);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Skip on error
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (synced.length === 0)
|
|
404
|
+
return synced;
|
|
405
|
+
// Note the bin path in settings.json so the calling shim can add it to PATH.
|
|
406
|
+
const configDir = path.join(versionHome, `.${agent}`);
|
|
407
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
408
|
+
let settings = {};
|
|
409
|
+
if (fs.existsSync(settingsPath)) {
|
|
410
|
+
try {
|
|
411
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
412
|
+
}
|
|
413
|
+
catch { /* start fresh */ }
|
|
414
|
+
}
|
|
415
|
+
if (!Array.isArray(settings.pluginBinPaths)) {
|
|
416
|
+
settings.pluginBinPaths = [];
|
|
417
|
+
}
|
|
418
|
+
const binPaths = settings.pluginBinPaths;
|
|
419
|
+
if (!binPaths.includes(targetBinDir)) {
|
|
420
|
+
binPaths.push(targetBinDir);
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
424
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
425
|
+
}
|
|
426
|
+
catch { /* ignore write errors */ }
|
|
427
|
+
return synced;
|
|
428
|
+
}
|
|
197
429
|
/**
|
|
198
430
|
* Merge plugin hooks into Claude's settings.json.
|
|
199
431
|
* Reads the plugin's hooks/hooks.json and merges each event's hooks
|
|
200
432
|
* into the version's settings.json, expanding variables.
|
|
201
433
|
*/
|
|
202
|
-
function syncPluginHooks(plugin, agent, versionHome) {
|
|
434
|
+
function syncPluginHooks(plugin, agent, versionHome, userConfig) {
|
|
203
435
|
const synced = [];
|
|
204
436
|
const hooksFile = path.join(plugin.root, 'hooks', 'hooks.json');
|
|
205
437
|
if (!fs.existsSync(hooksFile))
|
|
@@ -211,7 +443,6 @@ function syncPluginHooks(plugin, agent, versionHome) {
|
|
|
211
443
|
catch {
|
|
212
444
|
return synced;
|
|
213
445
|
}
|
|
214
|
-
// Read existing settings.json
|
|
215
446
|
const configDir = path.join(versionHome, `.${agent}`);
|
|
216
447
|
const settingsPath = path.join(configDir, 'settings.json');
|
|
217
448
|
let settings = {};
|
|
@@ -219,15 +450,12 @@ function syncPluginHooks(plugin, agent, versionHome) {
|
|
|
219
450
|
try {
|
|
220
451
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
221
452
|
}
|
|
222
|
-
catch {
|
|
223
|
-
// Start fresh if parse fails
|
|
224
|
-
}
|
|
453
|
+
catch { /* start fresh */ }
|
|
225
454
|
}
|
|
226
455
|
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
|
227
456
|
settings.hooks = {};
|
|
228
457
|
}
|
|
229
458
|
const hooksConfig = settings.hooks;
|
|
230
|
-
// Merge each event from the plugin
|
|
231
459
|
for (const [event, matcherGroups] of Object.entries(pluginHooks)) {
|
|
232
460
|
if (!hooksConfig[event]) {
|
|
233
461
|
hooksConfig[event] = [];
|
|
@@ -237,9 +465,8 @@ function syncPluginHooks(plugin, agent, versionHome) {
|
|
|
237
465
|
const matcher = group.matcher || '';
|
|
238
466
|
const expandedHooks = (group.hooks || []).map(h => ({
|
|
239
467
|
...h,
|
|
240
|
-
command: expandPluginVars(h.command, plugin.root, plugin.name, agent, versionHome),
|
|
468
|
+
command: expandPluginVars(h.command, plugin.root, plugin.name, agent, versionHome, userConfig),
|
|
241
469
|
}));
|
|
242
|
-
// Find or create matcher group
|
|
243
470
|
let matcherGroup = eventEntries.find(e => (e.matcher || '') === matcher);
|
|
244
471
|
if (!matcherGroup) {
|
|
245
472
|
matcherGroup = { matcher, hooks: [] };
|
|
@@ -248,7 +475,6 @@ function syncPluginHooks(plugin, agent, versionHome) {
|
|
|
248
475
|
if (!matcherGroup.hooks) {
|
|
249
476
|
matcherGroup.hooks = [];
|
|
250
477
|
}
|
|
251
|
-
// Add hooks that aren't already registered (by command path)
|
|
252
478
|
for (const hook of expandedHooks) {
|
|
253
479
|
const exists = matcherGroup.hooks.some(h => h.command === hook.command);
|
|
254
480
|
if (!exists) {
|
|
@@ -258,21 +484,116 @@ function syncPluginHooks(plugin, agent, versionHome) {
|
|
|
258
484
|
}
|
|
259
485
|
synced.push(event);
|
|
260
486
|
}
|
|
261
|
-
// Write back
|
|
262
487
|
try {
|
|
263
488
|
fs.mkdirSync(configDir, { recursive: true });
|
|
264
489
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
265
490
|
}
|
|
491
|
+
catch { /* ignore write errors */ }
|
|
492
|
+
return synced;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Merge plugin .mcp.json MCP server definitions into Claude's settings.json.
|
|
496
|
+
* Server names are namespaced as pluginName--serverName to avoid collisions.
|
|
497
|
+
* Expands ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${user_config.*} in args and env.
|
|
498
|
+
*/
|
|
499
|
+
function syncPluginMcp(plugin, agent, versionHome, userConfig) {
|
|
500
|
+
const mcpFile = path.join(plugin.root, '.mcp.json');
|
|
501
|
+
if (!fs.existsSync(mcpFile))
|
|
502
|
+
return false;
|
|
503
|
+
let pluginMcp;
|
|
504
|
+
try {
|
|
505
|
+
pluginMcp = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
|
|
506
|
+
}
|
|
266
507
|
catch {
|
|
267
|
-
|
|
508
|
+
return false;
|
|
268
509
|
}
|
|
269
|
-
|
|
510
|
+
const servers = pluginMcp.mcpServers;
|
|
511
|
+
if (!servers || Object.keys(servers).length === 0)
|
|
512
|
+
return false;
|
|
513
|
+
const configDir = path.join(versionHome, `.${agent}`);
|
|
514
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
515
|
+
let settings = {};
|
|
516
|
+
if (fs.existsSync(settingsPath)) {
|
|
517
|
+
try {
|
|
518
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
519
|
+
}
|
|
520
|
+
catch { /* start fresh */ }
|
|
521
|
+
}
|
|
522
|
+
if (!settings.mcpServers || typeof settings.mcpServers !== 'object') {
|
|
523
|
+
settings.mcpServers = {};
|
|
524
|
+
}
|
|
525
|
+
const existing = settings.mcpServers;
|
|
526
|
+
let merged = false;
|
|
527
|
+
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
528
|
+
const namespacedName = `${plugin.name}--${serverName}`;
|
|
529
|
+
// Expand variables inside the server config
|
|
530
|
+
const configStr = expandPluginVars(JSON.stringify(serverConfig), plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
531
|
+
existing[namespacedName] = JSON.parse(configStr);
|
|
532
|
+
merged = true;
|
|
533
|
+
}
|
|
534
|
+
if (merged) {
|
|
535
|
+
try {
|
|
536
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
537
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return merged;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Merge non-permission keys from plugin's settings.json non-destructively into agent settings.
|
|
547
|
+
* Only adds keys that don't already exist — never overwrites user config.
|
|
548
|
+
* Permission keys are handled separately by syncPluginPermissions.
|
|
549
|
+
*/
|
|
550
|
+
function syncPluginSettings(plugin, agent, versionHome) {
|
|
551
|
+
const pluginSettingsPath = path.join(plugin.root, 'settings.json');
|
|
552
|
+
if (!fs.existsSync(pluginSettingsPath))
|
|
553
|
+
return false;
|
|
554
|
+
let pluginSettings;
|
|
555
|
+
try {
|
|
556
|
+
pluginSettings = JSON.parse(fs.readFileSync(pluginSettingsPath, 'utf-8'));
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
// Exclude permissions — those are handled by syncPluginPermissions
|
|
562
|
+
const keysToMerge = Object.entries(pluginSettings).filter(([k]) => k !== 'permissions');
|
|
563
|
+
if (keysToMerge.length === 0)
|
|
564
|
+
return false;
|
|
565
|
+
const configDir = path.join(versionHome, `.${agent}`);
|
|
566
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
567
|
+
let settings = {};
|
|
568
|
+
if (fs.existsSync(settingsPath)) {
|
|
569
|
+
try {
|
|
570
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
571
|
+
}
|
|
572
|
+
catch { /* start fresh */ }
|
|
573
|
+
}
|
|
574
|
+
let changed = false;
|
|
575
|
+
for (const [key, value] of keysToMerge) {
|
|
576
|
+
if (!(key in settings)) {
|
|
577
|
+
settings[key] = value;
|
|
578
|
+
changed = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (changed) {
|
|
582
|
+
try {
|
|
583
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
584
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return changed;
|
|
270
591
|
}
|
|
271
592
|
/**
|
|
272
593
|
* Merge plugin permissions into Claude's settings.json.
|
|
273
|
-
* Reads the plugin's settings.json and merges permissions.allow entries.
|
|
594
|
+
* Reads the plugin's settings.json and merges permissions.allow / deny entries.
|
|
274
595
|
*/
|
|
275
|
-
function syncPluginPermissions(plugin, agent, versionHome) {
|
|
596
|
+
function syncPluginPermissions(plugin, agent, versionHome, userConfig) {
|
|
276
597
|
const pluginSettingsPath = path.join(plugin.root, 'settings.json');
|
|
277
598
|
if (!fs.existsSync(pluginSettingsPath))
|
|
278
599
|
return false;
|
|
@@ -287,10 +608,8 @@ function syncPluginPermissions(plugin, agent, versionHome) {
|
|
|
287
608
|
const pluginDeny = pluginSettings.permissions?.deny || [];
|
|
288
609
|
if (pluginAllow.length === 0 && pluginDeny.length === 0)
|
|
289
610
|
return false;
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
const expandedDeny = pluginDeny.map(rule => expandPluginVars(rule, plugin.root, plugin.name, agent, versionHome));
|
|
293
|
-
// Read existing settings.json
|
|
611
|
+
const expandedAllow = pluginAllow.map(rule => expandPluginVars(rule, plugin.root, plugin.name, agent, versionHome, userConfig));
|
|
612
|
+
const expandedDeny = pluginDeny.map(rule => expandPluginVars(rule, plugin.root, plugin.name, agent, versionHome, userConfig));
|
|
294
613
|
const configDir = path.join(versionHome, `.${agent}`);
|
|
295
614
|
const settingsPath = path.join(configDir, 'settings.json');
|
|
296
615
|
let settings = {};
|
|
@@ -298,9 +617,7 @@ function syncPluginPermissions(plugin, agent, versionHome) {
|
|
|
298
617
|
try {
|
|
299
618
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
300
619
|
}
|
|
301
|
-
catch {
|
|
302
|
-
// Start fresh
|
|
303
|
-
}
|
|
620
|
+
catch { /* start fresh */ }
|
|
304
621
|
}
|
|
305
622
|
if (!settings.permissions || typeof settings.permissions !== 'object') {
|
|
306
623
|
settings.permissions = { allow: [], deny: [] };
|
|
@@ -310,19 +627,16 @@ function syncPluginPermissions(plugin, agent, versionHome) {
|
|
|
310
627
|
perms.allow = [];
|
|
311
628
|
if (!perms.deny)
|
|
312
629
|
perms.deny = [];
|
|
313
|
-
// Merge allow rules (deduplicate)
|
|
314
630
|
for (const rule of expandedAllow) {
|
|
315
631
|
if (!perms.allow.includes(rule)) {
|
|
316
632
|
perms.allow.push(rule);
|
|
317
633
|
}
|
|
318
634
|
}
|
|
319
|
-
// Merge deny rules (deduplicate)
|
|
320
635
|
for (const rule of expandedDeny) {
|
|
321
636
|
if (!perms.deny.includes(rule)) {
|
|
322
637
|
perms.deny.push(rule);
|
|
323
638
|
}
|
|
324
639
|
}
|
|
325
|
-
// Write back
|
|
326
640
|
try {
|
|
327
641
|
fs.mkdirSync(configDir, { recursive: true });
|
|
328
642
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
@@ -332,11 +646,12 @@ function syncPluginPermissions(plugin, agent, versionHome) {
|
|
|
332
646
|
return false;
|
|
333
647
|
}
|
|
334
648
|
}
|
|
649
|
+
// ─── Utility ──────────────────────────────────────────────────────────────────
|
|
335
650
|
/**
|
|
336
|
-
* Copy a directory recursively, expanding plugin variables in file contents.
|
|
651
|
+
* Copy a directory recursively, expanding plugin variables in text file contents.
|
|
337
652
|
* Only expands variables in text files (.md, .json, .sh, .py, .js, .ts, .yaml, .yml, .toml).
|
|
338
653
|
*/
|
|
339
|
-
function copyDirWithVarExpansion(src, dest, pluginRoot, pluginName, agent, versionHome) {
|
|
654
|
+
function copyDirWithVarExpansion(src, dest, pluginRoot, pluginName, agent, versionHome, userConfig) {
|
|
340
655
|
fs.mkdirSync(dest, { recursive: true });
|
|
341
656
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
342
657
|
const textExtensions = new Set(['.md', '.json', '.sh', '.py', '.js', '.ts', '.yaml', '.yml', '.toml', '.txt']);
|
|
@@ -344,21 +659,18 @@ function copyDirWithVarExpansion(src, dest, pluginRoot, pluginName, agent, versi
|
|
|
344
659
|
const srcPath = path.join(src, entry.name);
|
|
345
660
|
const destPath = path.join(dest, entry.name);
|
|
346
661
|
if (entry.isDirectory()) {
|
|
347
|
-
copyDirWithVarExpansion(srcPath, destPath, pluginRoot, pluginName, agent, versionHome);
|
|
662
|
+
copyDirWithVarExpansion(srcPath, destPath, pluginRoot, pluginName, agent, versionHome, userConfig);
|
|
348
663
|
}
|
|
349
664
|
else {
|
|
350
665
|
const ext = path.extname(entry.name).toLowerCase();
|
|
351
666
|
if (textExtensions.has(ext)) {
|
|
352
|
-
// Expand variables in text files
|
|
353
667
|
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
354
|
-
content = expandPluginVars(content, pluginRoot, pluginName, agent, versionHome);
|
|
668
|
+
content = expandPluginVars(content, pluginRoot, pluginName, agent, versionHome, userConfig);
|
|
355
669
|
fs.writeFileSync(destPath, content, 'utf-8');
|
|
356
670
|
}
|
|
357
671
|
else {
|
|
358
|
-
// Binary copy
|
|
359
672
|
fs.copyFileSync(srcPath, destPath);
|
|
360
673
|
}
|
|
361
|
-
// Preserve executable permission
|
|
362
674
|
const stat = fs.statSync(srcPath);
|
|
363
675
|
if (stat.mode & 0o111) {
|
|
364
676
|
fs.chmodSync(destPath, stat.mode);
|
|
@@ -366,41 +678,68 @@ function copyDirWithVarExpansion(src, dest, pluginRoot, pluginName, agent, versi
|
|
|
366
678
|
}
|
|
367
679
|
}
|
|
368
680
|
}
|
|
681
|
+
// ─── Sync status ──────────────────────────────────────────────────────────────
|
|
369
682
|
/**
|
|
370
683
|
* Check if a plugin is synced to a version by inspecting the version home.
|
|
371
|
-
* Checks
|
|
372
|
-
* and plugin permissions in settings.json.
|
|
684
|
+
* Checks skills, commands, agent defs, bin, hook commands, and permissions.
|
|
373
685
|
*/
|
|
374
686
|
export function isPluginSynced(plugin, agent, versionHome) {
|
|
375
|
-
|
|
687
|
+
const prefix = `${plugin.name}--`;
|
|
688
|
+
// Check 1: plugin skill directories
|
|
376
689
|
if (plugin.skills.length > 0) {
|
|
377
690
|
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
378
691
|
if (fs.existsSync(skillsDir)) {
|
|
379
692
|
for (const skillName of plugin.skills) {
|
|
380
|
-
|
|
381
|
-
if (fs.existsSync(path.join(skillsDir, fsSafeName))) {
|
|
693
|
+
if (fs.existsSync(path.join(skillsDir, `${prefix}${skillName}`))) {
|
|
382
694
|
return true;
|
|
383
695
|
}
|
|
384
696
|
}
|
|
385
697
|
}
|
|
386
698
|
}
|
|
387
|
-
// Check 2: plugin
|
|
699
|
+
// Check 2: plugin command files
|
|
700
|
+
if (plugin.commands.length > 0 && (agent === 'claude' || agent === 'openclaw')) {
|
|
701
|
+
const agentConfig = AGENTS[agent];
|
|
702
|
+
const commandsDir = path.join(versionHome, `.${agent}`, agentConfig.commandsSubdir);
|
|
703
|
+
if (fs.existsSync(commandsDir)) {
|
|
704
|
+
for (const cmdName of plugin.commands) {
|
|
705
|
+
if (fs.existsSync(path.join(commandsDir, `${prefix}${cmdName}.md`))) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Check 3: plugin agent definition files
|
|
712
|
+
if (plugin.agentDefs.length > 0 && agent === 'claude') {
|
|
713
|
+
const agentsDir = path.join(versionHome, `.${agent}`, 'agents');
|
|
714
|
+
if (fs.existsSync(agentsDir)) {
|
|
715
|
+
for (const agentDefName of plugin.agentDefs) {
|
|
716
|
+
if (fs.existsSync(path.join(agentsDir, `${prefix}${agentDefName}.md`))) {
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Check 4: plugin bin directory
|
|
723
|
+
if (plugin.bin.length > 0 && agent === 'claude') {
|
|
724
|
+
const binDir = path.join(versionHome, `.${agent}`, 'plugin-bin', plugin.name);
|
|
725
|
+
if (fs.existsSync(binDir)) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Check 5: plugin hooks registered in settings.json (commands referencing plugin root)
|
|
388
730
|
if (plugin.hooks.length > 0 && agent === 'claude') {
|
|
389
731
|
const settingsPath = path.join(versionHome, `.${agent}`, 'settings.json');
|
|
390
732
|
if (fs.existsSync(settingsPath)) {
|
|
391
733
|
try {
|
|
392
734
|
const content = fs.readFileSync(settingsPath, 'utf-8');
|
|
393
|
-
// Check if any hook command references this plugin's root path
|
|
394
735
|
if (content.includes(plugin.root)) {
|
|
395
736
|
return true;
|
|
396
737
|
}
|
|
397
738
|
}
|
|
398
|
-
catch {
|
|
399
|
-
// Ignore read errors
|
|
400
|
-
}
|
|
739
|
+
catch { /* ignore */ }
|
|
401
740
|
}
|
|
402
741
|
}
|
|
403
|
-
// Check
|
|
742
|
+
// Check 6: plugin permissions in settings.json
|
|
404
743
|
if (agent === 'claude') {
|
|
405
744
|
const settingsPath = path.join(versionHome, `.${agent}`, 'settings.json');
|
|
406
745
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -410,48 +749,100 @@ export function isPluginSynced(plugin, agent, versionHome) {
|
|
|
410
749
|
if (allow.some((rule) => rule.includes(plugin.root))) {
|
|
411
750
|
return true;
|
|
412
751
|
}
|
|
752
|
+
// Check MCP servers
|
|
753
|
+
const mcpServers = settings.mcpServers;
|
|
754
|
+
if (mcpServers) {
|
|
755
|
+
const hasNamespacedServer = Object.keys(mcpServers).some(k => k.startsWith(prefix));
|
|
756
|
+
if (hasNamespacedServer)
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
413
759
|
}
|
|
414
|
-
catch {
|
|
415
|
-
// Ignore parse errors
|
|
416
|
-
}
|
|
760
|
+
catch { /* ignore */ }
|
|
417
761
|
}
|
|
418
762
|
}
|
|
419
763
|
return false;
|
|
420
764
|
}
|
|
765
|
+
// ─── Removal ─────────────────────────────────────────────────────────────────
|
|
421
766
|
/**
|
|
422
767
|
* Remove a plugin from a specific agent version's home directory.
|
|
423
|
-
* Inverse of syncPluginToVersion
|
|
424
|
-
* 1. Delete synced skill directories (pluginName--*)
|
|
425
|
-
* 2. Strip hook entries whose commands reference the plugin root
|
|
426
|
-
* 3. Strip permission rules that reference the plugin root
|
|
768
|
+
* Inverse of syncPluginToVersion.
|
|
427
769
|
*
|
|
428
|
-
* Works whether or not the plugin source still exists on disk
|
|
429
|
-
* matches by the plugin's name and root path rather than re-reading manifests.
|
|
770
|
+
* Works whether or not the plugin source still exists on disk.
|
|
430
771
|
*/
|
|
431
772
|
export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHome) {
|
|
432
|
-
const result = {
|
|
773
|
+
const result = {
|
|
774
|
+
skills: [],
|
|
775
|
+
commands: [],
|
|
776
|
+
agentDefs: [],
|
|
777
|
+
bin: [],
|
|
778
|
+
hooks: [],
|
|
779
|
+
permissions: 0,
|
|
780
|
+
mcp: 0,
|
|
781
|
+
};
|
|
782
|
+
const prefix = `${pluginName}--`;
|
|
433
783
|
// 1. Remove synced skill dirs
|
|
434
784
|
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
435
785
|
if (fs.existsSync(skillsDir)) {
|
|
436
|
-
const prefix = `${pluginName}--`;
|
|
437
786
|
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
438
|
-
if (!entry.isDirectory())
|
|
439
|
-
continue;
|
|
440
|
-
if (!entry.name.startsWith(prefix))
|
|
787
|
+
if (!entry.isDirectory() || !entry.name.startsWith(prefix))
|
|
441
788
|
continue;
|
|
442
789
|
try {
|
|
443
790
|
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
|
|
444
791
|
result.skills.push(entry.name);
|
|
445
792
|
}
|
|
446
|
-
catch {
|
|
447
|
-
|
|
793
|
+
catch { /* skip on error */ }
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// 2. Remove synced command files
|
|
797
|
+
if (agent === 'claude' || agent === 'openclaw') {
|
|
798
|
+
const agentConfig = AGENTS[agent];
|
|
799
|
+
const commandsDir = path.join(versionHome, `.${agent}`, agentConfig.commandsSubdir);
|
|
800
|
+
if (fs.existsSync(commandsDir)) {
|
|
801
|
+
for (const entry of fs.readdirSync(commandsDir, { withFileTypes: true })) {
|
|
802
|
+
if (!entry.isFile())
|
|
803
|
+
continue;
|
|
804
|
+
if (!entry.name.startsWith(prefix) || !entry.name.endsWith('.md'))
|
|
805
|
+
continue;
|
|
806
|
+
try {
|
|
807
|
+
fs.unlinkSync(path.join(commandsDir, entry.name));
|
|
808
|
+
result.commands.push(entry.name);
|
|
809
|
+
}
|
|
810
|
+
catch { /* skip on error */ }
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// 3. Remove synced agent def files
|
|
815
|
+
if (agent === 'claude') {
|
|
816
|
+
const agentsDir = path.join(versionHome, `.${agent}`, 'agents');
|
|
817
|
+
if (fs.existsSync(agentsDir)) {
|
|
818
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
819
|
+
if (!entry.isFile())
|
|
820
|
+
continue;
|
|
821
|
+
if (!entry.name.startsWith(prefix) || !entry.name.endsWith('.md'))
|
|
822
|
+
continue;
|
|
823
|
+
try {
|
|
824
|
+
fs.unlinkSync(path.join(agentsDir, entry.name));
|
|
825
|
+
result.agentDefs.push(entry.name);
|
|
826
|
+
}
|
|
827
|
+
catch { /* skip on error */ }
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// 4. Remove plugin-bin directory
|
|
832
|
+
if (agent === 'claude') {
|
|
833
|
+
const binDir = path.join(versionHome, `.${agent}`, 'plugin-bin', pluginName);
|
|
834
|
+
if (fs.existsSync(binDir)) {
|
|
835
|
+
try {
|
|
836
|
+
fs.rmSync(binDir, { recursive: true, force: true });
|
|
837
|
+
result.bin.push(binDir);
|
|
448
838
|
}
|
|
839
|
+
catch { /* skip on error */ }
|
|
449
840
|
}
|
|
450
841
|
}
|
|
451
842
|
if (agent !== 'claude') {
|
|
452
843
|
return result;
|
|
453
844
|
}
|
|
454
|
-
//
|
|
845
|
+
// 5 + 6 + 7: edit settings.json — strip hooks, permissions, mcpServers matching plugin
|
|
455
846
|
const settingsPath = path.join(versionHome, `.${agent}`, 'settings.json');
|
|
456
847
|
if (!fs.existsSync(settingsPath))
|
|
457
848
|
return result;
|
|
@@ -483,13 +874,11 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
483
874
|
if (group.hooks.length !== originalLen)
|
|
484
875
|
changed = true;
|
|
485
876
|
}
|
|
486
|
-
// Drop matcher groups whose hooks array is now empty
|
|
487
877
|
const kept = eventEntries.filter(g => Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
488
878
|
if (kept.length !== eventEntries.length) {
|
|
489
879
|
hooksConfig[event] = kept;
|
|
490
880
|
changed = true;
|
|
491
881
|
}
|
|
492
|
-
// Drop the event key entirely if it has no matcher groups left
|
|
493
882
|
if (Array.isArray(hooksConfig[event]) && hooksConfig[event].length === 0) {
|
|
494
883
|
delete hooksConfig[event];
|
|
495
884
|
changed = true;
|
|
@@ -515,22 +904,39 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
515
904
|
}
|
|
516
905
|
}
|
|
517
906
|
}
|
|
907
|
+
// Strip namespaced MCP servers added by this plugin
|
|
908
|
+
const mcpServers = settings.mcpServers;
|
|
909
|
+
if (mcpServers && typeof mcpServers === 'object') {
|
|
910
|
+
for (const serverName of Object.keys(mcpServers)) {
|
|
911
|
+
if (serverName.startsWith(prefix)) {
|
|
912
|
+
delete mcpServers[serverName];
|
|
913
|
+
result.mcp += 1;
|
|
914
|
+
changed = true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Strip bin path from pluginBinPaths
|
|
919
|
+
if (Array.isArray(settings.pluginBinPaths)) {
|
|
920
|
+
const binDir = path.join(versionHome, `.${agent}`, 'plugin-bin', pluginName);
|
|
921
|
+
const before = settings.pluginBinPaths.length;
|
|
922
|
+
settings.pluginBinPaths = settings.pluginBinPaths.filter(p => p !== binDir);
|
|
923
|
+
if (settings.pluginBinPaths.length !== before)
|
|
924
|
+
changed = true;
|
|
925
|
+
}
|
|
518
926
|
if (changed) {
|
|
519
927
|
try {
|
|
520
928
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
521
929
|
}
|
|
522
|
-
catch {
|
|
523
|
-
// Ignore write errors
|
|
524
|
-
}
|
|
930
|
+
catch { /* ignore write errors */ }
|
|
525
931
|
}
|
|
526
932
|
return result;
|
|
527
933
|
}
|
|
934
|
+
// ─── Orphan cleanup ───────────────────────────────────────────────────────────
|
|
528
935
|
/**
|
|
529
936
|
* Remove orphaned plugin skill directories from a version home.
|
|
530
|
-
*
|
|
531
|
-
* where the plugin no longer exists in ~/.agents/plugins/.
|
|
937
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
532
938
|
*/
|
|
533
|
-
export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames) {
|
|
939
|
+
export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames, version) {
|
|
534
940
|
const removed = [];
|
|
535
941
|
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
536
942
|
if (!fs.existsSync(skillsDir))
|
|
@@ -539,20 +945,193 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames)
|
|
|
539
945
|
for (const entry of entries) {
|
|
540
946
|
if (!entry.isDirectory())
|
|
541
947
|
continue;
|
|
542
|
-
// Plugin skill dirs use the pattern: pluginName--skillName
|
|
543
948
|
const dashIdx = entry.name.indexOf('--');
|
|
544
949
|
if (dashIdx === -1)
|
|
545
950
|
continue;
|
|
546
951
|
const pluginName = entry.name.slice(0, dashIdx);
|
|
547
952
|
if (!activePluginNames.has(pluginName)) {
|
|
548
953
|
try {
|
|
549
|
-
|
|
954
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
955
|
+
const trashDir = path.join(getTrashPluginsDir(), agent, version || 'unknown', entry.name);
|
|
956
|
+
const trashDest = path.join(trashDir, stamp);
|
|
957
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
958
|
+
fs.renameSync(path.join(skillsDir, entry.name), trashDest);
|
|
550
959
|
removed.push(entry.name);
|
|
551
960
|
}
|
|
552
|
-
catch {
|
|
553
|
-
// Skip on error
|
|
554
|
-
}
|
|
961
|
+
catch { /* skip on error */ }
|
|
555
962
|
}
|
|
556
963
|
}
|
|
557
964
|
return removed;
|
|
558
965
|
}
|
|
966
|
+
export function diffVersionPlugins(agent, version) {
|
|
967
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
968
|
+
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
969
|
+
const orphans = [];
|
|
970
|
+
if (!fs.existsSync(skillsDir)) {
|
|
971
|
+
return { agent, version, orphans };
|
|
972
|
+
}
|
|
973
|
+
const activePlugins = new Set(discoverPlugins().map(p => p.name));
|
|
974
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
975
|
+
if (!entry.isDirectory())
|
|
976
|
+
continue;
|
|
977
|
+
const dashIdx = entry.name.indexOf('--');
|
|
978
|
+
if (dashIdx === -1)
|
|
979
|
+
continue;
|
|
980
|
+
const pluginName = entry.name.slice(0, dashIdx);
|
|
981
|
+
if (!activePlugins.has(pluginName)) {
|
|
982
|
+
orphans.push(entry.name);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return { agent, version, orphans: orphans.sort() };
|
|
986
|
+
}
|
|
987
|
+
export function iterPluginsCapableVersions(filter) {
|
|
988
|
+
const pairs = [];
|
|
989
|
+
const agents = filter?.agent ? [filter.agent] : PLUGINS_CAPABLE_AGENTS;
|
|
990
|
+
for (const agent of agents) {
|
|
991
|
+
if (!PLUGINS_CAPABLE_AGENTS.includes(agent))
|
|
992
|
+
continue;
|
|
993
|
+
const versions = listInstalledVersions(agent);
|
|
994
|
+
for (const version of versions) {
|
|
995
|
+
if (filter?.version && filter.version !== version)
|
|
996
|
+
continue;
|
|
997
|
+
pairs.push({ agent, version });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return pairs;
|
|
1001
|
+
}
|
|
1002
|
+
export function removePluginSkillFromVersion(agent, version, skillName) {
|
|
1003
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
1004
|
+
const skillPath = path.join(versionHome, `.${agent}`, 'skills', skillName);
|
|
1005
|
+
if (!fs.existsSync(skillPath)) {
|
|
1006
|
+
return { success: true };
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1010
|
+
const trashDir = path.join(getTrashPluginsDir(), agent, version, skillName);
|
|
1011
|
+
const trashDest = path.join(trashDir, stamp);
|
|
1012
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
1013
|
+
fs.renameSync(skillPath, trashDest);
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
return { success: false, error: err.message };
|
|
1017
|
+
}
|
|
1018
|
+
return { success: true };
|
|
1019
|
+
}
|
|
1020
|
+
// ─── Install / Update ─────────────────────────────────────────────────────────
|
|
1021
|
+
/**
|
|
1022
|
+
* Parse an install spec of the form `name@source` or just `source`.
|
|
1023
|
+
* Source can be a git URL or an absolute/relative local path.
|
|
1024
|
+
*/
|
|
1025
|
+
export function parseInstallSpec(spec) {
|
|
1026
|
+
// Check for name@source form
|
|
1027
|
+
const atIdx = spec.indexOf('@');
|
|
1028
|
+
if (atIdx > 0) {
|
|
1029
|
+
const name = spec.slice(0, atIdx);
|
|
1030
|
+
const source = spec.slice(atIdx + 1);
|
|
1031
|
+
return { name, source };
|
|
1032
|
+
}
|
|
1033
|
+
return { name: null, source: spec };
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Install a plugin from a git URL or local path.
|
|
1037
|
+
* Clones/copies to ~/.agents/.cache/plugins/<name>/.
|
|
1038
|
+
* Returns the installed plugin name and root path.
|
|
1039
|
+
*/
|
|
1040
|
+
export async function installPlugin(spec) {
|
|
1041
|
+
const { name: specName, source } = parseInstallSpec(spec);
|
|
1042
|
+
// Resolve local path (handle ~)
|
|
1043
|
+
const isLocalPath = source.startsWith('/') || source.startsWith('./') || source.startsWith('../') || source.startsWith('~');
|
|
1044
|
+
const resolvedSource = isLocalPath
|
|
1045
|
+
? source.replace(/^~/, process.env.HOME || '~')
|
|
1046
|
+
: source;
|
|
1047
|
+
const pluginsDir = getPluginsDir();
|
|
1048
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
1049
|
+
// If local path, load manifest to get the name
|
|
1050
|
+
let targetName = specName;
|
|
1051
|
+
if (!targetName) {
|
|
1052
|
+
if (isLocalPath) {
|
|
1053
|
+
const manifest = loadPluginManifest(resolvedSource);
|
|
1054
|
+
if (!manifest)
|
|
1055
|
+
throw new Error(`No valid plugin.json found at ${resolvedSource}`);
|
|
1056
|
+
targetName = manifest.name;
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
// Derive from git URL: last path segment without .git
|
|
1060
|
+
targetName = path.basename(resolvedSource).replace(/\.git$/, '');
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const targetRoot = path.join(pluginsDir, targetName);
|
|
1064
|
+
const isNew = !fs.existsSync(targetRoot);
|
|
1065
|
+
if (isLocalPath) {
|
|
1066
|
+
// Copy local directory
|
|
1067
|
+
if (fs.existsSync(targetRoot)) {
|
|
1068
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1069
|
+
}
|
|
1070
|
+
fs.cpSync(resolvedSource, targetRoot, { recursive: true });
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
// Git clone
|
|
1074
|
+
if (fs.existsSync(targetRoot)) {
|
|
1075
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1076
|
+
}
|
|
1077
|
+
execSync(`git clone --depth 1 ${JSON.stringify(resolvedSource)} ${JSON.stringify(targetRoot)}`, {
|
|
1078
|
+
stdio: 'pipe',
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
// Validate manifest
|
|
1082
|
+
const manifest = loadPluginManifest(targetRoot);
|
|
1083
|
+
if (!manifest) {
|
|
1084
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1085
|
+
throw new Error(`Installed source has no valid .claude-plugin/plugin.json`);
|
|
1086
|
+
}
|
|
1087
|
+
// Persist source for future updates
|
|
1088
|
+
fs.writeFileSync(path.join(targetRoot, SOURCE_FILE), JSON.stringify({ source, isGit: !isLocalPath }), 'utf-8');
|
|
1089
|
+
return { name: manifest.name, root: targetRoot, isNew };
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Update an installed plugin by re-pulling from its original source.
|
|
1093
|
+
* Returns true if the update succeeded.
|
|
1094
|
+
*/
|
|
1095
|
+
export async function updatePlugin(name) {
|
|
1096
|
+
const plugin = getPlugin(name);
|
|
1097
|
+
if (!plugin) {
|
|
1098
|
+
return { success: false, error: `Plugin '${name}' not found` };
|
|
1099
|
+
}
|
|
1100
|
+
const sourceFile = path.join(plugin.root, SOURCE_FILE);
|
|
1101
|
+
if (!fs.existsSync(sourceFile)) {
|
|
1102
|
+
return { success: false, error: `No source recorded for '${name}' — was it installed with 'agents plugins install'?` };
|
|
1103
|
+
}
|
|
1104
|
+
let sourceInfo;
|
|
1105
|
+
try {
|
|
1106
|
+
sourceInfo = JSON.parse(fs.readFileSync(sourceFile, 'utf-8'));
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
return { success: false, error: `Could not read source info for '${name}'` };
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
if (sourceInfo.isGit) {
|
|
1113
|
+
execSync(`git -C ${JSON.stringify(plugin.root)} pull --ff-only`, { stdio: 'pipe' });
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
const resolvedSource = sourceInfo.source.replace(/^~/, process.env.HOME || '~');
|
|
1117
|
+
if (!fs.existsSync(resolvedSource)) {
|
|
1118
|
+
return { success: false, error: `Source path no longer exists: ${resolvedSource}` };
|
|
1119
|
+
}
|
|
1120
|
+
// Preserve .user-config.json and .source during re-copy
|
|
1121
|
+
const userConfigPath = path.join(plugin.root, USER_CONFIG_FILE);
|
|
1122
|
+
const userConfigBackup = fs.existsSync(userConfigPath)
|
|
1123
|
+
? fs.readFileSync(userConfigPath, 'utf-8')
|
|
1124
|
+
: null;
|
|
1125
|
+
fs.rmSync(plugin.root, { recursive: true, force: true });
|
|
1126
|
+
fs.cpSync(resolvedSource, plugin.root, { recursive: true });
|
|
1127
|
+
fs.writeFileSync(path.join(plugin.root, SOURCE_FILE), JSON.stringify(sourceInfo), 'utf-8');
|
|
1128
|
+
if (userConfigBackup !== null) {
|
|
1129
|
+
fs.writeFileSync(userConfigPath, userConfigBackup, 'utf-8');
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
catch (err) {
|
|
1134
|
+
return { success: false, error: err.message };
|
|
1135
|
+
}
|
|
1136
|
+
return { success: true };
|
|
1137
|
+
}
|