claude-autopm 2.8.1 → 2.8.2
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/README.md +116 -8
- package/bin/autopm.js +2 -0
- package/bin/commands/plugin.js +395 -0
- package/bin/commands/team.js +184 -10
- package/install/install.js +223 -4
- package/lib/plugins/PluginManager.js +1328 -0
- package/lib/plugins/PluginManager.old.js +400 -0
- package/package.json +4 -1
- package/scripts/publish-plugins.sh +166 -0
- package/autopm/.claude/agents/cloud/README.md +0 -55
- package/autopm/.claude/agents/cloud/aws-cloud-architect.md +0 -521
- package/autopm/.claude/agents/cloud/azure-cloud-architect.md +0 -436
- package/autopm/.claude/agents/cloud/gcp-cloud-architect.md +0 -385
- package/autopm/.claude/agents/cloud/gcp-cloud-functions-engineer.md +0 -306
- package/autopm/.claude/agents/cloud/gemini-api-expert.md +0 -880
- package/autopm/.claude/agents/cloud/kubernetes-orchestrator.md +0 -566
- package/autopm/.claude/agents/cloud/openai-python-expert.md +0 -1087
- package/autopm/.claude/agents/cloud/terraform-infrastructure-expert.md +0 -454
- package/autopm/.claude/agents/core/agent-manager.md +0 -296
- package/autopm/.claude/agents/core/code-analyzer.md +0 -131
- package/autopm/.claude/agents/core/file-analyzer.md +0 -162
- package/autopm/.claude/agents/core/test-runner.md +0 -200
- package/autopm/.claude/agents/data/airflow-orchestration-expert.md +0 -52
- package/autopm/.claude/agents/data/kedro-pipeline-expert.md +0 -50
- package/autopm/.claude/agents/data/langgraph-workflow-expert.md +0 -520
- package/autopm/.claude/agents/databases/README.md +0 -50
- package/autopm/.claude/agents/databases/bigquery-expert.md +0 -392
- package/autopm/.claude/agents/databases/cosmosdb-expert.md +0 -368
- package/autopm/.claude/agents/databases/mongodb-expert.md +0 -398
- package/autopm/.claude/agents/databases/postgresql-expert.md +0 -321
- package/autopm/.claude/agents/databases/redis-expert.md +0 -52
- package/autopm/.claude/agents/devops/README.md +0 -52
- package/autopm/.claude/agents/devops/azure-devops-specialist.md +0 -308
- package/autopm/.claude/agents/devops/docker-containerization-expert.md +0 -298
- package/autopm/.claude/agents/devops/github-operations-specialist.md +0 -335
- package/autopm/.claude/agents/devops/mcp-context-manager.md +0 -319
- package/autopm/.claude/agents/devops/observability-engineer.md +0 -574
- package/autopm/.claude/agents/devops/ssh-operations-expert.md +0 -1093
- package/autopm/.claude/agents/devops/traefik-proxy-expert.md +0 -444
- package/autopm/.claude/agents/frameworks/README.md +0 -64
- package/autopm/.claude/agents/frameworks/e2e-test-engineer.md +0 -360
- package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +0 -254
- package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +0 -217
- package/autopm/.claude/agents/frameworks/react-ui-expert.md +0 -226
- package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +0 -770
- package/autopm/.claude/agents/frameworks/ux-design-expert.md +0 -244
- package/autopm/.claude/agents/integration/message-queue-engineer.md +0 -794
- package/autopm/.claude/agents/languages/README.md +0 -50
- package/autopm/.claude/agents/languages/bash-scripting-expert.md +0 -541
- package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +0 -197
- package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +0 -226
- package/autopm/.claude/agents/languages/python-backend-engineer.md +0 -214
- package/autopm/.claude/agents/languages/python-backend-expert.md +0 -289
- package/autopm/.claude/agents/testing/frontend-testing-engineer.md +0 -395
- package/autopm/.claude/commands/ai/langgraph-workflow.md +0 -65
- package/autopm/.claude/commands/ai/openai-chat.md +0 -65
- package/autopm/.claude/commands/azure/COMMANDS.md +0 -107
- package/autopm/.claude/commands/azure/COMMAND_MAPPING.md +0 -252
- package/autopm/.claude/commands/azure/INTEGRATION_FIX.md +0 -103
- package/autopm/.claude/commands/azure/README.md +0 -246
- package/autopm/.claude/commands/azure/active-work.md +0 -198
- package/autopm/.claude/commands/azure/aliases.md +0 -143
- package/autopm/.claude/commands/azure/blocked-items.md +0 -287
- package/autopm/.claude/commands/azure/clean.md +0 -93
- package/autopm/.claude/commands/azure/docs-query.md +0 -48
- package/autopm/.claude/commands/azure/feature-decompose.md +0 -380
- package/autopm/.claude/commands/azure/feature-list.md +0 -61
- package/autopm/.claude/commands/azure/feature-new.md +0 -115
- package/autopm/.claude/commands/azure/feature-show.md +0 -205
- package/autopm/.claude/commands/azure/feature-start.md +0 -130
- package/autopm/.claude/commands/azure/fix-integration-example.md +0 -93
- package/autopm/.claude/commands/azure/help.md +0 -150
- package/autopm/.claude/commands/azure/import-us.md +0 -269
- package/autopm/.claude/commands/azure/init.md +0 -211
- package/autopm/.claude/commands/azure/next-task.md +0 -262
- package/autopm/.claude/commands/azure/search.md +0 -160
- package/autopm/.claude/commands/azure/sprint-status.md +0 -235
- package/autopm/.claude/commands/azure/standup.md +0 -260
- package/autopm/.claude/commands/azure/sync-all.md +0 -99
- package/autopm/.claude/commands/azure/task-analyze.md +0 -186
- package/autopm/.claude/commands/azure/task-close.md +0 -329
- package/autopm/.claude/commands/azure/task-edit.md +0 -145
- package/autopm/.claude/commands/azure/task-list.md +0 -263
- package/autopm/.claude/commands/azure/task-new.md +0 -84
- package/autopm/.claude/commands/azure/task-reopen.md +0 -79
- package/autopm/.claude/commands/azure/task-show.md +0 -126
- package/autopm/.claude/commands/azure/task-start.md +0 -301
- package/autopm/.claude/commands/azure/task-status.md +0 -65
- package/autopm/.claude/commands/azure/task-sync.md +0 -67
- package/autopm/.claude/commands/azure/us-edit.md +0 -164
- package/autopm/.claude/commands/azure/us-list.md +0 -202
- package/autopm/.claude/commands/azure/us-new.md +0 -265
- package/autopm/.claude/commands/azure/us-parse.md +0 -253
- package/autopm/.claude/commands/azure/us-show.md +0 -188
- package/autopm/.claude/commands/azure/us-status.md +0 -320
- package/autopm/.claude/commands/azure/validate.md +0 -86
- package/autopm/.claude/commands/azure/work-item-sync.md +0 -47
- package/autopm/.claude/commands/cloud/infra-deploy.md +0 -38
- package/autopm/.claude/commands/github/workflow-create.md +0 -42
- package/autopm/.claude/commands/infrastructure/ssh-security.md +0 -65
- package/autopm/.claude/commands/infrastructure/traefik-setup.md +0 -65
- package/autopm/.claude/commands/kubernetes/deploy.md +0 -37
- package/autopm/.claude/commands/playwright/test-scaffold.md +0 -38
- package/autopm/.claude/commands/pm/blocked.md +0 -28
- package/autopm/.claude/commands/pm/clean.md +0 -119
- package/autopm/.claude/commands/pm/context-create.md +0 -136
- package/autopm/.claude/commands/pm/context-prime.md +0 -170
- package/autopm/.claude/commands/pm/context-update.md +0 -292
- package/autopm/.claude/commands/pm/context.md +0 -28
- package/autopm/.claude/commands/pm/epic-close.md +0 -86
- package/autopm/.claude/commands/pm/epic-decompose.md +0 -370
- package/autopm/.claude/commands/pm/epic-edit.md +0 -83
- package/autopm/.claude/commands/pm/epic-list.md +0 -30
- package/autopm/.claude/commands/pm/epic-merge.md +0 -222
- package/autopm/.claude/commands/pm/epic-oneshot.md +0 -119
- package/autopm/.claude/commands/pm/epic-refresh.md +0 -119
- package/autopm/.claude/commands/pm/epic-show.md +0 -28
- package/autopm/.claude/commands/pm/epic-split.md +0 -120
- package/autopm/.claude/commands/pm/epic-start.md +0 -195
- package/autopm/.claude/commands/pm/epic-status.md +0 -28
- package/autopm/.claude/commands/pm/epic-sync-modular.md +0 -338
- package/autopm/.claude/commands/pm/epic-sync-original.md +0 -473
- package/autopm/.claude/commands/pm/epic-sync.md +0 -486
- package/autopm/.claude/commands/pm/help.md +0 -28
- package/autopm/.claude/commands/pm/import.md +0 -115
- package/autopm/.claude/commands/pm/in-progress.md +0 -28
- package/autopm/.claude/commands/pm/init.md +0 -28
- package/autopm/.claude/commands/pm/issue-analyze.md +0 -202
- package/autopm/.claude/commands/pm/issue-close.md +0 -119
- package/autopm/.claude/commands/pm/issue-edit.md +0 -93
- package/autopm/.claude/commands/pm/issue-reopen.md +0 -87
- package/autopm/.claude/commands/pm/issue-show.md +0 -41
- package/autopm/.claude/commands/pm/issue-start.md +0 -234
- package/autopm/.claude/commands/pm/issue-status.md +0 -95
- package/autopm/.claude/commands/pm/issue-sync.md +0 -411
- package/autopm/.claude/commands/pm/next.md +0 -28
- package/autopm/.claude/commands/pm/prd-edit.md +0 -82
- package/autopm/.claude/commands/pm/prd-list.md +0 -28
- package/autopm/.claude/commands/pm/prd-new.md +0 -55
- package/autopm/.claude/commands/pm/prd-parse.md +0 -42
- package/autopm/.claude/commands/pm/prd-status.md +0 -28
- package/autopm/.claude/commands/pm/search.md +0 -28
- package/autopm/.claude/commands/pm/standup.md +0 -28
- package/autopm/.claude/commands/pm/status.md +0 -28
- package/autopm/.claude/commands/pm/sync.md +0 -99
- package/autopm/.claude/commands/pm/test-reference-update.md +0 -151
- package/autopm/.claude/commands/pm/validate.md +0 -28
- package/autopm/.claude/commands/pm/what-next.md +0 -28
- package/autopm/.claude/commands/python/api-scaffold.md +0 -50
- package/autopm/.claude/commands/python/docs-query.md +0 -48
- package/autopm/.claude/commands/react/app-scaffold.md +0 -50
- package/autopm/.claude/commands/testing/prime.md +0 -314
- package/autopm/.claude/commands/testing/run.md +0 -125
- package/autopm/.claude/commands/ui/bootstrap-scaffold.md +0 -65
- package/autopm/.claude/commands/ui/tailwind-system.md +0 -64
- package/autopm/.claude/rules/ai-integration-patterns.md +0 -219
- package/autopm/.claude/rules/ci-cd-kubernetes-strategy.md +0 -25
- package/autopm/.claude/rules/database-management-strategy.md +0 -17
- package/autopm/.claude/rules/database-pipeline.md +0 -94
- package/autopm/.claude/rules/devops-troubleshooting-playbook.md +0 -450
- package/autopm/.claude/rules/docker-first-development.md +0 -404
- package/autopm/.claude/rules/infrastructure-pipeline.md +0 -128
- package/autopm/.claude/rules/performance-guidelines.md +0 -403
- package/autopm/.claude/rules/ui-development-standards.md +0 -281
- package/autopm/.claude/rules/ui-framework-rules.md +0 -151
- package/autopm/.claude/rules/ux-design-rules.md +0 -209
- package/autopm/.claude/rules/visual-testing.md +0 -223
- package/autopm/.claude/scripts/azure/README.md +0 -192
- package/autopm/.claude/scripts/azure/active-work.js +0 -524
- package/autopm/.claude/scripts/azure/active-work.sh +0 -20
- package/autopm/.claude/scripts/azure/blocked.js +0 -520
- package/autopm/.claude/scripts/azure/blocked.sh +0 -20
- package/autopm/.claude/scripts/azure/daily.js +0 -533
- package/autopm/.claude/scripts/azure/daily.sh +0 -20
- package/autopm/.claude/scripts/azure/dashboard.js +0 -970
- package/autopm/.claude/scripts/azure/dashboard.sh +0 -20
- package/autopm/.claude/scripts/azure/feature-list.js +0 -254
- package/autopm/.claude/scripts/azure/feature-list.sh +0 -20
- package/autopm/.claude/scripts/azure/feature-show.js +0 -7
- package/autopm/.claude/scripts/azure/feature-show.sh +0 -20
- package/autopm/.claude/scripts/azure/feature-status.js +0 -604
- package/autopm/.claude/scripts/azure/feature-status.sh +0 -20
- package/autopm/.claude/scripts/azure/help.js +0 -342
- package/autopm/.claude/scripts/azure/help.sh +0 -20
- package/autopm/.claude/scripts/azure/next-task.js +0 -508
- package/autopm/.claude/scripts/azure/next-task.sh +0 -20
- package/autopm/.claude/scripts/azure/search.js +0 -469
- package/autopm/.claude/scripts/azure/search.sh +0 -20
- package/autopm/.claude/scripts/azure/setup.js +0 -745
- package/autopm/.claude/scripts/azure/setup.sh +0 -20
- package/autopm/.claude/scripts/azure/sprint-report.js +0 -1012
- package/autopm/.claude/scripts/azure/sprint-report.sh +0 -20
- package/autopm/.claude/scripts/azure/sync.js +0 -563
- package/autopm/.claude/scripts/azure/sync.sh +0 -20
- package/autopm/.claude/scripts/azure/us-list.js +0 -210
- package/autopm/.claude/scripts/azure/us-list.sh +0 -20
- package/autopm/.claude/scripts/azure/us-status.js +0 -238
- package/autopm/.claude/scripts/azure/us-status.sh +0 -20
- package/autopm/.claude/scripts/azure/validate.js +0 -626
- package/autopm/.claude/scripts/azure/validate.sh +0 -20
- package/autopm/.claude/scripts/azure/wrapper-template.sh +0 -20
- package/autopm/.claude/scripts/github/dependency-tracker.js +0 -554
- package/autopm/.claude/scripts/github/dependency-validator.js +0 -545
- package/autopm/.claude/scripts/github/dependency-visualizer.js +0 -477
- package/autopm/.claude/scripts/pm/analytics.js +0 -425
- package/autopm/.claude/scripts/pm/blocked.js +0 -164
- package/autopm/.claude/scripts/pm/blocked.sh +0 -78
- package/autopm/.claude/scripts/pm/clean.js +0 -464
- package/autopm/.claude/scripts/pm/context-create.js +0 -216
- package/autopm/.claude/scripts/pm/context-prime.js +0 -335
- package/autopm/.claude/scripts/pm/context-update.js +0 -344
- package/autopm/.claude/scripts/pm/context.js +0 -338
- package/autopm/.claude/scripts/pm/epic-close.js +0 -347
- package/autopm/.claude/scripts/pm/epic-edit.js +0 -382
- package/autopm/.claude/scripts/pm/epic-list.js +0 -273
- package/autopm/.claude/scripts/pm/epic-list.sh +0 -109
- package/autopm/.claude/scripts/pm/epic-show.js +0 -291
- package/autopm/.claude/scripts/pm/epic-show.sh +0 -105
- package/autopm/.claude/scripts/pm/epic-split.js +0 -522
- package/autopm/.claude/scripts/pm/epic-start/epic-start.js +0 -183
- package/autopm/.claude/scripts/pm/epic-start/epic-start.sh +0 -94
- package/autopm/.claude/scripts/pm/epic-status.js +0 -291
- package/autopm/.claude/scripts/pm/epic-status.sh +0 -104
- package/autopm/.claude/scripts/pm/epic-sync/README.md +0 -208
- package/autopm/.claude/scripts/pm/epic-sync/create-epic-issue.sh +0 -77
- package/autopm/.claude/scripts/pm/epic-sync/create-task-issues.sh +0 -86
- package/autopm/.claude/scripts/pm/epic-sync/update-epic-file.sh +0 -79
- package/autopm/.claude/scripts/pm/epic-sync/update-references.sh +0 -89
- package/autopm/.claude/scripts/pm/epic-sync.sh +0 -137
- package/autopm/.claude/scripts/pm/help.js +0 -92
- package/autopm/.claude/scripts/pm/help.sh +0 -90
- package/autopm/.claude/scripts/pm/in-progress.js +0 -178
- package/autopm/.claude/scripts/pm/in-progress.sh +0 -93
- package/autopm/.claude/scripts/pm/init.js +0 -321
- package/autopm/.claude/scripts/pm/init.sh +0 -178
- package/autopm/.claude/scripts/pm/issue-close.js +0 -232
- package/autopm/.claude/scripts/pm/issue-edit.js +0 -310
- package/autopm/.claude/scripts/pm/issue-show.js +0 -272
- package/autopm/.claude/scripts/pm/issue-start.js +0 -181
- package/autopm/.claude/scripts/pm/issue-sync/format-comment.sh +0 -468
- package/autopm/.claude/scripts/pm/issue-sync/gather-updates.sh +0 -460
- package/autopm/.claude/scripts/pm/issue-sync/post-comment.sh +0 -330
- package/autopm/.claude/scripts/pm/issue-sync/preflight-validation.sh +0 -348
- package/autopm/.claude/scripts/pm/issue-sync/update-frontmatter.sh +0 -387
- package/autopm/.claude/scripts/pm/lib/README.md +0 -85
- package/autopm/.claude/scripts/pm/lib/epic-discovery.js +0 -119
- package/autopm/.claude/scripts/pm/lib/logger.js +0 -78
- package/autopm/.claude/scripts/pm/next.js +0 -189
- package/autopm/.claude/scripts/pm/next.sh +0 -72
- package/autopm/.claude/scripts/pm/optimize.js +0 -407
- package/autopm/.claude/scripts/pm/pr-create.js +0 -337
- package/autopm/.claude/scripts/pm/pr-list.js +0 -257
- package/autopm/.claude/scripts/pm/prd-list.js +0 -242
- package/autopm/.claude/scripts/pm/prd-list.sh +0 -103
- package/autopm/.claude/scripts/pm/prd-new.js +0 -684
- package/autopm/.claude/scripts/pm/prd-parse.js +0 -547
- package/autopm/.claude/scripts/pm/prd-status.js +0 -152
- package/autopm/.claude/scripts/pm/prd-status.sh +0 -63
- package/autopm/.claude/scripts/pm/release.js +0 -460
- package/autopm/.claude/scripts/pm/search.js +0 -192
- package/autopm/.claude/scripts/pm/search.sh +0 -89
- package/autopm/.claude/scripts/pm/standup.js +0 -362
- package/autopm/.claude/scripts/pm/standup.sh +0 -95
- package/autopm/.claude/scripts/pm/status.js +0 -148
- package/autopm/.claude/scripts/pm/status.sh +0 -59
- package/autopm/.claude/scripts/pm/sync-batch.js +0 -337
- package/autopm/.claude/scripts/pm/sync.js +0 -343
- package/autopm/.claude/scripts/pm/template-list.js +0 -141
- package/autopm/.claude/scripts/pm/template-new.js +0 -366
- package/autopm/.claude/scripts/pm/validate.js +0 -274
- package/autopm/.claude/scripts/pm/validate.sh +0 -106
- package/autopm/.claude/scripts/pm/what-next.js +0 -660
- package/bin/node/azure-feature-show.js +0 -7
|
@@ -0,0 +1,1328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginManager - Core plugin management system
|
|
3
|
+
*
|
|
4
|
+
* Based on Context7 research:
|
|
5
|
+
* - Factory pattern from unplugin (/unjs/unplugin)
|
|
6
|
+
* - npm workspaces best practices (/websites/npmjs)
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Plugin discovery and loading
|
|
10
|
+
* - Dependency resolution with peer dependencies
|
|
11
|
+
* - Hook system for extensibility
|
|
12
|
+
* - Metadata-driven agent registration
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const { EventEmitter } = require('events');
|
|
19
|
+
|
|
20
|
+
class PluginManager extends EventEmitter {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super();
|
|
23
|
+
|
|
24
|
+
this.options = {
|
|
25
|
+
pluginDir: options.pluginDir || path.join(process.cwd(), 'node_modules'),
|
|
26
|
+
agentDir: options.agentDir || path.join(process.cwd(), '.claude', 'agents'),
|
|
27
|
+
scopePrefix: options.scopePrefix || '@claudeautopm',
|
|
28
|
+
minCoreVersion: options.minCoreVersion || '2.8.0',
|
|
29
|
+
projectRoot: options.projectRoot || process.cwd(),
|
|
30
|
+
...options
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Plugin registry
|
|
34
|
+
this.plugins = new Map();
|
|
35
|
+
this.agents = new Map();
|
|
36
|
+
this.hooks = new Map();
|
|
37
|
+
|
|
38
|
+
// State
|
|
39
|
+
this.initialized = false;
|
|
40
|
+
this.loadedPlugins = new Set();
|
|
41
|
+
|
|
42
|
+
// Registry file location
|
|
43
|
+
this.registryPath = path.join(
|
|
44
|
+
os.homedir(),
|
|
45
|
+
'.claudeautopm',
|
|
46
|
+
'plugins',
|
|
47
|
+
'registry.json'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Load persistent registry
|
|
51
|
+
this.registry = this.loadRegistry();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load plugin registry from disk
|
|
56
|
+
* Based on npm workspaces pattern - maintains state across sessions
|
|
57
|
+
*/
|
|
58
|
+
loadRegistry() {
|
|
59
|
+
try {
|
|
60
|
+
const registryDir = path.dirname(this.registryPath);
|
|
61
|
+
|
|
62
|
+
// Ensure directory exists
|
|
63
|
+
if (!fs.existsSync(registryDir)) {
|
|
64
|
+
fs.mkdirSync(registryDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(this.registryPath)) {
|
|
68
|
+
return JSON.parse(fs.readFileSync(this.registryPath, 'utf-8'));
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.emit('registry:load-error', { error: error.message });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Default registry structure
|
|
75
|
+
return {
|
|
76
|
+
version: '1.0.0',
|
|
77
|
+
installed: [],
|
|
78
|
+
enabled: [],
|
|
79
|
+
lastUpdate: new Date().toISOString()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Save plugin registry to disk
|
|
85
|
+
*/
|
|
86
|
+
saveRegistry() {
|
|
87
|
+
try {
|
|
88
|
+
this.registry.lastUpdate = new Date().toISOString();
|
|
89
|
+
|
|
90
|
+
const registryDir = path.dirname(this.registryPath);
|
|
91
|
+
if (!fs.existsSync(registryDir)) {
|
|
92
|
+
fs.mkdirSync(registryDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fs.writeFileSync(
|
|
96
|
+
this.registryPath,
|
|
97
|
+
JSON.stringify(this.registry, null, 2),
|
|
98
|
+
'utf-8'
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
this.emit('registry:saved');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
this.emit('registry:save-error', { error: error.message });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Initialize the plugin system
|
|
109
|
+
* Discovers and validates all installed plugins
|
|
110
|
+
*/
|
|
111
|
+
async initialize() {
|
|
112
|
+
if (this.initialized) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.emit('init:start');
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await this.discoverPlugins();
|
|
120
|
+
await this.validatePlugins();
|
|
121
|
+
|
|
122
|
+
this.initialized = true;
|
|
123
|
+
this.emit('init:complete', {
|
|
124
|
+
pluginCount: this.plugins.size,
|
|
125
|
+
agentCount: this.agents.size
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
this.emit('init:error', error);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Discover all installed plugins in node_modules
|
|
135
|
+
* Based on npm workspaces pattern from Context7
|
|
136
|
+
*/
|
|
137
|
+
async discoverPlugins() {
|
|
138
|
+
const pluginPattern = `${this.options.scopePrefix}/plugin-`;
|
|
139
|
+
const nodeModulesPath = this.options.pluginDir;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Check if scoped directory exists
|
|
143
|
+
const scopePath = path.join(nodeModulesPath, this.options.scopePrefix);
|
|
144
|
+
|
|
145
|
+
if (!fs.existsSync(scopePath)) {
|
|
146
|
+
this.emit('discover:no-plugins', { scopePath });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const scopedPackages = fs.readdirSync(scopePath);
|
|
151
|
+
|
|
152
|
+
for (const packageName of scopedPackages) {
|
|
153
|
+
if (!packageName.startsWith('plugin-')) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const pluginPath = path.join(scopePath, packageName);
|
|
158
|
+
const pluginJsonPath = path.join(pluginPath, 'plugin.json');
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(pluginJsonPath)) {
|
|
161
|
+
this.emit('discover:skip', {
|
|
162
|
+
package: packageName,
|
|
163
|
+
reason: 'No plugin.json found'
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const metadata = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf-8'));
|
|
170
|
+
const fullName = `${this.options.scopePrefix}/${packageName}`;
|
|
171
|
+
|
|
172
|
+
this.plugins.set(fullName, {
|
|
173
|
+
name: fullName,
|
|
174
|
+
path: pluginPath,
|
|
175
|
+
metadata,
|
|
176
|
+
loaded: false
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.emit('discover:found', { name: fullName, metadata });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.emit('discover:error', {
|
|
182
|
+
package: packageName,
|
|
183
|
+
error: error.message
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.emit('discover:error', { error: error.message });
|
|
189
|
+
throw new Error(`Plugin discovery failed: ${error.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Validate plugin compatibility with core version
|
|
195
|
+
* Uses peer dependency pattern from Context7 npm docs
|
|
196
|
+
*/
|
|
197
|
+
async validatePlugins() {
|
|
198
|
+
const coreVersion = this.getCoreVersion();
|
|
199
|
+
|
|
200
|
+
for (const [name, plugin] of this.plugins.entries()) {
|
|
201
|
+
const { metadata } = plugin;
|
|
202
|
+
|
|
203
|
+
// Check compatibility
|
|
204
|
+
if (!this.isCompatible(coreVersion, metadata.compatibleWith)) {
|
|
205
|
+
this.emit('validate:incompatible', {
|
|
206
|
+
name,
|
|
207
|
+
required: metadata.compatibleWith,
|
|
208
|
+
current: coreVersion
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
plugin.compatible = false;
|
|
212
|
+
plugin.incompatibilityReason = `Requires core version ${metadata.compatibleWith}, but ${coreVersion} is installed`;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
plugin.compatible = true;
|
|
217
|
+
this.emit('validate:compatible', { name, metadata });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Load a specific plugin and register its agents
|
|
223
|
+
* Implements factory pattern from unplugin Context7 research
|
|
224
|
+
*/
|
|
225
|
+
async loadPlugin(pluginName) {
|
|
226
|
+
const plugin = this.plugins.get(pluginName);
|
|
227
|
+
|
|
228
|
+
if (!plugin) {
|
|
229
|
+
throw new Error(`Plugin not found: ${pluginName}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (plugin.loaded) {
|
|
233
|
+
this.emit('load:already-loaded', { name: pluginName });
|
|
234
|
+
return plugin;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!plugin.compatible) {
|
|
238
|
+
throw new Error(`Plugin incompatible: ${plugin.incompatibilityReason}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.emit('load:start', { name: pluginName });
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Register agents from plugin metadata
|
|
245
|
+
await this.registerAgents(plugin);
|
|
246
|
+
|
|
247
|
+
// Execute plugin hooks if defined
|
|
248
|
+
await this.executePluginHooks(plugin, 'onLoad');
|
|
249
|
+
|
|
250
|
+
plugin.loaded = true;
|
|
251
|
+
this.loadedPlugins.add(pluginName);
|
|
252
|
+
|
|
253
|
+
this.emit('load:complete', {
|
|
254
|
+
name: pluginName,
|
|
255
|
+
agentCount: plugin.metadata.agents.length
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return plugin;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
this.emit('load:error', { name: pluginName, error: error.message });
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Register agents from plugin metadata
|
|
267
|
+
*/
|
|
268
|
+
async registerAgents(plugin) {
|
|
269
|
+
const { metadata, path: pluginPath } = plugin;
|
|
270
|
+
|
|
271
|
+
for (const agentMeta of metadata.agents) {
|
|
272
|
+
const agentId = `${plugin.name}:${agentMeta.name}`;
|
|
273
|
+
const agentFilePath = path.join(pluginPath, agentMeta.file);
|
|
274
|
+
|
|
275
|
+
if (!fs.existsSync(agentFilePath)) {
|
|
276
|
+
this.emit('agent:missing', {
|
|
277
|
+
agentId,
|
|
278
|
+
path: agentFilePath
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.agents.set(agentId, {
|
|
284
|
+
id: agentId,
|
|
285
|
+
name: agentMeta.name,
|
|
286
|
+
plugin: plugin.name,
|
|
287
|
+
description: agentMeta.description,
|
|
288
|
+
tags: agentMeta.tags || [],
|
|
289
|
+
filePath: agentFilePath,
|
|
290
|
+
metadata: agentMeta
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
this.emit('agent:registered', {
|
|
294
|
+
agentId,
|
|
295
|
+
plugin: plugin.name
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Install plugin resources to project directories
|
|
302
|
+
* Supports: agents, commands, rules, hooks, scripts (Schema v2.0)
|
|
303
|
+
*/
|
|
304
|
+
async installPlugin(pluginName) {
|
|
305
|
+
const plugin = this.plugins.get(pluginName);
|
|
306
|
+
|
|
307
|
+
if (!plugin) {
|
|
308
|
+
throw new Error(`Plugin not found: ${pluginName}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Ensure plugin is loaded
|
|
312
|
+
if (!plugin.loaded) {
|
|
313
|
+
await this.loadPlugin(pluginName);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.emit('install:start', { name: pluginName });
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const { metadata, path: pluginPath } = plugin;
|
|
320
|
+
const results = {
|
|
321
|
+
agents: [],
|
|
322
|
+
commands: [],
|
|
323
|
+
rules: [],
|
|
324
|
+
hooks: [],
|
|
325
|
+
scripts: []
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Install agents (existing logic)
|
|
329
|
+
if (metadata.agents && metadata.agents.length > 0) {
|
|
330
|
+
results.agents = await this.installAgents(plugin, pluginPath);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Install commands (Schema v2.0)
|
|
334
|
+
if (metadata.commands && metadata.commands.length > 0) {
|
|
335
|
+
results.commands = await this.installCommands(plugin, pluginPath);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Install rules (Schema v2.0)
|
|
339
|
+
if (metadata.rules && metadata.rules.length > 0) {
|
|
340
|
+
results.rules = await this.installRules(plugin, pluginPath);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Install hooks (Schema v2.0)
|
|
344
|
+
if (metadata.hooks && metadata.hooks.length > 0) {
|
|
345
|
+
results.hooks = await this.installHooks(plugin, pluginPath);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Install scripts (Schema v2.0)
|
|
349
|
+
if (metadata.scripts && metadata.scripts.length > 0) {
|
|
350
|
+
results.scripts = await this.installScripts(plugin, pluginPath);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Update registry
|
|
354
|
+
const shortName = pluginName.replace(`${this.options.scopePrefix}/`, '');
|
|
355
|
+
if (!this.registry.installed.includes(shortName)) {
|
|
356
|
+
this.registry.installed.push(shortName);
|
|
357
|
+
}
|
|
358
|
+
if (!this.registry.enabled.includes(shortName)) {
|
|
359
|
+
this.registry.enabled.push(shortName);
|
|
360
|
+
}
|
|
361
|
+
this.saveRegistry();
|
|
362
|
+
|
|
363
|
+
this.emit('install:complete', {
|
|
364
|
+
name: pluginName,
|
|
365
|
+
results
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
success: true,
|
|
370
|
+
pluginName: shortName,
|
|
371
|
+
displayName: metadata.displayName,
|
|
372
|
+
category: metadata.category,
|
|
373
|
+
agentsInstalled: results.agents.length,
|
|
374
|
+
commandsInstalled: results.commands.length,
|
|
375
|
+
rulesInstalled: results.rules.length,
|
|
376
|
+
hooksInstalled: results.hooks.length,
|
|
377
|
+
scriptsInstalled: results.scripts.length,
|
|
378
|
+
agents: results.agents,
|
|
379
|
+
commands: results.commands,
|
|
380
|
+
rules: results.rules,
|
|
381
|
+
hooks: results.hooks,
|
|
382
|
+
scripts: results.scripts
|
|
383
|
+
};
|
|
384
|
+
} catch (error) {
|
|
385
|
+
this.emit('install:error', {
|
|
386
|
+
name: pluginName,
|
|
387
|
+
error: error.message
|
|
388
|
+
});
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Install agents from plugin
|
|
395
|
+
*/
|
|
396
|
+
async installAgents(plugin, pluginPath) {
|
|
397
|
+
const { metadata } = plugin;
|
|
398
|
+
const category = metadata.category;
|
|
399
|
+
const targetDir = path.join(this.options.agentDir, category);
|
|
400
|
+
|
|
401
|
+
// Create category directory if it doesn't exist
|
|
402
|
+
if (!fs.existsSync(targetDir)) {
|
|
403
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const installed = [];
|
|
407
|
+
|
|
408
|
+
for (const agent of this.agents.values()) {
|
|
409
|
+
if (agent.plugin !== plugin.name) continue;
|
|
410
|
+
|
|
411
|
+
const targetPath = path.join(targetDir, path.basename(agent.filePath));
|
|
412
|
+
|
|
413
|
+
// Skip if already exists
|
|
414
|
+
if (fs.existsSync(targetPath)) {
|
|
415
|
+
this.emit('install:skip', {
|
|
416
|
+
type: 'agent',
|
|
417
|
+
name: agent.name,
|
|
418
|
+
reason: 'Already exists'
|
|
419
|
+
});
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fs.copyFileSync(agent.filePath, targetPath);
|
|
424
|
+
installed.push({
|
|
425
|
+
name: agent.name,
|
|
426
|
+
file: targetPath,
|
|
427
|
+
description: agent.description
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
this.emit('install:agent', {
|
|
431
|
+
agent: agent.name,
|
|
432
|
+
path: targetPath
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return installed;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Install commands from plugin
|
|
441
|
+
*/
|
|
442
|
+
async installCommands(plugin, pluginPath) {
|
|
443
|
+
const { metadata } = plugin;
|
|
444
|
+
const targetDir = path.join(this.options.projectRoot, '.claude', 'commands');
|
|
445
|
+
|
|
446
|
+
// Create commands directory if it doesn't exist
|
|
447
|
+
if (!fs.existsSync(targetDir)) {
|
|
448
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const installed = [];
|
|
452
|
+
|
|
453
|
+
for (const command of metadata.commands) {
|
|
454
|
+
const sourcePath = path.join(pluginPath, command.file);
|
|
455
|
+
const targetPath = path.join(targetDir, path.basename(command.file));
|
|
456
|
+
|
|
457
|
+
if (!fs.existsSync(sourcePath)) {
|
|
458
|
+
this.emit('install:missing', {
|
|
459
|
+
type: 'command',
|
|
460
|
+
name: command.name,
|
|
461
|
+
path: sourcePath
|
|
462
|
+
});
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Skip if already exists
|
|
467
|
+
if (fs.existsSync(targetPath)) {
|
|
468
|
+
this.emit('install:skip', {
|
|
469
|
+
type: 'command',
|
|
470
|
+
name: command.name,
|
|
471
|
+
reason: 'Already exists'
|
|
472
|
+
});
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
477
|
+
installed.push({
|
|
478
|
+
name: command.name,
|
|
479
|
+
file: targetPath,
|
|
480
|
+
description: command.description
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
this.emit('install:command', {
|
|
484
|
+
command: command.name,
|
|
485
|
+
path: targetPath
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return installed;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Install rules from plugin
|
|
494
|
+
*/
|
|
495
|
+
async installRules(plugin, pluginPath) {
|
|
496
|
+
const { metadata } = plugin;
|
|
497
|
+
const targetDir = path.join(this.options.projectRoot, '.claude', 'rules');
|
|
498
|
+
|
|
499
|
+
// Create rules directory if it doesn't exist
|
|
500
|
+
if (!fs.existsSync(targetDir)) {
|
|
501
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const installed = [];
|
|
505
|
+
|
|
506
|
+
for (const rule of metadata.rules) {
|
|
507
|
+
const sourcePath = path.join(pluginPath, rule.file);
|
|
508
|
+
const targetPath = path.join(targetDir, path.basename(rule.file));
|
|
509
|
+
|
|
510
|
+
if (!fs.existsSync(sourcePath)) {
|
|
511
|
+
this.emit('install:missing', {
|
|
512
|
+
type: 'rule',
|
|
513
|
+
name: rule.name,
|
|
514
|
+
path: sourcePath
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Skip if already exists
|
|
520
|
+
if (fs.existsSync(targetPath)) {
|
|
521
|
+
this.emit('install:skip', {
|
|
522
|
+
type: 'rule',
|
|
523
|
+
name: rule.name,
|
|
524
|
+
reason: 'Already exists'
|
|
525
|
+
});
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
530
|
+
installed.push({
|
|
531
|
+
name: rule.name,
|
|
532
|
+
file: targetPath,
|
|
533
|
+
priority: rule.priority,
|
|
534
|
+
description: rule.description
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
this.emit('install:rule', {
|
|
538
|
+
rule: rule.name,
|
|
539
|
+
priority: rule.priority,
|
|
540
|
+
path: targetPath
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return installed;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Install hooks from plugin
|
|
549
|
+
* Supports both single-file and dual-language hooks
|
|
550
|
+
*/
|
|
551
|
+
async installHooks(plugin, pluginPath) {
|
|
552
|
+
const { metadata } = plugin;
|
|
553
|
+
const targetDir = path.join(this.options.projectRoot, '.claude', 'hooks');
|
|
554
|
+
|
|
555
|
+
// Create hooks directory if it doesn't exist
|
|
556
|
+
if (!fs.existsSync(targetDir)) {
|
|
557
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const installed = [];
|
|
561
|
+
|
|
562
|
+
for (const hook of metadata.hooks) {
|
|
563
|
+
const files = hook.dual && hook.files ? hook.files : [hook.file];
|
|
564
|
+
const installedFiles = [];
|
|
565
|
+
|
|
566
|
+
for (const file of files) {
|
|
567
|
+
const sourcePath = path.join(pluginPath, file);
|
|
568
|
+
const targetPath = path.join(targetDir, path.basename(file));
|
|
569
|
+
|
|
570
|
+
if (!fs.existsSync(sourcePath)) {
|
|
571
|
+
this.emit('install:missing', {
|
|
572
|
+
type: 'hook',
|
|
573
|
+
name: hook.name,
|
|
574
|
+
file: file,
|
|
575
|
+
path: sourcePath
|
|
576
|
+
});
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Skip if already exists
|
|
581
|
+
if (fs.existsSync(targetPath)) {
|
|
582
|
+
this.emit('install:skip', {
|
|
583
|
+
type: 'hook',
|
|
584
|
+
name: hook.name,
|
|
585
|
+
file: file,
|
|
586
|
+
reason: 'Already exists'
|
|
587
|
+
});
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
592
|
+
|
|
593
|
+
// Make executable if shell script
|
|
594
|
+
if (file.endsWith('.sh')) {
|
|
595
|
+
fs.chmodSync(targetPath, 0o755);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
installedFiles.push(targetPath);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (installedFiles.length > 0) {
|
|
602
|
+
installed.push({
|
|
603
|
+
name: hook.name,
|
|
604
|
+
type: hook.type,
|
|
605
|
+
files: installedFiles,
|
|
606
|
+
dual: hook.dual || false,
|
|
607
|
+
blocking: hook.blocking,
|
|
608
|
+
description: hook.description
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
this.emit('install:hook', {
|
|
612
|
+
hook: hook.name,
|
|
613
|
+
type: hook.type,
|
|
614
|
+
paths: installedFiles,
|
|
615
|
+
dual: hook.dual
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return installed;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Install scripts from plugin
|
|
625
|
+
* Supports both single scripts and script collections (subdirectories)
|
|
626
|
+
*/
|
|
627
|
+
async installScripts(plugin, pluginPath) {
|
|
628
|
+
const { metadata } = plugin;
|
|
629
|
+
const targetBaseDir = path.join(this.options.projectRoot, 'scripts');
|
|
630
|
+
|
|
631
|
+
// Create scripts directory if it doesn't exist
|
|
632
|
+
if (!fs.existsSync(targetBaseDir)) {
|
|
633
|
+
fs.mkdirSync(targetBaseDir, { recursive: true });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const installed = [];
|
|
637
|
+
|
|
638
|
+
for (const script of metadata.scripts) {
|
|
639
|
+
// Handle script collection (subdirectory)
|
|
640
|
+
if (script.subdirectory && script.files) {
|
|
641
|
+
const installedFiles = [];
|
|
642
|
+
// Remove 'scripts/' prefix from subdirectory if present
|
|
643
|
+
const cleanSubdir = script.subdirectory.replace(/^scripts\//, '');
|
|
644
|
+
const targetDir = path.join(targetBaseDir, cleanSubdir);
|
|
645
|
+
|
|
646
|
+
// Create subdirectory
|
|
647
|
+
if (!fs.existsSync(targetDir)) {
|
|
648
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
for (const file of script.files) {
|
|
652
|
+
const sourcePath = path.join(pluginPath, script.subdirectory, file);
|
|
653
|
+
const targetPath = path.join(targetDir, file);
|
|
654
|
+
|
|
655
|
+
if (!fs.existsSync(sourcePath)) {
|
|
656
|
+
this.emit('install:missing', {
|
|
657
|
+
type: 'script',
|
|
658
|
+
name: script.name,
|
|
659
|
+
file: file,
|
|
660
|
+
path: sourcePath
|
|
661
|
+
});
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Skip if already exists
|
|
666
|
+
if (fs.existsSync(targetPath)) {
|
|
667
|
+
this.emit('install:skip', {
|
|
668
|
+
type: 'script',
|
|
669
|
+
name: script.name,
|
|
670
|
+
file: file,
|
|
671
|
+
reason: 'Already exists'
|
|
672
|
+
});
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
677
|
+
|
|
678
|
+
// Make executable if shell script
|
|
679
|
+
if (file.endsWith('.sh')) {
|
|
680
|
+
fs.chmodSync(targetPath, 0o755);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
installedFiles.push(targetPath);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (installedFiles.length > 0) {
|
|
687
|
+
installed.push({
|
|
688
|
+
name: script.name,
|
|
689
|
+
type: script.type,
|
|
690
|
+
subdirectory: script.subdirectory,
|
|
691
|
+
files: installedFiles,
|
|
692
|
+
exported: script.exported,
|
|
693
|
+
description: script.description
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
this.emit('install:script-collection', {
|
|
697
|
+
script: script.name,
|
|
698
|
+
subdirectory: script.subdirectory,
|
|
699
|
+
paths: installedFiles
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Handle single script
|
|
704
|
+
else if (script.file) {
|
|
705
|
+
const sourcePath = path.join(pluginPath, script.file);
|
|
706
|
+
// Remove 'scripts/' prefix from file path if present
|
|
707
|
+
const cleanFile = script.file.replace(/^scripts\//, '');
|
|
708
|
+
const targetPath = path.join(targetBaseDir, cleanFile);
|
|
709
|
+
|
|
710
|
+
if (!fs.existsSync(sourcePath)) {
|
|
711
|
+
this.emit('install:missing', {
|
|
712
|
+
type: 'script',
|
|
713
|
+
name: script.name,
|
|
714
|
+
path: sourcePath
|
|
715
|
+
});
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Create subdirectories if needed (e.g., lib/)
|
|
720
|
+
const targetDir = path.dirname(targetPath);
|
|
721
|
+
if (!fs.existsSync(targetDir)) {
|
|
722
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Skip if already exists
|
|
726
|
+
if (fs.existsSync(targetPath)) {
|
|
727
|
+
this.emit('install:skip', {
|
|
728
|
+
type: 'script',
|
|
729
|
+
name: script.name,
|
|
730
|
+
reason: 'Already exists'
|
|
731
|
+
});
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
736
|
+
|
|
737
|
+
// Make executable if shell script
|
|
738
|
+
if (script.file.endsWith('.sh')) {
|
|
739
|
+
fs.chmodSync(targetPath, 0o755);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
installed.push({
|
|
743
|
+
name: script.name,
|
|
744
|
+
type: script.type,
|
|
745
|
+
file: targetPath,
|
|
746
|
+
exported: script.exported,
|
|
747
|
+
description: script.description
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
this.emit('install:script', {
|
|
751
|
+
script: script.name,
|
|
752
|
+
path: targetPath
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return installed;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Uninstall plugin - remove all resources and update registry
|
|
762
|
+
* Supports: agents, commands, rules, hooks, scripts (Schema v2.0)
|
|
763
|
+
*/
|
|
764
|
+
async uninstallPlugin(pluginName) {
|
|
765
|
+
const fullName = pluginName.includes('/') ? pluginName : `${this.options.scopePrefix}/${pluginName}`;
|
|
766
|
+
const plugin = this.plugins.get(fullName);
|
|
767
|
+
|
|
768
|
+
if (!plugin) {
|
|
769
|
+
throw new Error(`Plugin not found: ${pluginName}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.emit('uninstall:start', { name: fullName });
|
|
773
|
+
|
|
774
|
+
try {
|
|
775
|
+
const { metadata } = plugin;
|
|
776
|
+
const results = {
|
|
777
|
+
agents: [],
|
|
778
|
+
commands: [],
|
|
779
|
+
rules: [],
|
|
780
|
+
hooks: [],
|
|
781
|
+
scripts: []
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// Remove agents
|
|
785
|
+
if (metadata.agents && metadata.agents.length > 0) {
|
|
786
|
+
const targetDir = path.join(this.options.agentDir, metadata.category);
|
|
787
|
+
for (const agent of metadata.agents) {
|
|
788
|
+
const targetPath = path.join(targetDir, path.basename(agent.file));
|
|
789
|
+
if (fs.existsSync(targetPath)) {
|
|
790
|
+
fs.unlinkSync(targetPath);
|
|
791
|
+
results.agents.push(agent.name);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Remove empty category directory
|
|
796
|
+
if (fs.existsSync(targetDir)) {
|
|
797
|
+
const remaining = fs.readdirSync(targetDir);
|
|
798
|
+
if (remaining.length === 0) {
|
|
799
|
+
fs.rmdirSync(targetDir);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Remove commands
|
|
805
|
+
if (metadata.commands && metadata.commands.length > 0) {
|
|
806
|
+
const targetDir = path.join(this.options.projectRoot, '.claude', 'commands');
|
|
807
|
+
for (const command of metadata.commands) {
|
|
808
|
+
const targetPath = path.join(targetDir, path.basename(command.file));
|
|
809
|
+
if (fs.existsSync(targetPath)) {
|
|
810
|
+
fs.unlinkSync(targetPath);
|
|
811
|
+
results.commands.push(command.name);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Remove rules
|
|
817
|
+
if (metadata.rules && metadata.rules.length > 0) {
|
|
818
|
+
const targetDir = path.join(this.options.projectRoot, '.claude', 'rules');
|
|
819
|
+
for (const rule of metadata.rules) {
|
|
820
|
+
const targetPath = path.join(targetDir, path.basename(rule.file));
|
|
821
|
+
if (fs.existsSync(targetPath)) {
|
|
822
|
+
fs.unlinkSync(targetPath);
|
|
823
|
+
results.rules.push(rule.name);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Remove hooks
|
|
829
|
+
if (metadata.hooks && metadata.hooks.length > 0) {
|
|
830
|
+
const targetDir = path.join(this.options.projectRoot, '.claude', 'hooks');
|
|
831
|
+
for (const hook of metadata.hooks) {
|
|
832
|
+
const files = hook.dual && hook.files ? hook.files : [hook.file];
|
|
833
|
+
for (const file of files) {
|
|
834
|
+
const targetPath = path.join(targetDir, path.basename(file));
|
|
835
|
+
if (fs.existsSync(targetPath)) {
|
|
836
|
+
fs.unlinkSync(targetPath);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
results.hooks.push(hook.name);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Remove scripts
|
|
844
|
+
if (metadata.scripts && metadata.scripts.length > 0) {
|
|
845
|
+
const targetBaseDir = path.join(this.options.projectRoot, 'scripts');
|
|
846
|
+
for (const script of metadata.scripts) {
|
|
847
|
+
if (script.subdirectory && script.files) {
|
|
848
|
+
// Remove 'scripts/' prefix from subdirectory if present
|
|
849
|
+
const cleanSubdir = script.subdirectory.replace(/^scripts\//, '');
|
|
850
|
+
const targetDir = path.join(targetBaseDir, cleanSubdir);
|
|
851
|
+
for (const file of script.files) {
|
|
852
|
+
const targetPath = path.join(targetDir, file);
|
|
853
|
+
if (fs.existsSync(targetPath)) {
|
|
854
|
+
fs.unlinkSync(targetPath);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Remove empty subdirectory
|
|
859
|
+
if (fs.existsSync(targetDir)) {
|
|
860
|
+
const remaining = fs.readdirSync(targetDir);
|
|
861
|
+
if (remaining.length === 0) {
|
|
862
|
+
fs.rmdirSync(targetDir);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
} else if (script.file) {
|
|
866
|
+
// Remove 'scripts/' prefix from file path if present
|
|
867
|
+
const cleanFile = script.file.replace(/^scripts\//, '');
|
|
868
|
+
const targetPath = path.join(targetBaseDir, cleanFile);
|
|
869
|
+
if (fs.existsSync(targetPath)) {
|
|
870
|
+
fs.unlinkSync(targetPath);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
results.scripts.push(script.name);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Update registry
|
|
878
|
+
const shortName = pluginName.replace(`${this.options.scopePrefix}/`, '');
|
|
879
|
+
this.registry.installed = this.registry.installed.filter(p => p !== shortName);
|
|
880
|
+
this.registry.enabled = this.registry.enabled.filter(p => p !== shortName);
|
|
881
|
+
this.saveRegistry();
|
|
882
|
+
|
|
883
|
+
this.emit('uninstall:complete', {
|
|
884
|
+
name: fullName,
|
|
885
|
+
results
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
return {
|
|
889
|
+
success: true,
|
|
890
|
+
pluginName: shortName,
|
|
891
|
+
agentsRemoved: results.agents.length,
|
|
892
|
+
commandsRemoved: results.commands.length,
|
|
893
|
+
rulesRemoved: results.rules.length,
|
|
894
|
+
hooksRemoved: results.hooks.length,
|
|
895
|
+
scriptsRemoved: results.scripts.length,
|
|
896
|
+
agents: results.agents,
|
|
897
|
+
commands: results.commands,
|
|
898
|
+
rules: results.rules,
|
|
899
|
+
hooks: results.hooks,
|
|
900
|
+
scripts: results.scripts
|
|
901
|
+
};
|
|
902
|
+
} catch (error) {
|
|
903
|
+
this.emit('uninstall:error', {
|
|
904
|
+
name: fullName,
|
|
905
|
+
error: error.message
|
|
906
|
+
});
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Get list of installed plugins (from registry)
|
|
913
|
+
*/
|
|
914
|
+
getInstalledPlugins() {
|
|
915
|
+
return this.registry.installed;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Get list of enabled plugins (from registry)
|
|
920
|
+
*/
|
|
921
|
+
getEnabledPlugins() {
|
|
922
|
+
return this.registry.enabled;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Check if plugin is installed
|
|
927
|
+
*/
|
|
928
|
+
isInstalled(pluginName) {
|
|
929
|
+
const shortName = pluginName.replace(`${this.options.scopePrefix}/`, '').replace('plugin-', '');
|
|
930
|
+
return this.registry.installed.some(p => p === shortName || p === `plugin-${shortName}`);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Check if plugin is enabled
|
|
935
|
+
*/
|
|
936
|
+
isEnabled(pluginName) {
|
|
937
|
+
const shortName = pluginName.replace(`${this.options.scopePrefix}/`, '').replace('plugin-', '');
|
|
938
|
+
return this.registry.enabled.some(p => p === shortName || p === `plugin-${shortName}`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Enable plugin
|
|
943
|
+
*/
|
|
944
|
+
enablePlugin(pluginName) {
|
|
945
|
+
const shortName = pluginName.replace(`${this.options.scopePrefix}/`, '');
|
|
946
|
+
|
|
947
|
+
if (!this.isInstalled(shortName)) {
|
|
948
|
+
throw new Error(`Plugin not installed: ${pluginName}`);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (!this.registry.enabled.includes(shortName)) {
|
|
952
|
+
this.registry.enabled.push(shortName);
|
|
953
|
+
this.saveRegistry();
|
|
954
|
+
this.emit('plugin:enabled', { name: shortName });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Disable plugin
|
|
960
|
+
*/
|
|
961
|
+
disablePlugin(pluginName) {
|
|
962
|
+
const shortName = pluginName.replace(`${this.options.scopePrefix}/`, '');
|
|
963
|
+
this.registry.enabled = this.registry.enabled.filter(p => p !== shortName);
|
|
964
|
+
this.saveRegistry();
|
|
965
|
+
this.emit('plugin:disabled', { name: shortName });
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Search plugins by keyword
|
|
970
|
+
*/
|
|
971
|
+
async searchPlugins(keyword) {
|
|
972
|
+
await this.initialize();
|
|
973
|
+
|
|
974
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
975
|
+
const results = [];
|
|
976
|
+
|
|
977
|
+
for (const [name, plugin] of this.plugins.entries()) {
|
|
978
|
+
const { metadata } = plugin;
|
|
979
|
+
|
|
980
|
+
// Search in name
|
|
981
|
+
if (name.toLowerCase().includes(lowerKeyword)) {
|
|
982
|
+
results.push(this.formatPluginForSearch(name, plugin));
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Search in display name
|
|
987
|
+
if (metadata.displayName.toLowerCase().includes(lowerKeyword)) {
|
|
988
|
+
results.push(this.formatPluginForSearch(name, plugin));
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Search in description
|
|
993
|
+
if (metadata.description.toLowerCase().includes(lowerKeyword)) {
|
|
994
|
+
results.push(this.formatPluginForSearch(name, plugin));
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Search in keywords
|
|
999
|
+
if (metadata.keywords && metadata.keywords.some(k => k.toLowerCase().includes(lowerKeyword))) {
|
|
1000
|
+
results.push(this.formatPluginForSearch(name, plugin));
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Search in agent names
|
|
1005
|
+
if (metadata.agents.some(a => a.name.toLowerCase().includes(lowerKeyword))) {
|
|
1006
|
+
results.push(this.formatPluginForSearch(name, plugin));
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return results;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Format plugin for search results
|
|
1016
|
+
*/
|
|
1017
|
+
formatPluginForSearch(name, plugin) {
|
|
1018
|
+
const shortName = name.replace(`${this.options.scopePrefix}/`, '');
|
|
1019
|
+
return {
|
|
1020
|
+
pluginName: shortName,
|
|
1021
|
+
displayName: plugin.metadata.displayName,
|
|
1022
|
+
description: plugin.metadata.description,
|
|
1023
|
+
category: plugin.metadata.category,
|
|
1024
|
+
agents: plugin.metadata.agents,
|
|
1025
|
+
keywords: plugin.metadata.keywords || []
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Get plugin info with status
|
|
1031
|
+
*/
|
|
1032
|
+
async getPluginInfo(pluginName) {
|
|
1033
|
+
const fullName = pluginName.includes('/') ? pluginName : `${this.options.scopePrefix}/${pluginName}`;
|
|
1034
|
+
|
|
1035
|
+
// Ensure plugin is discovered
|
|
1036
|
+
if (!this.initialized) {
|
|
1037
|
+
await this.initialize();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const plugin = this.plugins.get(fullName);
|
|
1041
|
+
|
|
1042
|
+
if (!plugin) {
|
|
1043
|
+
throw new Error(`Plugin not found: ${pluginName}`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const shortName = fullName.replace(`${this.options.scopePrefix}/`, '');
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
...plugin.metadata,
|
|
1050
|
+
pluginName: shortName,
|
|
1051
|
+
path: plugin.path,
|
|
1052
|
+
installed: this.isInstalled(shortName),
|
|
1053
|
+
enabled: this.isEnabled(shortName),
|
|
1054
|
+
compatible: plugin.compatible
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Load plugin metadata (for CLI compatibility)
|
|
1060
|
+
*/
|
|
1061
|
+
async loadPluginMetadata(pluginName) {
|
|
1062
|
+
const fullName = pluginName.includes('/') ? pluginName : `${this.options.scopePrefix}/${pluginName}`;
|
|
1063
|
+
|
|
1064
|
+
if (!this.initialized) {
|
|
1065
|
+
await this.initialize();
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const plugin = this.plugins.get(fullName);
|
|
1069
|
+
|
|
1070
|
+
if (!plugin) {
|
|
1071
|
+
throw new Error(`Plugin not found: ${pluginName}`);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
...plugin.metadata,
|
|
1076
|
+
pluginName: pluginName.replace(`${this.options.scopePrefix}/`, ''),
|
|
1077
|
+
path: plugin.path
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* List all available plugins
|
|
1083
|
+
*/
|
|
1084
|
+
listPlugins(options = {}) {
|
|
1085
|
+
const {
|
|
1086
|
+
loaded = null,
|
|
1087
|
+
compatible = null,
|
|
1088
|
+
category = null
|
|
1089
|
+
} = options;
|
|
1090
|
+
|
|
1091
|
+
let plugins = Array.from(this.plugins.values());
|
|
1092
|
+
|
|
1093
|
+
// Apply filters
|
|
1094
|
+
if (loaded !== null) {
|
|
1095
|
+
plugins = plugins.filter(p => p.loaded === loaded);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (compatible !== null) {
|
|
1099
|
+
plugins = plugins.filter(p => p.compatible === compatible);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (category !== null) {
|
|
1103
|
+
plugins = plugins.filter(p => p.metadata.category === category);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return plugins.map(p => ({
|
|
1107
|
+
name: p.name,
|
|
1108
|
+
displayName: p.metadata.displayName,
|
|
1109
|
+
description: p.metadata.description,
|
|
1110
|
+
category: p.metadata.category,
|
|
1111
|
+
agentCount: p.metadata.agents.length,
|
|
1112
|
+
loaded: p.loaded,
|
|
1113
|
+
compatible: p.compatible,
|
|
1114
|
+
version: p.metadata.version
|
|
1115
|
+
}));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* List all registered agents
|
|
1120
|
+
*/
|
|
1121
|
+
listAgents(options = {}) {
|
|
1122
|
+
const { plugin = null, tags = null } = options;
|
|
1123
|
+
|
|
1124
|
+
let agents = Array.from(this.agents.values());
|
|
1125
|
+
|
|
1126
|
+
// Apply filters
|
|
1127
|
+
if (plugin) {
|
|
1128
|
+
agents = agents.filter(a => a.plugin === plugin);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (tags && tags.length > 0) {
|
|
1132
|
+
agents = agents.filter(a =>
|
|
1133
|
+
tags.some(tag => a.tags.includes(tag))
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return agents.map(a => ({
|
|
1138
|
+
id: a.id,
|
|
1139
|
+
name: a.name,
|
|
1140
|
+
plugin: a.plugin,
|
|
1141
|
+
description: a.description,
|
|
1142
|
+
tags: a.tags,
|
|
1143
|
+
filePath: a.filePath
|
|
1144
|
+
}));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Register a hook for plugin extensibility
|
|
1149
|
+
* Based on unplugin hooks pattern
|
|
1150
|
+
*/
|
|
1151
|
+
registerHook(hookName, handler) {
|
|
1152
|
+
if (!this.hooks.has(hookName)) {
|
|
1153
|
+
this.hooks.set(hookName, []);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
this.hooks.get(hookName).push(handler);
|
|
1157
|
+
|
|
1158
|
+
this.emit('hook:registered', { hookName });
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Execute plugin hooks
|
|
1163
|
+
*/
|
|
1164
|
+
async executePluginHooks(plugin, hookName, data = {}) {
|
|
1165
|
+
const hooks = this.hooks.get(hookName) || [];
|
|
1166
|
+
|
|
1167
|
+
for (const hook of hooks) {
|
|
1168
|
+
try {
|
|
1169
|
+
await hook(plugin, data);
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
this.emit('hook:error', {
|
|
1172
|
+
hookName,
|
|
1173
|
+
plugin: plugin.name,
|
|
1174
|
+
error: error.message
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Get core version from package.json
|
|
1182
|
+
*/
|
|
1183
|
+
getCoreVersion() {
|
|
1184
|
+
try {
|
|
1185
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
1186
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
1187
|
+
return pkg.version || this.options.minCoreVersion;
|
|
1188
|
+
} catch {
|
|
1189
|
+
return this.options.minCoreVersion;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Check version compatibility
|
|
1195
|
+
* Supports semver range syntax (>=, ^, ~)
|
|
1196
|
+
*/
|
|
1197
|
+
isCompatible(currentVersion, requiredVersion) {
|
|
1198
|
+
// Simple implementation - can be enhanced with semver library
|
|
1199
|
+
const cleanRequired = requiredVersion.replace(/[><=^~]/g, '');
|
|
1200
|
+
|
|
1201
|
+
if (requiredVersion.startsWith('>=')) {
|
|
1202
|
+
return this.compareVersions(currentVersion, cleanRequired) >= 0;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Default: exact match or higher
|
|
1206
|
+
return this.compareVersions(currentVersion, cleanRequired) >= 0;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Compare semantic versions
|
|
1211
|
+
* Returns: -1 (less), 0 (equal), 1 (greater)
|
|
1212
|
+
*/
|
|
1213
|
+
compareVersions(v1, v2) {
|
|
1214
|
+
const parts1 = v1.split('.').map(Number);
|
|
1215
|
+
const parts2 = v2.split('.').map(Number);
|
|
1216
|
+
|
|
1217
|
+
for (let i = 0; i < 3; i++) {
|
|
1218
|
+
const p1 = parts1[i] || 0;
|
|
1219
|
+
const p2 = parts2[i] || 0;
|
|
1220
|
+
|
|
1221
|
+
if (p1 > p2) return 1;
|
|
1222
|
+
if (p1 < p2) return -1;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return 0;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Get plugin statistics
|
|
1230
|
+
*/
|
|
1231
|
+
getStats() {
|
|
1232
|
+
return {
|
|
1233
|
+
totalPlugins: this.plugins.size,
|
|
1234
|
+
loadedPlugins: this.loadedPlugins.size,
|
|
1235
|
+
totalAgents: this.agents.size,
|
|
1236
|
+
compatiblePlugins: Array.from(this.plugins.values())
|
|
1237
|
+
.filter(p => p.compatible).length,
|
|
1238
|
+
categories: Array.from(
|
|
1239
|
+
new Set(
|
|
1240
|
+
Array.from(this.plugins.values())
|
|
1241
|
+
.map(p => p.metadata.category)
|
|
1242
|
+
)
|
|
1243
|
+
)
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Find which plugin contains a specific agent
|
|
1249
|
+
* @param {string} agentFileName - Agent file name (e.g., "react-ui-expert.md")
|
|
1250
|
+
* @returns {Object|null} - Plugin info or null if not found
|
|
1251
|
+
*/
|
|
1252
|
+
async findPluginForAgent(agentFileName) {
|
|
1253
|
+
// Ensure plugins are discovered
|
|
1254
|
+
if (!this.initialized) {
|
|
1255
|
+
await this.initialize();
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Normalize agent filename
|
|
1259
|
+
const normalizedName = agentFileName.endsWith('.md') ? agentFileName : `${agentFileName}.md`;
|
|
1260
|
+
|
|
1261
|
+
// Search through all plugins
|
|
1262
|
+
for (const [pluginName, plugin] of this.plugins.entries()) {
|
|
1263
|
+
const { metadata } = plugin;
|
|
1264
|
+
|
|
1265
|
+
// Skip if plugin has no agents
|
|
1266
|
+
if (!metadata.agents || !Array.isArray(metadata.agents)) {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Check if any agent in this plugin matches
|
|
1271
|
+
const matchingAgent = metadata.agents.find(agent => {
|
|
1272
|
+
const agentFile = path.basename(agent.file);
|
|
1273
|
+
return agentFile === normalizedName;
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
if (matchingAgent) {
|
|
1277
|
+
const shortName = pluginName.replace(`${this.options.scopePrefix}/`, '');
|
|
1278
|
+
return {
|
|
1279
|
+
pluginName: shortName,
|
|
1280
|
+
fullPluginName: pluginName,
|
|
1281
|
+
displayName: metadata.displayName,
|
|
1282
|
+
category: metadata.category,
|
|
1283
|
+
agent: matchingAgent,
|
|
1284
|
+
installed: this.isInstalled(shortName),
|
|
1285
|
+
enabled: this.isEnabled(shortName)
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Find plugins for multiple agents
|
|
1295
|
+
* @param {Array<string>} agentFileNames - Array of agent file names
|
|
1296
|
+
* @returns {Object} - Map of agents to plugin info, plus missing agents
|
|
1297
|
+
*/
|
|
1298
|
+
async findPluginsForAgents(agentFileNames) {
|
|
1299
|
+
const result = {
|
|
1300
|
+
found: new Map(), // agentFileName -> plugin info
|
|
1301
|
+
missing: [], // agents not found in any plugin
|
|
1302
|
+
byPlugin: new Map() // pluginName -> [agentFileNames]
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
for (const agentFileName of agentFileNames) {
|
|
1306
|
+
const pluginInfo = await this.findPluginForAgent(agentFileName);
|
|
1307
|
+
|
|
1308
|
+
if (pluginInfo) {
|
|
1309
|
+
result.found.set(agentFileName, pluginInfo);
|
|
1310
|
+
|
|
1311
|
+
// Group by plugin
|
|
1312
|
+
if (!result.byPlugin.has(pluginInfo.pluginName)) {
|
|
1313
|
+
result.byPlugin.set(pluginInfo.pluginName, {
|
|
1314
|
+
...pluginInfo,
|
|
1315
|
+
agents: []
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
result.byPlugin.get(pluginInfo.pluginName).agents.push(agentFileName);
|
|
1319
|
+
} else {
|
|
1320
|
+
result.missing.push(agentFileName);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return result;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
module.exports = PluginManager;
|