@phnx-labs/agents-cli 1.20.4 → 1.20.5
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 +19 -0
- package/README.md +48 -17
- package/dist/commands/cli.js +1 -1
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +2 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/exec.js +52 -16
- package/dist/commands/hooks.js +6 -6
- package/dist/commands/inspect.d.ts +26 -0
- package/dist/commands/inspect.js +590 -0
- package/dist/commands/mcp.js +17 -16
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -4
- package/dist/commands/permissions.js +13 -12
- package/dist/commands/plugins.d.ts +13 -0
- package/dist/commands/plugins.js +100 -11
- package/dist/commands/prune.js +3 -2
- package/dist/commands/pull.d.ts +12 -5
- package/dist/commands/pull.js +26 -422
- package/dist/commands/push.d.ts +14 -0
- package/dist/commands/push.js +30 -0
- package/dist/commands/repo.d.ts +1 -1
- package/dist/commands/repo.js +155 -112
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +12 -3
- package/dist/commands/routines.js +32 -7
- package/dist/commands/rules.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup.d.ts +3 -3
- package/dist/commands/setup.js +15 -15
- package/dist/commands/skills.js +6 -5
- package/dist/commands/subagents.js +5 -4
- package/dist/commands/sync.d.ts +18 -5
- package/dist/commands/sync.js +251 -65
- package/dist/commands/teams.js +1 -0
- package/dist/commands/tmux.d.ts +25 -0
- package/dist/commands/tmux.js +415 -0
- package/dist/commands/trash.d.ts +2 -2
- package/dist/commands/trash.js +1 -1
- package/dist/commands/versions.js +2 -2
- package/dist/commands/view.js +9 -4
- package/dist/commands/workflows.js +4 -3
- package/dist/commands/worktree.d.ts +4 -5
- package/dist/commands/worktree.js +4 -4
- package/dist/index.js +68 -20
- package/dist/lib/agents.d.ts +19 -10
- package/dist/lib/agents.js +79 -25
- package/dist/lib/auto-pull-worker.d.ts +1 -1
- package/dist/lib/auto-pull-worker.js +2 -2
- package/dist/lib/auto-pull.d.ts +1 -1
- package/dist/lib/auto-pull.js +1 -1
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/capabilities.js +2 -0
- package/dist/lib/commands.d.ts +28 -1
- package/dist/lib/commands.js +125 -20
- package/dist/lib/doctor-diff.js +2 -2
- package/dist/lib/exec.d.ts +14 -0
- package/dist/lib/exec.js +39 -5
- package/dist/lib/fuzzy.d.ts +12 -2
- package/dist/lib/fuzzy.js +29 -4
- package/dist/lib/git.js +8 -1
- package/dist/lib/hooks.d.ts +2 -2
- package/dist/lib/hooks.js +97 -10
- package/dist/lib/mcp.js +32 -2
- package/dist/lib/migrate.d.ts +51 -0
- package/dist/lib/migrate.js +227 -1
- package/dist/lib/models.js +62 -15
- package/dist/lib/permissions.d.ts +36 -2
- package/dist/lib/permissions.js +217 -7
- package/dist/lib/plugin-marketplace.d.ts +98 -40
- package/dist/lib/plugin-marketplace.js +196 -93
- package/dist/lib/plugins.d.ts +21 -4
- package/dist/lib/plugins.js +130 -49
- package/dist/lib/profiles-presets.js +12 -12
- package/dist/lib/project-launch.d.ts +65 -0
- package/dist/lib/project-launch.js +367 -0
- package/dist/lib/pty-client.js +1 -1
- package/dist/lib/pty-server.d.ts +1 -1
- package/dist/lib/pty-server.js +1 -1
- package/dist/lib/refresh.d.ts +26 -0
- package/dist/lib/refresh.js +315 -0
- package/dist/lib/resource-patterns.d.ts +1 -1
- package/dist/lib/resource-patterns.js +1 -1
- package/dist/lib/resources/commands.js +2 -2
- package/dist/lib/resources/hooks.d.ts +1 -1
- package/dist/lib/resources/hooks.js +1 -1
- package/dist/lib/resources/mcp.d.ts +1 -1
- package/dist/lib/resources/mcp.js +5 -6
- package/dist/lib/resources/permissions.js +5 -2
- package/dist/lib/resources/rules.js +3 -2
- package/dist/lib/resources/skills.js +3 -2
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.js +2 -2
- package/dist/lib/rotate.d.ts +1 -1
- package/dist/lib/rotate.js +1 -1
- package/dist/lib/routines.d.ts +16 -4
- package/dist/lib/routines.js +67 -17
- package/dist/lib/rules/compile.js +22 -10
- package/dist/lib/rules/rules.js +3 -3
- package/dist/lib/runner.js +16 -3
- package/dist/lib/scheduler.js +15 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +9 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/secrets/linux.d.ts +44 -9
- package/dist/lib/secrets/linux.js +302 -48
- package/dist/lib/session/db.js +15 -2
- package/dist/lib/session/discover.js +118 -3
- package/dist/lib/session/parse.js +3 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +10 -9
- package/dist/lib/shims.js +101 -50
- package/dist/lib/skills.d.ts +1 -1
- package/dist/lib/skills.js +10 -9
- package/dist/lib/staleness/detectors/commands.d.ts +3 -0
- package/dist/lib/staleness/detectors/commands.js +46 -0
- package/dist/lib/staleness/detectors/hooks.d.ts +3 -0
- package/dist/lib/staleness/detectors/hooks.js +44 -0
- package/dist/lib/staleness/detectors/mcp.d.ts +3 -0
- package/dist/lib/staleness/detectors/mcp.js +31 -0
- package/dist/lib/staleness/detectors/permissions.d.ts +3 -0
- package/dist/lib/staleness/detectors/permissions.js +201 -0
- package/dist/lib/staleness/detectors/plugins.d.ts +8 -0
- package/dist/lib/staleness/detectors/plugins.js +23 -0
- package/dist/lib/staleness/detectors/rules.d.ts +3 -0
- package/dist/lib/staleness/detectors/rules.js +34 -0
- package/dist/lib/staleness/detectors/skills.d.ts +3 -0
- package/dist/lib/staleness/detectors/skills.js +71 -0
- package/dist/lib/staleness/detectors/subagents.d.ts +3 -0
- package/dist/lib/staleness/detectors/subagents.js +50 -0
- package/dist/lib/staleness/detectors/types.d.ts +22 -0
- package/dist/lib/staleness/detectors/types.js +1 -0
- package/dist/lib/staleness/detectors/workflows.d.ts +3 -0
- package/dist/lib/staleness/detectors/workflows.js +28 -0
- package/dist/lib/staleness/registry.d.ts +26 -0
- package/dist/lib/staleness/registry.js +123 -0
- package/dist/lib/staleness/writers/commands.d.ts +3 -0
- package/dist/lib/staleness/writers/commands.js +111 -0
- package/dist/lib/staleness/writers/hooks.d.ts +3 -0
- package/dist/lib/staleness/writers/hooks.js +47 -0
- package/dist/lib/staleness/writers/kinds.d.ts +10 -0
- package/dist/lib/staleness/writers/kinds.js +15 -0
- package/dist/lib/staleness/writers/lazy-map.d.ts +13 -0
- package/dist/lib/staleness/writers/lazy-map.js +19 -0
- package/dist/lib/staleness/writers/mcp.d.ts +10 -0
- package/dist/lib/staleness/writers/mcp.js +19 -0
- package/dist/lib/staleness/writers/permissions.d.ts +13 -0
- package/dist/lib/staleness/writers/permissions.js +26 -0
- package/dist/lib/staleness/writers/plugins.d.ts +7 -0
- package/dist/lib/staleness/writers/plugins.js +31 -0
- package/dist/lib/staleness/writers/rules.d.ts +7 -0
- package/dist/lib/staleness/writers/rules.js +55 -0
- package/dist/lib/staleness/writers/skills.d.ts +3 -0
- package/dist/lib/staleness/writers/skills.js +81 -0
- package/dist/lib/staleness/writers/sources.d.ts +16 -0
- package/dist/lib/staleness/writers/sources.js +72 -0
- package/dist/lib/staleness/writers/subagents.d.ts +3 -0
- package/dist/lib/staleness/writers/subagents.js +53 -0
- package/dist/lib/staleness/writers/types.d.ts +36 -0
- package/dist/lib/staleness/writers/types.js +1 -0
- package/dist/lib/staleness/writers/workflows.d.ts +7 -0
- package/dist/lib/staleness/writers/workflows.js +31 -0
- package/dist/lib/state.d.ts +34 -11
- package/dist/lib/state.js +58 -13
- package/dist/lib/subagents.d.ts +0 -2
- package/dist/lib/subagents.js +6 -6
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/binary.d.ts +67 -0
- package/dist/lib/tmux/binary.js +141 -0
- package/dist/lib/tmux/index.d.ts +8 -0
- package/dist/lib/tmux/index.js +8 -0
- package/dist/lib/tmux/paths.d.ts +17 -0
- package/dist/lib/tmux/paths.js +30 -0
- package/dist/lib/tmux/session.d.ts +122 -0
- package/dist/lib/tmux/session.js +305 -0
- package/dist/lib/types.d.ts +58 -7
- package/dist/lib/types.js +1 -1
- package/dist/lib/usage.js +1 -1
- package/dist/lib/versions.d.ts +4 -4
- package/dist/lib/versions.js +135 -493
- package/dist/lib/workflows.d.ts +2 -4
- package/dist/lib/workflows.js +3 -4
- package/package.json +2 -2
- package/scripts/postinstall.js +16 -63
- package/dist/commands/status.d.ts +0 -9
- package/dist/commands/status.js +0 -25
package/dist/lib/versions.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Version management module for agents-cli.
|
|
3
3
|
*
|
|
4
4
|
* Handles installing, removing, listing, and switching between agent CLI versions.
|
|
5
|
-
* Each version is installed into an isolated directory under ~/.agents
|
|
5
|
+
* Each version is installed into an isolated directory under ~/.agents/.system/versions/{agent}/{version}/
|
|
6
6
|
* with its own HOME directory for config isolation. Resources (commands, skills, hooks, memory,
|
|
7
7
|
* MCP servers, permissions, subagents, plugins) from ~/.agents/ are synced into version homes
|
|
8
8
|
* via copies or conversions (not symlinks).
|
|
@@ -21,28 +21,22 @@ import * as yaml from 'yaml';
|
|
|
21
21
|
import { exec, execFile } from 'child_process';
|
|
22
22
|
import { promisify } from 'util';
|
|
23
23
|
import chalk from 'chalk';
|
|
24
|
-
import * as TOML from 'smol-toml';
|
|
25
24
|
import { checkbox, select } from '@inquirer/prompts';
|
|
26
25
|
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
|
|
27
26
|
import { defaultPatterns, expandPatterns } from './resource-patterns.js';
|
|
28
27
|
import { listResources } from './resources.js';
|
|
29
|
-
import { AGENTS, getAccountEmail,
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { markdownToToml } from './convert.js';
|
|
28
|
+
import { AGENTS, getAccountEmail, resolveAgentName, formatAgentError } from './agents.js';
|
|
29
|
+
import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
30
|
+
import { parseMcpServerConfig } from './mcp.js';
|
|
33
31
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
34
32
|
import { importInstallScriptBinary } from './import.js';
|
|
35
|
-
import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenclaw, SUBAGENT_CAPABLE_AGENTS } from './subagents.js';
|
|
36
|
-
import { WORKFLOW_CAPABLE_AGENTS, listInstalledWorkflows, syncWorkflowToVersion } from './workflows.js';
|
|
37
|
-
import { registerHooksToSettings } from './hooks.js';
|
|
38
33
|
import { supports, explainSkip } from './capabilities.js';
|
|
39
|
-
import { discoverPlugins
|
|
40
|
-
import { composeRulesFromState } from './rules/compose.js';
|
|
34
|
+
import { discoverPlugins } from './plugins.js';
|
|
41
35
|
import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
|
|
42
|
-
import { PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
43
36
|
import { emit } from './events.js';
|
|
44
37
|
import { safeJoin } from './paths.js';
|
|
45
|
-
import {
|
|
38
|
+
import { listCommandSkillsInVersion, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
39
|
+
import { getWriter, getDetector } from './staleness/registry.js';
|
|
46
40
|
/** Promisified exec for running shell commands. */
|
|
47
41
|
const execAsync = promisify(exec);
|
|
48
42
|
const execFileAsync = promisify(execFile);
|
|
@@ -281,10 +275,8 @@ function skillDirsMatch(src, dest) {
|
|
|
281
275
|
* This is the source of truth - not the tracking in agents.yaml.
|
|
282
276
|
*/
|
|
283
277
|
export function getActuallySyncedResources(agent, version, options = {}) {
|
|
284
|
-
const agentConfig = AGENTS[agent];
|
|
285
278
|
const versionHome = path.join(getVersionsDir(), agent, version, 'home');
|
|
286
|
-
const
|
|
287
|
-
const projectAgentsDir = getProjectAgentsDir(options.cwd || process.cwd());
|
|
279
|
+
const cwd = options.cwd || process.cwd();
|
|
288
280
|
const result = {
|
|
289
281
|
commands: [],
|
|
290
282
|
skills: [],
|
|
@@ -297,219 +289,20 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
297
289
|
workflows: [],
|
|
298
290
|
promptcuts: false,
|
|
299
291
|
};
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Skills - check what directories exist AND content matches central source
|
|
317
|
-
const skillsDir = path.join(configDir, 'skills');
|
|
318
|
-
const centralSkillsDir = getSkillsDir();
|
|
319
|
-
const projectSkillsDir = projectAgentsDir ? path.join(projectAgentsDir, 'skills') : null;
|
|
320
|
-
const userAgentsDir = getUserAgentsDir();
|
|
321
|
-
const extraRepos = getEnabledExtraRepos();
|
|
322
|
-
if (fs.existsSync(skillsDir)) {
|
|
323
|
-
const installedSkills = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
324
|
-
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
325
|
-
.map(d => d.name);
|
|
326
|
-
for (const skill of installedSkills) {
|
|
327
|
-
const versionSkillDir = path.join(skillsDir, skill);
|
|
328
|
-
const sourceCandidates = [
|
|
329
|
-
projectSkillsDir ? path.join(projectSkillsDir, skill) : null,
|
|
330
|
-
path.join(userAgentsDir, 'skills', skill),
|
|
331
|
-
path.join(centralSkillsDir, skill),
|
|
332
|
-
...extraRepos.map((e) => path.join(e.dir, 'skills', skill)),
|
|
333
|
-
];
|
|
334
|
-
const sourceDir = sourceCandidates.find((p) => p && fs.existsSync(p)) || null;
|
|
335
|
-
if (!sourceDir) {
|
|
336
|
-
// True orphan — no source in project, primary, or any extra. Still
|
|
337
|
-
// count as synced so version-home cleanup knows it's accounted for.
|
|
338
|
-
result.skills.push(skill);
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
const allMatch = skillDirsMatch(sourceDir, versionSkillDir);
|
|
342
|
-
if (allMatch) {
|
|
343
|
-
result.skills.push(skill);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
// Hooks - check what files exist AND content matches central source
|
|
348
|
-
const hooksDir = path.join(configDir, 'hooks');
|
|
349
|
-
const centralHooksDir = getHooksDir();
|
|
350
|
-
const projectHooksDir = projectAgentsDir ? path.join(projectAgentsDir, 'hooks') : null;
|
|
351
|
-
const userHooksDir = path.join(userAgentsDir, 'hooks');
|
|
352
|
-
if (fs.existsSync(hooksDir)) {
|
|
353
|
-
const installedHooks = fs.readdirSync(hooksDir).filter(f => !f.startsWith('.'));
|
|
354
|
-
for (const hook of installedHooks) {
|
|
355
|
-
const projectFile = projectHooksDir ? path.join(projectHooksDir, hook) : null;
|
|
356
|
-
const centralFile = path.join(centralHooksDir, hook);
|
|
357
|
-
const userFile = path.join(userHooksDir, hook);
|
|
358
|
-
const versionFile = path.join(hooksDir, hook);
|
|
359
|
-
const hasProject = projectFile ? fs.existsSync(projectFile) : false;
|
|
360
|
-
const hasUser = fs.existsSync(userFile);
|
|
361
|
-
const hasCentral = fs.existsSync(centralFile);
|
|
362
|
-
const sourceFile = hasProject ? projectFile : hasUser ? userFile : centralFile;
|
|
363
|
-
if (!hasProject && !hasCentral && !hasUser) {
|
|
364
|
-
result.hooks.push(hook);
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
try {
|
|
368
|
-
const centralContent = fs.readFileSync(sourceFile, 'utf-8');
|
|
369
|
-
const versionContent = fs.readFileSync(versionFile, 'utf-8');
|
|
370
|
-
if (centralContent === versionContent) {
|
|
371
|
-
result.hooks.push(hook);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
// If read fails, consider not synced
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
// Rules — single composed instruction file per agent. If the file exists in
|
|
380
|
-
// the version home, we consider the active preset synced. Available presets
|
|
381
|
-
// are surfaced from rules.yaml; this set is the subset that materialized.
|
|
382
|
-
const instrFile = path.join(configDir, agentConfig.instructionsFile);
|
|
383
|
-
if (fs.existsSync(instrFile)) {
|
|
384
|
-
const activePreset = getActiveRulesPreset(agent, version);
|
|
385
|
-
result.memory.push(activePreset);
|
|
386
|
-
}
|
|
387
|
-
// MCP - use canonical config path + parser per agent
|
|
388
|
-
if (MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
389
|
-
const mcpConfigPath = getMcpConfigPathForHome(agent, versionHome);
|
|
390
|
-
if (fs.existsSync(mcpConfigPath)) {
|
|
391
|
-
try {
|
|
392
|
-
const servers = parseMcpConfig(agent, mcpConfigPath);
|
|
393
|
-
result.mcp = Object.keys(servers);
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
// Ignore parse errors
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
// Permissions - check agent-specific config files
|
|
401
|
-
const settingsPath = path.join(configDir, 'settings.json');
|
|
402
|
-
if (PERMISSIONS_CAPABLE_AGENTS.includes(agent)) {
|
|
403
|
-
if (agent === 'claude' && fs.existsSync(settingsPath)) {
|
|
404
|
-
// Claude: check settings.json permissions.allow and deny
|
|
405
|
-
try {
|
|
406
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
407
|
-
const allowRules = settings.permissions?.allow || [];
|
|
408
|
-
const denyRules = settings.permissions?.deny || [];
|
|
409
|
-
if (allowRules.length > 0 || denyRules.length > 0) {
|
|
410
|
-
const permGroups = discoverPermissionGroups();
|
|
411
|
-
const appliedGroups = [];
|
|
412
|
-
for (const group of permGroups) {
|
|
413
|
-
const groupSet = buildPermissionsFromGroups([group.name]);
|
|
414
|
-
// Empty groups (like header files) are considered synced if ANY permissions are applied
|
|
415
|
-
if (groupSet.allow.length === 0 && (!groupSet.deny || groupSet.deny.length === 0)) {
|
|
416
|
-
appliedGroups.push(group.name);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
const hasAllowRule = groupSet.allow.some(rule => allowRules.includes(rule));
|
|
420
|
-
const hasDenyRule = groupSet.deny?.some(rule => denyRules.includes(rule)) || false;
|
|
421
|
-
if (hasAllowRule || hasDenyRule) {
|
|
422
|
-
appliedGroups.push(group.name);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
result.permissions = appliedGroups;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
// Ignore parse errors
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
else if (agent === 'codex') {
|
|
433
|
-
// Codex: config.toml for approval_policy/sandbox_mode, .rules for deny
|
|
434
|
-
const codexConfigPath = path.join(configDir, 'config.toml');
|
|
435
|
-
const codexRulesPath = path.join(configDir, 'rules', CODEX_RULES_FILENAME);
|
|
436
|
-
const hasConfig = fs.existsSync(codexConfigPath);
|
|
437
|
-
const hasRules = fs.existsSync(codexRulesPath);
|
|
438
|
-
if (hasConfig || hasRules) {
|
|
439
|
-
try {
|
|
440
|
-
// Codex format is lossy — all groups merge into a few keys.
|
|
441
|
-
// If any permission artifacts exist, all groups were applied together.
|
|
442
|
-
let hasPermKeys = false;
|
|
443
|
-
if (hasConfig) {
|
|
444
|
-
const content = fs.readFileSync(codexConfigPath, 'utf-8');
|
|
445
|
-
const config = TOML.parse(content);
|
|
446
|
-
hasPermKeys = !!(config.approval_policy || config.sandbox_mode || config.sandbox_workspace_write);
|
|
447
|
-
}
|
|
448
|
-
if (hasPermKeys || hasRules) {
|
|
449
|
-
result.permissions = discoverPermissionGroups().map(g => g.name);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
// Ignore parse errors
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
else if (agent === 'opencode') {
|
|
458
|
-
// OpenCode: opencode.jsonc for permission.bash
|
|
459
|
-
const opencodeConfigPath = path.join(configDir, 'opencode.jsonc');
|
|
460
|
-
if (fs.existsSync(opencodeConfigPath)) {
|
|
461
|
-
try {
|
|
462
|
-
const content = fs.readFileSync(opencodeConfigPath, 'utf-8');
|
|
463
|
-
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
464
|
-
const config = JSON.parse(stripped);
|
|
465
|
-
if (config.permission && Object.keys(config.permission.bash || {}).length > 0) {
|
|
466
|
-
result.permissions = discoverPermissionGroups().map(g => g.name);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
// Ignore parse errors
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// Subagents - check agent-specific locations
|
|
476
|
-
if (SUBAGENT_CAPABLE_AGENTS.includes(agent)) {
|
|
477
|
-
if (agent === 'claude') {
|
|
478
|
-
const agentsDir = path.join(configDir, 'agents');
|
|
479
|
-
if (fs.existsSync(agentsDir)) {
|
|
480
|
-
result.subagents = fs.readdirSync(agentsDir)
|
|
481
|
-
.filter(f => f.endsWith('.md'))
|
|
482
|
-
.map(f => f.replace('.md', ''));
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
else if (agent === 'openclaw') {
|
|
486
|
-
// OpenClaw: directories with AGENTS.md
|
|
487
|
-
const openclawDir = path.join(versionHome, '.openclaw');
|
|
488
|
-
if (fs.existsSync(openclawDir)) {
|
|
489
|
-
result.subagents = fs.readdirSync(openclawDir, { withFileTypes: true })
|
|
490
|
-
.filter(d => d.isDirectory() && fs.existsSync(path.join(openclawDir, d.name, 'AGENTS.md')))
|
|
491
|
-
.map(d => d.name);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
// Plugins - check which discovered plugins have their skills in the version
|
|
496
|
-
if (PLUGINS_CAPABLE_AGENTS.includes(agent)) {
|
|
497
|
-
const allPlugins = discoverPlugins();
|
|
498
|
-
for (const plugin of allPlugins) {
|
|
499
|
-
if (isPluginSynced(plugin, agent, versionHome)) {
|
|
500
|
-
result.plugins.push(plugin.name);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// Workflows - check {versionHome}/workflows/ for synced workflow directories
|
|
505
|
-
if (WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
|
|
506
|
-
const workflowsDir = path.join(versionHome, 'workflows');
|
|
507
|
-
if (fs.existsSync(workflowsDir)) {
|
|
508
|
-
result.workflows = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
|
509
|
-
.filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
|
|
510
|
-
.map(d => d.name);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
292
|
+
// Dispatch each kind through DETECTORS. The registry guarantees a detector
|
|
293
|
+
// exists for every supported (agent, kind) pair; unsupported pairs leave
|
|
294
|
+
// the field empty. The previous per-agent if-ladder silently dropped
|
|
295
|
+
// antigravity/gemini/grok detection — see PR description for details.
|
|
296
|
+
const ctx = { version, versionHome, cwd };
|
|
297
|
+
result.commands = getDetector('commands', agent)?.list(ctx) ?? [];
|
|
298
|
+
result.skills = getDetector('skills', agent)?.list(ctx) ?? [];
|
|
299
|
+
result.hooks = getDetector('hooks', agent)?.list(ctx) ?? [];
|
|
300
|
+
result.memory = getDetector('rules', agent)?.list(ctx) ?? [];
|
|
301
|
+
result.mcp = getDetector('mcp', agent)?.list(ctx) ?? [];
|
|
302
|
+
result.permissions = getDetector('permissions', agent)?.list(ctx) ?? [];
|
|
303
|
+
result.subagents = getDetector('subagents', agent)?.list(ctx) ?? [];
|
|
304
|
+
result.plugins = getDetector('plugins', agent)?.list(ctx) ?? [];
|
|
305
|
+
result.workflows = getDetector('workflows', agent)?.list(ctx) ?? [];
|
|
513
306
|
return result;
|
|
514
307
|
}
|
|
515
308
|
/**
|
|
@@ -660,9 +453,9 @@ export function hasNewResources(diff, agent, version) {
|
|
|
660
453
|
const hooksApply = agent ? supports(agent, 'hooks', version).ok : true;
|
|
661
454
|
const mcpApply = agent ? supports(agent, 'mcp', version).ok : true;
|
|
662
455
|
const permsApply = agent ? supports(agent, 'allowlist', version).ok : true;
|
|
663
|
-
const subagentsApply = agent ?
|
|
456
|
+
const subagentsApply = agent ? supports(agent, 'subagents', version).ok : true;
|
|
664
457
|
const pluginsApply = agent ? supports(agent, 'plugins', version).ok : true;
|
|
665
|
-
const workflowsApply = agent ?
|
|
458
|
+
const workflowsApply = agent ? supports(agent, 'workflows', version).ok : true;
|
|
666
459
|
return ((diff.commands.length > 0 && commandsApply) ||
|
|
667
460
|
diff.skills.length > 0 ||
|
|
668
461
|
(diff.hooks.length > 0 && hooksApply) ||
|
|
@@ -683,8 +476,9 @@ function buildNewResourcesSummary(newResources, agent, version) {
|
|
|
683
476
|
// Use version-aware gates so Codex >= 0.117.0 (which converts commands to skills) doesn't
|
|
684
477
|
// double-count and so "16 commands" never appears in the summary when commands have
|
|
685
478
|
// already been emitted as skills in the version home.
|
|
686
|
-
const commandsApply =
|
|
479
|
+
const commandsApply = supports(agent, 'commands', version).ok;
|
|
687
480
|
const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
|
|
481
|
+
const rulesApply = supports(agent, 'rules', version).ok;
|
|
688
482
|
if (newResources.commands.length > 0 && (commandsApply || commandsAsSkills)) {
|
|
689
483
|
parts.push(`${newResources.commands.length} command${newResources.commands.length === 1 ? '' : 's'}`);
|
|
690
484
|
}
|
|
@@ -694,22 +488,22 @@ function buildNewResourcesSummary(newResources, agent, version) {
|
|
|
694
488
|
if (newResources.hooks.length > 0 && agentConfig.supportsHooks) {
|
|
695
489
|
parts.push(`${newResources.hooks.length} hook${newResources.hooks.length === 1 ? '' : 's'}`);
|
|
696
490
|
}
|
|
697
|
-
if (newResources.memory.length > 0 &&
|
|
491
|
+
if (newResources.memory.length > 0 && rulesApply) {
|
|
698
492
|
parts.push(`${newResources.memory.length} rule file${newResources.memory.length === 1 ? '' : 's'}`);
|
|
699
493
|
}
|
|
700
|
-
if (newResources.mcp.length > 0 &&
|
|
494
|
+
if (newResources.mcp.length > 0 && supports(agent, 'mcp', version).ok) {
|
|
701
495
|
parts.push(`${newResources.mcp.length} MCP${newResources.mcp.length === 1 ? '' : 's'}`);
|
|
702
496
|
}
|
|
703
|
-
if (newResources.permissions.length > 0 &&
|
|
497
|
+
if (newResources.permissions.length > 0 && supports(agent, 'allowlist', version).ok) {
|
|
704
498
|
parts.push(`${newResources.permissions.length} permission group${newResources.permissions.length === 1 ? '' : 's'}`);
|
|
705
499
|
}
|
|
706
|
-
if (newResources.subagents.length > 0 &&
|
|
500
|
+
if (newResources.subagents.length > 0 && supports(agent, 'subagents', version).ok) {
|
|
707
501
|
parts.push(`${newResources.subagents.length} subagent${newResources.subagents.length === 1 ? '' : 's'}`);
|
|
708
502
|
}
|
|
709
|
-
if (newResources.plugins.length > 0 &&
|
|
503
|
+
if (newResources.plugins.length > 0 && supports(agent, 'plugins', version).ok) {
|
|
710
504
|
parts.push(`${newResources.plugins.length} plugin${newResources.plugins.length === 1 ? '' : 's'}`);
|
|
711
505
|
}
|
|
712
|
-
if (newResources.workflows.length > 0 &&
|
|
506
|
+
if (newResources.workflows.length > 0 && supports(agent, 'workflows', version).ok) {
|
|
713
507
|
parts.push(`${newResources.workflows.length} workflow${newResources.workflows.length === 1 ? '' : 's'}`);
|
|
714
508
|
}
|
|
715
509
|
return parts.join(', ');
|
|
@@ -724,9 +518,10 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
724
518
|
// Version-aware gates. When version is known, prefer per-version capability checks; the
|
|
725
519
|
// commands branch is allowed when either native commands are supported OR when the
|
|
726
520
|
// version emits commands as converted skills (Codex >= 0.117.0).
|
|
727
|
-
const commandsApply =
|
|
521
|
+
const commandsApply = supports(agent, 'commands', version).ok;
|
|
728
522
|
const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
|
|
729
523
|
const commandsBranch = commandsApply || commandsAsSkills;
|
|
524
|
+
const rulesBranch = supports(agent, 'rules', version).ok;
|
|
730
525
|
// Get permission group info for display
|
|
731
526
|
const permissionGroups = discoverPermissionGroups();
|
|
732
527
|
const newPermissionGroups = permissionGroups.filter(g => newResources.permissions.includes(g.name));
|
|
@@ -756,17 +551,17 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
756
551
|
selection.skills = newResources.skills;
|
|
757
552
|
if (newResources.hooks.length > 0 && agentConfig.supportsHooks)
|
|
758
553
|
selection.hooks = newResources.hooks;
|
|
759
|
-
if (newResources.memory.length > 0 &&
|
|
554
|
+
if (newResources.memory.length > 0 && rulesBranch)
|
|
760
555
|
selection.memory = newResources.memory;
|
|
761
|
-
if (newResources.mcp.length > 0 &&
|
|
556
|
+
if (newResources.mcp.length > 0 && supports(agent, 'mcp', version).ok)
|
|
762
557
|
selection.mcp = newResources.mcp;
|
|
763
|
-
if (newResources.permissions.length > 0 &&
|
|
558
|
+
if (newResources.permissions.length > 0 && supports(agent, 'allowlist', version).ok)
|
|
764
559
|
selection.permissions = newResources.permissions;
|
|
765
|
-
if (newResources.subagents.length > 0 &&
|
|
560
|
+
if (newResources.subagents.length > 0 && supports(agent, 'subagents', version).ok)
|
|
766
561
|
selection.subagents = newResources.subagents;
|
|
767
|
-
if (newResources.plugins.length > 0 &&
|
|
562
|
+
if (newResources.plugins.length > 0 && supports(agent, 'plugins', version).ok)
|
|
768
563
|
selection.plugins = newResources.plugins;
|
|
769
|
-
if (newResources.workflows.length > 0 &&
|
|
564
|
+
if (newResources.workflows.length > 0 && supports(agent, 'workflows', version).ok)
|
|
770
565
|
selection.workflows = newResources.workflows;
|
|
771
566
|
return selection;
|
|
772
567
|
}
|
|
@@ -795,7 +590,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
795
590
|
if (selected.length > 0)
|
|
796
591
|
selection.hooks = selected;
|
|
797
592
|
}
|
|
798
|
-
if (newResources.memory.length > 0 &&
|
|
593
|
+
if (newResources.memory.length > 0 && rulesBranch) {
|
|
799
594
|
const selected = await checkbox({
|
|
800
595
|
message: 'Select new rule files to sync:',
|
|
801
596
|
choices: newResources.memory.map(m => ({ name: m, value: m, checked: true })),
|
|
@@ -803,7 +598,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
803
598
|
if (selected.length > 0)
|
|
804
599
|
selection.memory = selected;
|
|
805
600
|
}
|
|
806
|
-
if (newResources.mcp.length > 0 &&
|
|
601
|
+
if (newResources.mcp.length > 0 && supports(agent, 'mcp', version).ok) {
|
|
807
602
|
const selected = await checkbox({
|
|
808
603
|
message: 'Select new MCPs to sync:',
|
|
809
604
|
choices: newResources.mcp.map(m => ({ name: m, value: m, checked: true })),
|
|
@@ -811,7 +606,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
811
606
|
if (selected.length > 0)
|
|
812
607
|
selection.mcp = selected;
|
|
813
608
|
}
|
|
814
|
-
if (newResources.permissions.length > 0 &&
|
|
609
|
+
if (newResources.permissions.length > 0 && supports(agent, 'allowlist', version).ok) {
|
|
815
610
|
const selected = await checkbox({
|
|
816
611
|
message: 'Select new permission groups to sync:',
|
|
817
612
|
choices: newPermissionGroups.map(g => ({
|
|
@@ -823,7 +618,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
823
618
|
if (selected.length > 0)
|
|
824
619
|
selection.permissions = selected;
|
|
825
620
|
}
|
|
826
|
-
if (newResources.subagents.length > 0 &&
|
|
621
|
+
if (newResources.subagents.length > 0 && supports(agent, 'subagents', version).ok) {
|
|
827
622
|
const selected = await checkbox({
|
|
828
623
|
message: 'Select new subagents to sync:',
|
|
829
624
|
choices: newResources.subagents.map(s => ({ name: s, value: s, checked: true })),
|
|
@@ -831,7 +626,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
831
626
|
if (selected.length > 0)
|
|
832
627
|
selection.subagents = selected;
|
|
833
628
|
}
|
|
834
|
-
if (newResources.plugins.length > 0 &&
|
|
629
|
+
if (newResources.plugins.length > 0 && supports(agent, 'plugins', version).ok) {
|
|
835
630
|
const allPlugins = discoverPlugins();
|
|
836
631
|
const pluginMap = new Map(allPlugins.map(p => [p.name, p]));
|
|
837
632
|
const selected = await checkbox({
|
|
@@ -845,7 +640,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
845
640
|
if (selected.length > 0)
|
|
846
641
|
selection.plugins = selected;
|
|
847
642
|
}
|
|
848
|
-
if (newResources.workflows.length > 0 &&
|
|
643
|
+
if (newResources.workflows.length > 0 && supports(agent, 'workflows', version).ok) {
|
|
849
644
|
const selected = await checkbox({
|
|
850
645
|
message: 'Select new workflows to sync:',
|
|
851
646
|
choices: newResources.workflows.map(w => ({ name: w, value: w, checked: true })),
|
|
@@ -867,14 +662,14 @@ export async function promptResourceSelection(agent) {
|
|
|
867
662
|
const permissionGroups = discoverPermissionGroups();
|
|
868
663
|
const totalPermissionRules = permissionGroups.reduce((sum, g) => sum + g.ruleCount, 0);
|
|
869
664
|
const categories = [
|
|
870
|
-
{ key: 'commands', label: 'Commands', available:
|
|
665
|
+
{ key: 'commands', label: 'Commands', available: supports(agent, 'commands').ok && available.commands.length > 0, displayCount: `${available.commands.length} available` },
|
|
871
666
|
{ key: 'skills', label: 'Skills', available: available.skills.length > 0, displayCount: `${available.skills.length} available` },
|
|
872
667
|
{ key: 'hooks', label: 'Hooks', available: agentConfig.supportsHooks && available.hooks.length > 0, displayCount: `${available.hooks.length} available` },
|
|
873
|
-
{ key: 'memory', label: 'Rules', available:
|
|
874
|
-
{ key: 'mcp', label: 'MCPs', available:
|
|
875
|
-
{ key: 'permissions', label: 'Permissions', available:
|
|
876
|
-
{ key: 'subagents', label: 'Subagents', available:
|
|
877
|
-
{ key: 'plugins', label: 'Plugins', available:
|
|
668
|
+
{ key: 'memory', label: 'Rules', available: supports(agent, 'rules').ok && available.memory.length > 0, displayCount: `${available.memory.length} available` },
|
|
669
|
+
{ key: 'mcp', label: 'MCPs', available: supports(agent, 'mcp').ok && available.mcp.length > 0, displayCount: `${available.mcp.length} available` },
|
|
670
|
+
{ key: 'permissions', label: 'Permissions', available: supports(agent, 'allowlist').ok && permissionGroups.length > 0, displayCount: `${permissionGroups.length} groups, ${totalPermissionRules} rules` },
|
|
671
|
+
{ key: 'subagents', label: 'Subagents', available: supports(agent, 'subagents').ok && available.subagents.length > 0, displayCount: `${available.subagents.length} available` },
|
|
672
|
+
{ key: 'plugins', label: 'Plugins', available: supports(agent, 'plugins').ok && available.plugins.length > 0, displayCount: `${available.plugins.length} available` },
|
|
878
673
|
];
|
|
879
674
|
const availableCategories = categories.filter(c => c.available);
|
|
880
675
|
if (availableCategories.length === 0) {
|
|
@@ -1051,7 +846,7 @@ export async function getLatestNpmVersion(agent) {
|
|
|
1051
846
|
if (!agentConfig.npmPackage)
|
|
1052
847
|
return null;
|
|
1053
848
|
try {
|
|
1054
|
-
const { stdout } = await execFileAsync('npm', ['view', agentConfig.npmPackage, 'version']);
|
|
849
|
+
const { stdout } = await execFileAsync('npm', ['view', agentConfig.npmPackage, 'version'], { shell: process.platform === 'win32' });
|
|
1055
850
|
return stdout.trim();
|
|
1056
851
|
}
|
|
1057
852
|
catch {
|
|
@@ -1187,8 +982,8 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1187
982
|
// ~/.grok/downloads), so we skip the symlink there.
|
|
1188
983
|
if (agent !== 'grok') {
|
|
1189
984
|
try {
|
|
1190
|
-
const { stdout: whichOut } = await execFileAsync('which', [agentConfig.cliCommand]);
|
|
1191
|
-
const installedBinary = whichOut.trim();
|
|
985
|
+
const { stdout: whichOut } = await execFileAsync(process.platform === 'win32' ? 'where' : 'which', [agentConfig.cliCommand]);
|
|
986
|
+
const installedBinary = whichOut.trim().split('\n')[0];
|
|
1192
987
|
if (installedBinary && fs.existsSync(installedBinary)) {
|
|
1193
988
|
importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
|
|
1194
989
|
}
|
|
@@ -1219,8 +1014,9 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1219
1014
|
: `${agentConfig.npmPackage}@${version}`;
|
|
1220
1015
|
try {
|
|
1221
1016
|
// Check npm is available
|
|
1017
|
+
const winShell = process.platform === 'win32';
|
|
1222
1018
|
try {
|
|
1223
|
-
await execFileAsync('
|
|
1019
|
+
await execFileAsync('npm', ['--version'], { shell: winShell });
|
|
1224
1020
|
}
|
|
1225
1021
|
catch {
|
|
1226
1022
|
return {
|
|
@@ -1230,7 +1026,7 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1230
1026
|
};
|
|
1231
1027
|
}
|
|
1232
1028
|
onProgress?.(`Installing ${packageSpec}...`);
|
|
1233
|
-
const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir });
|
|
1029
|
+
const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir, shell: winShell });
|
|
1234
1030
|
// Determine the actual installed version
|
|
1235
1031
|
let installedVersion = version;
|
|
1236
1032
|
if (version === 'latest') {
|
|
@@ -1303,10 +1099,10 @@ function removeInstallArtifacts(versionDir) {
|
|
|
1303
1099
|
}
|
|
1304
1100
|
}
|
|
1305
1101
|
/**
|
|
1306
|
-
* Soft-delete a version directory by moving it to ~/.agents
|
|
1102
|
+
* Soft-delete a version directory by moving it to ~/.agents/.system/trash/versions/.
|
|
1307
1103
|
* Returns the trash path on success or null on failure / no source.
|
|
1308
1104
|
*
|
|
1309
|
-
* Trash layout: ~/.agents
|
|
1105
|
+
* Trash layout: ~/.agents/.system/trash/versions/<agent>/<version>/<timestamp>/
|
|
1310
1106
|
* The timestamp suffix lets a user soft-delete the same version twice (after
|
|
1311
1107
|
* re-install) without collision and gives a chronological audit trail.
|
|
1312
1108
|
*
|
|
@@ -1335,7 +1131,7 @@ export function softDeleteVersionDir(agent, version) {
|
|
|
1335
1131
|
* Remove a specific version of an agent.
|
|
1336
1132
|
*
|
|
1337
1133
|
* Soft-delete only: moves the entire version directory (including `home/`)
|
|
1338
|
-
* to ~/.agents
|
|
1134
|
+
* to ~/.agents/.system/trash/versions/. Recoverable via `agents trash restore`.
|
|
1339
1135
|
* Nothing is hard-deleted.
|
|
1340
1136
|
*/
|
|
1341
1137
|
export function removeVersion(agent, version) {
|
|
@@ -1474,7 +1270,7 @@ export function resolveVersionAliasLoose(agent, raw) {
|
|
|
1474
1270
|
return raw;
|
|
1475
1271
|
}
|
|
1476
1272
|
/**
|
|
1477
|
-
* Get version specified in a project-root agents.yaml (not the user ~/.agents
|
|
1273
|
+
* Get version specified in a project-root agents.yaml (not the user ~/.agents/.system/agents.yaml).
|
|
1478
1274
|
*/
|
|
1479
1275
|
export function getProjectVersion(agent, startPath) {
|
|
1480
1276
|
const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
@@ -1540,8 +1336,7 @@ export async function getInstalledVersion(agent, version) {
|
|
|
1540
1336
|
async function getCliVersionFromPath(agent) {
|
|
1541
1337
|
const agentConfig = AGENTS[agent];
|
|
1542
1338
|
try {
|
|
1543
|
-
await execFileAsync('
|
|
1544
|
-
const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000 });
|
|
1339
|
+
const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000, shell: process.platform === 'win32' });
|
|
1545
1340
|
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
1546
1341
|
return match ? match[1] : null;
|
|
1547
1342
|
}
|
|
@@ -1839,59 +1634,23 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1839
1634
|
}
|
|
1840
1635
|
return [];
|
|
1841
1636
|
};
|
|
1842
|
-
// Sync commands
|
|
1637
|
+
// Sync commands — dispatch through WRITERS.commands. The writer dispatches
|
|
1638
|
+
// between native (file copy / TOML conversion) and commands-as-skills
|
|
1639
|
+
// (grok, Codex >= 0.117.0) based on `shouldInstallCommandAsSkill`. The
|
|
1640
|
+
// previous COMMANDS_CAPABLE_AGENTS gate excluded grok even though it
|
|
1641
|
+
// takes the commands-as-skills path — silently dropping every command.
|
|
1642
|
+
const commandsWriter = getWriter('commands', agent);
|
|
1843
1643
|
const commandsToSync = selection
|
|
1844
1644
|
? resolveSelection(selection.commands, available.commands)
|
|
1845
1645
|
: available.commands; // No selection = sync all
|
|
1846
|
-
if (commandsToSync.length > 0 &&
|
|
1646
|
+
if (commandsToSync.length > 0 && commandsWriter) {
|
|
1847
1647
|
const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1848
1648
|
const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
|
|
1849
1649
|
if (commandsAsSkills) {
|
|
1850
1650
|
removePath(commandsTarget);
|
|
1851
1651
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
}
|
|
1855
|
-
const syncedCommands = [];
|
|
1856
|
-
for (const cmd of commandsToSync) {
|
|
1857
|
-
// Commands are content that gets injected into the agent's prompt
|
|
1858
|
-
// surface (slash commands, skill bodies). We intentionally do NOT pull
|
|
1859
|
-
// from the project's own .agents/commands/ directory: a cloned public
|
|
1860
|
-
// repo could ship a command whose body instructs the agent to do
|
|
1861
|
-
// something harmful the next time the user invokes it. Commands must
|
|
1862
|
-
// come from the user's central ~/.agents/commands/, the system layer,
|
|
1863
|
-
// or an explicitly enabled extra repo. Same defense as hooks below.
|
|
1864
|
-
const candidates = [
|
|
1865
|
-
safeJoin(path.join(userAgentsDir, 'commands'), `${cmd}.md`),
|
|
1866
|
-
safeJoin(getCommandsDir(), `${cmd}.md`),
|
|
1867
|
-
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'commands'), `${cmd}.md`)),
|
|
1868
|
-
];
|
|
1869
|
-
const srcFile = candidates.find((p) => p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink()) || null;
|
|
1870
|
-
if (!srcFile)
|
|
1871
|
-
continue;
|
|
1872
|
-
if (commandsAsSkills) {
|
|
1873
|
-
// Project skills dir is intentionally excluded for the same reason
|
|
1874
|
-
// commands are: the body of a project skill becomes agent context.
|
|
1875
|
-
const skillSourceDirs = [
|
|
1876
|
-
path.join(userAgentsDir, 'skills'),
|
|
1877
|
-
getSkillsDir(),
|
|
1878
|
-
...extraRepos.map((e) => path.join(e.dir, 'skills')),
|
|
1879
|
-
];
|
|
1880
|
-
const installed = installCommandSkillToVersion(agentDir, cmd, srcFile, skillSourceDirs);
|
|
1881
|
-
if (!installed.success)
|
|
1882
|
-
continue;
|
|
1883
|
-
}
|
|
1884
|
-
else if (agentConfig.format === 'toml') {
|
|
1885
|
-
const content = fs.readFileSync(srcFile, 'utf-8');
|
|
1886
|
-
const tomlContent = markdownToToml(cmd, content);
|
|
1887
|
-
fs.writeFileSync(safeJoin(commandsTarget, `${cmd}.toml`), tomlContent);
|
|
1888
|
-
}
|
|
1889
|
-
else {
|
|
1890
|
-
fs.copyFileSync(srcFile, safeJoin(commandsTarget, `${cmd}.md`));
|
|
1891
|
-
}
|
|
1892
|
-
syncedCommands.push(cmd);
|
|
1893
|
-
}
|
|
1894
|
-
result.commands = syncedCommands.length > 0;
|
|
1652
|
+
const r = commandsWriter.write({ version, versionHome, selection: commandsToSync, cwd });
|
|
1653
|
+
result.commands = r.synced.length > 0;
|
|
1895
1654
|
}
|
|
1896
1655
|
// Orphan-sweep stale top-level command files from previous syncs under a
|
|
1897
1656
|
// different cwd. Only runs in "full sync" mode — i.e. when the caller did
|
|
@@ -1900,7 +1659,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1900
1659
|
// alone), so the sweep would be a contract violation there. The
|
|
1901
1660
|
// cross-project leak always comes from the no-selection shim auto-sync at
|
|
1902
1661
|
// launch.
|
|
1903
|
-
if (!userPassedSelection &&
|
|
1662
|
+
if (!userPassedSelection && commandsWriter && !shouldInstallCommandAsSkill(agent, version)) {
|
|
1904
1663
|
const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1905
1664
|
if (fs.existsSync(commandsTargetSweep)) {
|
|
1906
1665
|
const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
|
|
@@ -1917,51 +1676,20 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1917
1676
|
}
|
|
1918
1677
|
}
|
|
1919
1678
|
}
|
|
1920
|
-
// Sync skills
|
|
1679
|
+
// Sync skills — dispatch through WRITERS.skills. Agents that natively read
|
|
1680
|
+
// ~/.agents/skills/ (Gemini) are not registered; we clear the version-home
|
|
1681
|
+
// skills dir for them so a stale per-version copy never shadows central.
|
|
1682
|
+
const skillsWriter = getWriter('skills', agent);
|
|
1683
|
+
const skillsToSync = selection
|
|
1684
|
+
? resolveSelection(selection.skills, available.skills)
|
|
1685
|
+
: available.skills;
|
|
1921
1686
|
if (agentConfig.nativeAgentsSkillsDir) {
|
|
1922
|
-
|
|
1923
|
-
const skillsTarget = path.join(agentDir, 'skills');
|
|
1924
|
-
removePath(skillsTarget);
|
|
1687
|
+
removePath(path.join(agentDir, 'skills'));
|
|
1925
1688
|
}
|
|
1926
|
-
else {
|
|
1927
|
-
const skillsToSync = selection
|
|
1928
|
-
? resolveSelection(selection.skills, available.skills)
|
|
1929
|
-
: available.skills;
|
|
1689
|
+
else if (skillsWriter) {
|
|
1930
1690
|
if (skillsToSync.length > 0) {
|
|
1931
|
-
const
|
|
1932
|
-
|
|
1933
|
-
// parent symlink before touching children so removePath(destDir) cannot
|
|
1934
|
-
// delete the central source through it.
|
|
1935
|
-
try {
|
|
1936
|
-
if (fs.lstatSync(skillsTarget).isSymbolicLink()) {
|
|
1937
|
-
removePath(skillsTarget);
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
catch { /* target does not exist yet */ }
|
|
1941
|
-
fs.mkdirSync(skillsTarget, { recursive: true });
|
|
1942
|
-
const syncedSkills = [];
|
|
1943
|
-
for (const skill of skillsToSync) {
|
|
1944
|
-
// Same defense as commands and hooks: don't pull skills from the
|
|
1945
|
-
// project's .agents/skills/ directory. A skill's contents (SKILL.md
|
|
1946
|
-
// and any auxiliary scripts) get loaded into the agent's tool/context
|
|
1947
|
-
// surface, and a malicious public repo could ship a SKILL.md whose
|
|
1948
|
-
// body coerces the agent. Trusted layers only.
|
|
1949
|
-
const skillCandidates = [
|
|
1950
|
-
safeJoin(path.join(userAgentsDir, 'skills'), skill),
|
|
1951
|
-
safeJoin(getSkillsDir(), skill),
|
|
1952
|
-
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'skills'), skill)),
|
|
1953
|
-
];
|
|
1954
|
-
const srcDir = skillCandidates.find((p) => fs.existsSync(p) &&
|
|
1955
|
-
!fs.lstatSync(p).isSymbolicLink() &&
|
|
1956
|
-
fs.lstatSync(p).isDirectory()) || null;
|
|
1957
|
-
if (!srcDir)
|
|
1958
|
-
continue;
|
|
1959
|
-
const destDir = safeJoin(skillsTarget, skill);
|
|
1960
|
-
removePath(destDir);
|
|
1961
|
-
copyDir(srcDir, destDir);
|
|
1962
|
-
syncedSkills.push(skill);
|
|
1963
|
-
}
|
|
1964
|
-
result.skills = syncedSkills.length > 0;
|
|
1691
|
+
const r = skillsWriter.write({ version, versionHome, selection: skillsToSync, cwd });
|
|
1692
|
+
result.skills = r.synced.length > 0;
|
|
1965
1693
|
}
|
|
1966
1694
|
// Orphan-sweep stale skill directories from previous syncs under a
|
|
1967
1695
|
// different cwd. Only runs in "full sync" mode (no explicit selection) —
|
|
@@ -1979,9 +1707,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1979
1707
|
}
|
|
1980
1708
|
}
|
|
1981
1709
|
}
|
|
1982
|
-
// Sync hooks
|
|
1710
|
+
// Sync hooks — dispatch through WRITERS.hooks. supports() gate enforces
|
|
1711
|
+
// the version cutoff (codex >= 0.116.0, gemini >= 0.26.0).
|
|
1983
1712
|
const hooksGate = supports(agent, 'hooks', version);
|
|
1984
|
-
|
|
1713
|
+
const hooksWriter = getWriter('hooks', agent);
|
|
1714
|
+
if (agentConfig.supportsHooks && hooksWriter) {
|
|
1985
1715
|
if (!hooksGate.ok) {
|
|
1986
1716
|
console.warn(explainSkip(agent, 'hooks', hooksGate, version) + ' -- skipped');
|
|
1987
1717
|
}
|
|
@@ -1990,35 +1720,13 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1990
1720
|
? resolveSelection(selection.hooks, available.hooks)
|
|
1991
1721
|
: available.hooks;
|
|
1992
1722
|
if (hooksToSync.length > 0) {
|
|
1993
|
-
const
|
|
1994
|
-
const hooksTarget = path.join(agentDir, 'hooks');
|
|
1995
|
-
fs.mkdirSync(hooksTarget, { recursive: true });
|
|
1996
|
-
const syncedHooks = [];
|
|
1997
|
-
for (const hook of hooksToSync) {
|
|
1998
|
-
// Hooks are executable shell scripts that run on agent events. We
|
|
1999
|
-
// intentionally do NOT pull from the project's own .agents/hooks/
|
|
2000
|
-
// directory: that would let any cloned public repo plant an
|
|
2001
|
-
// executable that fires the next time the user runs `agents use`
|
|
2002
|
-
// inside that repo. Hooks must come from the user's central
|
|
2003
|
-
// ~/.agents/hooks/ or an explicitly enabled extra repo.
|
|
2004
|
-
const candidates = [
|
|
2005
|
-
safeJoin(path.join(userAgentsDir, 'hooks'), hook),
|
|
2006
|
-
safeJoin(centralHooks, hook),
|
|
2007
|
-
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'hooks'), hook)),
|
|
2008
|
-
];
|
|
2009
|
-
const srcFile = candidates.find((p) => p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink()) || null;
|
|
2010
|
-
if (!srcFile)
|
|
2011
|
-
continue;
|
|
2012
|
-
const destFile = safeJoin(hooksTarget, hook);
|
|
2013
|
-
fs.copyFileSync(srcFile, destFile);
|
|
2014
|
-
fs.chmodSync(destFile, 0o755);
|
|
2015
|
-
syncedHooks.push(hook);
|
|
2016
|
-
}
|
|
1723
|
+
const r = hooksWriter.write({ version, versionHome, selection: hooksToSync, cwd });
|
|
2017
1724
|
// Remove orphan files from version home. The trusted set is the
|
|
2018
1725
|
// manifest-declared hook list (`available.hooks`) — auxiliary files
|
|
2019
1726
|
// like README.md or promptcuts.yaml may exist alongside hooks at the
|
|
2020
1727
|
// source but are not hooks and must not linger in version homes from
|
|
2021
1728
|
// older syncs.
|
|
1729
|
+
const hooksTarget = path.join(agentDir, 'hooks');
|
|
2022
1730
|
const trustedHookNames = new Set(available.hooks);
|
|
2023
1731
|
if (fs.existsSync(hooksTarget)) {
|
|
2024
1732
|
for (const file of fs.readdirSync(hooksTarget).filter(f => !f.startsWith('.'))) {
|
|
@@ -2027,23 +1735,19 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2027
1735
|
}
|
|
2028
1736
|
}
|
|
2029
1737
|
}
|
|
2030
|
-
result.hooks =
|
|
2031
|
-
// Register hooks into agent-native settings.json/hooks.json. Gemini
|
|
2032
|
-
// shipped hooks in 0.26.0; gate already passed above so this is safe.
|
|
2033
|
-
// Grok auto-discovers from ~/.grok/hooks/ so the script copy above
|
|
2034
|
-
// is sufficient — no settings.json registration needed.
|
|
2035
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity') {
|
|
2036
|
-
registerHooksToSettings(agent, versionHome);
|
|
2037
|
-
}
|
|
1738
|
+
result.hooks = r.synced.length > 0;
|
|
2038
1739
|
}
|
|
2039
1740
|
}
|
|
2040
1741
|
}
|
|
2041
|
-
// Sync rules —
|
|
2042
|
-
// single
|
|
2043
|
-
//
|
|
2044
|
-
//
|
|
1742
|
+
// Sync rules — dispatch through WRITERS.rules. The registry routes to the
|
|
1743
|
+
// single-target writer for any agent that declares `rules: { file }` in
|
|
1744
|
+
// its capability matrix (grok included; the previous gate used the wrong
|
|
1745
|
+
// CAPABLE_AGENTS list and silently skipped it). Project rules are NOT
|
|
1746
|
+
// synced into the version home — they are composed into the workspace at
|
|
1747
|
+
// agents-run time (see compileRulesForProject).
|
|
2045
1748
|
const skipMemory = selection && (selection.memory === undefined || (Array.isArray(selection.memory) && selection.memory.length === 0));
|
|
2046
|
-
|
|
1749
|
+
const rulesWriter = getWriter('rules', agent);
|
|
1750
|
+
if (!skipMemory && rulesWriter) {
|
|
2047
1751
|
try {
|
|
2048
1752
|
// If selection.memory names a single preset, treat it as a one-shot
|
|
2049
1753
|
// override; otherwise read the persisted active preset.
|
|
@@ -2051,13 +1755,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2051
1755
|
? selection.memory[0]
|
|
2052
1756
|
: null;
|
|
2053
1757
|
const preset = overridePreset || getActiveRulesPreset(agent, version);
|
|
2054
|
-
const
|
|
2055
|
-
|
|
2056
|
-
const destFile = safeJoin(agentDir, targetName);
|
|
2057
|
-
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
2058
|
-
removePath(destFile);
|
|
2059
|
-
fs.writeFileSync(destFile, composed.content);
|
|
2060
|
-
result.memory.push(targetName);
|
|
1758
|
+
const r = rulesWriter.write({ version, versionHome, selection: { preset }, cwd });
|
|
1759
|
+
result.memory.push(...r.synced);
|
|
2061
1760
|
// rulesPreset is tracked separately via setActiveRulesPreset.
|
|
2062
1761
|
}
|
|
2063
1762
|
catch (err) {
|
|
@@ -2089,6 +1788,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2089
1788
|
console.warn(`${PERMISSION_PRESET_ENV_VAR}=${activePresetName} but no recipe at ~/.agents/permissions/presets/${activePresetName}.yaml — falling back to all groups`);
|
|
2090
1789
|
}
|
|
2091
1790
|
}
|
|
1791
|
+
const permissionsWriter = getWriter('permissions', agent);
|
|
2092
1792
|
let permsToSync;
|
|
2093
1793
|
if (selection) {
|
|
2094
1794
|
permsToSync = resolveSelection(selection.permissions, allGroupNames);
|
|
@@ -2103,18 +1803,12 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2103
1803
|
}
|
|
2104
1804
|
}
|
|
2105
1805
|
else {
|
|
2106
|
-
permsToSync =
|
|
2107
|
-
? (presetFilteredGroups ?? allGroupNames)
|
|
2108
|
-
: [];
|
|
1806
|
+
permsToSync = permissionsWriter ? (presetFilteredGroups ?? allGroupNames) : [];
|
|
2109
1807
|
}
|
|
2110
|
-
if (permsToSync.length > 0 &&
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
const permResult = applyPermsToVersion(agent, builtPerms, versionHome, true);
|
|
2115
|
-
result.permissions = permResult.success;
|
|
2116
|
-
// permissions patterns already written via ensureVersionResourcePatterns above.
|
|
2117
|
-
}
|
|
1808
|
+
if (permsToSync.length > 0 && permissionsWriter) {
|
|
1809
|
+
const r = permissionsWriter.write({ version, versionHome, selection: permsToSync, cwd });
|
|
1810
|
+
result.permissions = r.synced.length > 0;
|
|
1811
|
+
// permissions patterns already written via ensureVersionResourcePatterns above.
|
|
2118
1812
|
}
|
|
2119
1813
|
// Install MCP servers (if agent supports them)
|
|
2120
1814
|
// For Claude/Codex: uses CLI commands (claude mcp add, codex mcp add)
|
|
@@ -2129,57 +1823,29 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2129
1823
|
// user entry, so name-collision shadowing is not fully closed here —
|
|
2130
1824
|
// tracked separately for a follow-up in lib/mcp.ts.)
|
|
2131
1825
|
const projectScopedMcpNames = new Set(getScopedMcpResources(cwd).filter(r => r.scope === 'project').map(r => r.name));
|
|
1826
|
+
const mcpWriter = getWriter('mcp', agent);
|
|
2132
1827
|
const mcpToSyncAll = selection
|
|
2133
1828
|
? resolveSelection(selection.mcp, available.mcp)
|
|
2134
|
-
: (
|
|
1829
|
+
: (mcpWriter ? available.mcp : []);
|
|
2135
1830
|
const mcpToSync = mcpToSyncAll.filter(n => !projectScopedMcpNames.has(n));
|
|
2136
|
-
if (mcpToSync.length > 0 &&
|
|
2137
|
-
const
|
|
2138
|
-
result.mcp =
|
|
1831
|
+
if (mcpToSync.length > 0 && mcpWriter) {
|
|
1832
|
+
const r = mcpWriter.write({ version, versionHome, selection: mcpToSync, cwd });
|
|
1833
|
+
result.mcp = r.synced;
|
|
2139
1834
|
// mcp patterns already written via ensureVersionResourcePatterns above.
|
|
2140
1835
|
}
|
|
2141
|
-
// Sync subagents
|
|
2142
|
-
//
|
|
2143
|
-
//
|
|
2144
|
-
|
|
2145
|
-
// to plant a subagent the user later invokes. Same defense as hooks.
|
|
1836
|
+
// Sync subagents — dispatch through WRITERS.subagents. listInstalledSubagents
|
|
1837
|
+
// reads only user + system layers (project excluded for the same defense
|
|
1838
|
+
// as commands/skills/hooks).
|
|
1839
|
+
const subagentsWriter = getWriter('subagents', agent);
|
|
2146
1840
|
const subagentsToSync = selection
|
|
2147
1841
|
? resolveSelection(selection.subagents, available.subagents)
|
|
2148
|
-
: (
|
|
2149
|
-
if (subagentsToSync.length > 0 &&
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
for
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
continue;
|
|
2156
|
-
try {
|
|
2157
|
-
if (agent === 'claude') {
|
|
2158
|
-
// Claude: flatten to single .md file
|
|
2159
|
-
const agentsDir = path.join(agentDir, 'agents');
|
|
2160
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
2161
|
-
const transformed = transformSubagentForClaude(subagent.path);
|
|
2162
|
-
fs.writeFileSync(safeJoin(agentsDir, `${subagent.name}.md`), transformed);
|
|
2163
|
-
result.subagents.push(subagent.name);
|
|
2164
|
-
}
|
|
2165
|
-
else if (agent === 'openclaw') {
|
|
2166
|
-
// OpenClaw: copy full directory, rename AGENT.md -> AGENTS.md
|
|
2167
|
-
const targetDir = safeJoin(path.join(versionHome, '.openclaw'), subagent.name);
|
|
2168
|
-
const syncResult = syncSubagentToOpenclaw(subagent.path, targetDir);
|
|
2169
|
-
if (syncResult.success) {
|
|
2170
|
-
result.subagents.push(subagent.name);
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
catch { /* resource sync failed for this item */ }
|
|
2175
|
-
}
|
|
2176
|
-
// Orphan-sweep stale subagents. Same selection-mode guard as the
|
|
2177
|
-
// commands/skills sweeps above. Claude stores them as flat .md files
|
|
2178
|
-
// under `<agentDir>/agents/`; OpenClaw stores them as subdirs at the
|
|
2179
|
-
// same level as commands/skills/hooks/plugins (no isolated parent dir),
|
|
2180
|
-
// which means a directory-readdir sweep would unsafely hit unrelated
|
|
2181
|
-
// resources. For OpenClaw we lean on the existing per-name copy path —
|
|
2182
|
-
// if the user wants strict isolation on OpenClaw, track via manifest.
|
|
1842
|
+
: (subagentsWriter ? available.subagents : []);
|
|
1843
|
+
if (subagentsToSync.length > 0 && subagentsWriter) {
|
|
1844
|
+
const r = subagentsWriter.write({ version, versionHome, selection: subagentsToSync, cwd });
|
|
1845
|
+
result.subagents.push(...r.synced);
|
|
1846
|
+
// Orphan-sweep for Claude only — see comment on commands/skills sweep
|
|
1847
|
+
// for the no-selection guard. OpenClaw stores subagents as siblings of
|
|
1848
|
+
// other resources so a readdir sweep would over-reach.
|
|
2183
1849
|
if (!userPassedSelection && agent === 'claude') {
|
|
2184
1850
|
const claudeAgentsDir = path.join(agentDir, 'agents');
|
|
2185
1851
|
if (fs.existsSync(claudeAgentsDir)) {
|
|
@@ -2196,48 +1862,24 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2196
1862
|
}
|
|
2197
1863
|
}
|
|
2198
1864
|
}
|
|
2199
|
-
// subagent patterns already written via ensureVersionResourcePatterns above.
|
|
2200
1865
|
}
|
|
2201
|
-
// Sync plugins
|
|
1866
|
+
// Sync plugins — dispatch through WRITERS.plugins.
|
|
1867
|
+
const pluginsWriter = getWriter('plugins', agent);
|
|
2202
1868
|
const pluginsToSync = selection
|
|
2203
1869
|
? resolveSelection(selection.plugins, available.plugins)
|
|
2204
|
-
: (
|
|
2205
|
-
if (pluginsToSync.length > 0 &&
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
// Clean orphaned plugin skills from plugins that no longer exist
|
|
2209
|
-
const activePluginNames = new Set(allPlugins.map(p => p.name));
|
|
2210
|
-
cleanOrphanedPluginSkills(agent, versionHome, activePluginNames);
|
|
2211
|
-
for (const name of pluginsToSync) {
|
|
2212
|
-
const plugin = pluginMap.get(name);
|
|
2213
|
-
if (!plugin || !pluginSupportsAgent(plugin, agent))
|
|
2214
|
-
continue;
|
|
2215
|
-
const pluginResult = syncPluginToVersion(plugin, agent, versionHome, { version });
|
|
2216
|
-
if (pluginResult.success) {
|
|
2217
|
-
result.plugins.push(name);
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
// plugin patterns already written via ensureVersionResourcePatterns above.
|
|
1870
|
+
: (pluginsWriter ? available.plugins : []);
|
|
1871
|
+
if (pluginsToSync.length > 0 && pluginsWriter) {
|
|
1872
|
+
const r = pluginsWriter.write({ version, versionHome, selection: pluginsToSync, cwd });
|
|
1873
|
+
result.plugins.push(...r.synced);
|
|
2221
1874
|
}
|
|
2222
|
-
// Sync workflows
|
|
1875
|
+
// Sync workflows — dispatch through WRITERS.workflows.
|
|
1876
|
+
const workflowsWriter = getWriter('workflows', agent);
|
|
2223
1877
|
const workflowsToSync = selection
|
|
2224
1878
|
? resolveSelection(selection.workflows, available.workflows)
|
|
2225
|
-
: (
|
|
2226
|
-
if (workflowsToSync.length > 0 &&
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
const workflow = allWorkflows.get(name);
|
|
2230
|
-
if (!workflow)
|
|
2231
|
-
continue;
|
|
2232
|
-
try {
|
|
2233
|
-
const syncResult = syncWorkflowToVersion(workflow.path, name, agent, versionHome);
|
|
2234
|
-
if (syncResult.success) {
|
|
2235
|
-
result.workflows.push(name);
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
catch { /* resource sync failed for this item */ }
|
|
2239
|
-
}
|
|
2240
|
-
// workflow patterns already written via ensureVersionResourcePatterns above.
|
|
1879
|
+
: (workflowsWriter ? available.workflows : []);
|
|
1880
|
+
if (workflowsToSync.length > 0 && workflowsWriter) {
|
|
1881
|
+
const r = workflowsWriter.write({ version, versionHome, selection: workflowsToSync, cwd });
|
|
1882
|
+
result.workflows.push(...r.synced);
|
|
2241
1883
|
}
|
|
2242
1884
|
// Write manifest after a full sync (no user-passed selection) so the next
|
|
2243
1885
|
// launch can skip the slow path. Pattern-derived selections still count as
|