@devran-ai/kit 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/.agent/CheatSheet.md +350 -0
  2. package/.agent/README.md +76 -0
  3. package/.agent/agents/README.md +155 -0
  4. package/.agent/agents/architect.md +185 -0
  5. package/.agent/agents/backend-specialist.md +276 -0
  6. package/.agent/agents/build-error-resolver.md +207 -0
  7. package/.agent/agents/code-reviewer.md +162 -0
  8. package/.agent/agents/database-architect.md +138 -0
  9. package/.agent/agents/devops-engineer.md +144 -0
  10. package/.agent/agents/doc-updater.md +229 -0
  11. package/.agent/agents/e2e-runner.md +145 -0
  12. package/.agent/agents/explorer-agent.md +143 -0
  13. package/.agent/agents/frontend-specialist.md +144 -0
  14. package/.agent/agents/go-reviewer.md +128 -0
  15. package/.agent/agents/knowledge-agent.md +197 -0
  16. package/.agent/agents/mobile-developer.md +150 -0
  17. package/.agent/agents/performance-optimizer.md +175 -0
  18. package/.agent/agents/planner.md +133 -0
  19. package/.agent/agents/pr-reviewer.md +148 -0
  20. package/.agent/agents/python-reviewer.md +123 -0
  21. package/.agent/agents/refactor-cleaner.md +201 -0
  22. package/.agent/agents/reliability-engineer.md +156 -0
  23. package/.agent/agents/security-reviewer.md +141 -0
  24. package/.agent/agents/sprint-orchestrator.md +124 -0
  25. package/.agent/agents/tdd-guide.md +179 -0
  26. package/.agent/agents/typescript-reviewer.md +110 -0
  27. package/.agent/checklists/README.md +102 -0
  28. package/.agent/checklists/pre-commit.md +93 -0
  29. package/.agent/checklists/session-end.md +99 -0
  30. package/.agent/checklists/session-start.md +102 -0
  31. package/.agent/checklists/task-complete.md +81 -0
  32. package/.agent/commands/README.md +130 -0
  33. package/.agent/commands/adr.md +29 -0
  34. package/.agent/commands/ask.md +28 -0
  35. package/.agent/commands/build.md +30 -0
  36. package/.agent/commands/changelog.md +40 -0
  37. package/.agent/commands/checkpoint.md +28 -0
  38. package/.agent/commands/code-review.md +65 -0
  39. package/.agent/commands/compact.md +28 -0
  40. package/.agent/commands/cook.md +30 -0
  41. package/.agent/commands/db.md +30 -0
  42. package/.agent/commands/debug.md +31 -0
  43. package/.agent/commands/deploy.md +37 -0
  44. package/.agent/commands/design.md +29 -0
  45. package/.agent/commands/doc.md +30 -0
  46. package/.agent/commands/eval.md +30 -0
  47. package/.agent/commands/fix.md +32 -0
  48. package/.agent/commands/git.md +32 -0
  49. package/.agent/commands/help.md +273 -0
  50. package/.agent/commands/implement.md +30 -0
  51. package/.agent/commands/integrate.md +32 -0
  52. package/.agent/commands/learn.md +29 -0
  53. package/.agent/commands/perf.md +31 -0
  54. package/.agent/commands/plan.md +56 -0
  55. package/.agent/commands/pr-describe.md +65 -0
  56. package/.agent/commands/pr-fix.md +45 -0
  57. package/.agent/commands/pr-merge.md +45 -0
  58. package/.agent/commands/pr-review.md +50 -0
  59. package/.agent/commands/pr-split.md +54 -0
  60. package/.agent/commands/pr-status.md +56 -0
  61. package/.agent/commands/pr.md +58 -0
  62. package/.agent/commands/refactor.md +32 -0
  63. package/.agent/commands/research.md +28 -0
  64. package/.agent/commands/scout.md +30 -0
  65. package/.agent/commands/security-scan.md +33 -0
  66. package/.agent/commands/setup.md +31 -0
  67. package/.agent/commands/status.md +59 -0
  68. package/.agent/commands/tdd.md +73 -0
  69. package/.agent/commands/verify.md +58 -0
  70. package/.agent/contexts/brainstorm.md +26 -0
  71. package/.agent/contexts/debug.md +28 -0
  72. package/.agent/contexts/implement.md +29 -0
  73. package/.agent/contexts/plan-quality-log.md +30 -0
  74. package/.agent/contexts/review.md +27 -0
  75. package/.agent/contexts/ship.md +28 -0
  76. package/.agent/decisions/001-trust-grade-governance.md +46 -0
  77. package/.agent/decisions/002-cross-ide-generation.md +15 -0
  78. package/.agent/engine/identity.json +4 -0
  79. package/.agent/engine/loading-rules.json +193 -0
  80. package/.agent/engine/marketplace-index.json +29 -0
  81. package/.agent/engine/mcp-servers/filesystem.json +9 -0
  82. package/.agent/engine/mcp-servers/github.json +11 -0
  83. package/.agent/engine/mcp-servers/postgres.json +11 -0
  84. package/.agent/engine/mcp-servers/supabase.json +11 -0
  85. package/.agent/engine/mcp-servers/vercel.json +11 -0
  86. package/.agent/engine/reliability-config.json +14 -0
  87. package/.agent/engine/sdlc-map.json +50 -0
  88. package/.agent/engine/workflow-state.json +167 -0
  89. package/.agent/hooks/README.md +101 -0
  90. package/.agent/hooks/hooks.json +104 -0
  91. package/.agent/hooks/templates/session-end.md +110 -0
  92. package/.agent/hooks/templates/session-start.md +95 -0
  93. package/.agent/manifest.json +466 -0
  94. package/.agent/rules/agent-upgrade-policy.md +56 -0
  95. package/.agent/rules/architecture.md +111 -0
  96. package/.agent/rules/coding-style.md +75 -0
  97. package/.agent/rules/documentation.md +74 -0
  98. package/.agent/rules/git-workflow.md +140 -0
  99. package/.agent/rules/quality-gate.md +117 -0
  100. package/.agent/rules/security.md +67 -0
  101. package/.agent/rules/sprint-tracking.md +103 -0
  102. package/.agent/rules/testing.md +80 -0
  103. package/.agent/rules/workflow-standards.md +30 -0
  104. package/.agent/rules.md +293 -0
  105. package/.agent/session-context.md +69 -0
  106. package/.agent/session-state.json +27 -0
  107. package/.agent/skills/README.md +135 -0
  108. package/.agent/skills/api-patterns/SKILL.md +117 -0
  109. package/.agent/skills/app-builder/SKILL.md +202 -0
  110. package/.agent/skills/architecture/SKILL.md +101 -0
  111. package/.agent/skills/behavioral-modes/SKILL.md +295 -0
  112. package/.agent/skills/brainstorming/SKILL.md +156 -0
  113. package/.agent/skills/clean-code/SKILL.md +142 -0
  114. package/.agent/skills/context-budget/SKILL.md +78 -0
  115. package/.agent/skills/continuous-learning/SKILL.md +145 -0
  116. package/.agent/skills/database-design/SKILL.md +303 -0
  117. package/.agent/skills/debugging-strategies/SKILL.md +158 -0
  118. package/.agent/skills/deployment-procedures/SKILL.md +191 -0
  119. package/.agent/skills/docker-patterns/SKILL.md +161 -0
  120. package/.agent/skills/eval-harness/SKILL.md +89 -0
  121. package/.agent/skills/frontend-patterns/SKILL.md +141 -0
  122. package/.agent/skills/git-workflow/SKILL.md +159 -0
  123. package/.agent/skills/i18n-localization/SKILL.md +191 -0
  124. package/.agent/skills/intelligent-routing/SKILL.md +180 -0
  125. package/.agent/skills/mcp-integration/SKILL.md +240 -0
  126. package/.agent/skills/mobile-design/SKILL.md +191 -0
  127. package/.agent/skills/nodejs-patterns/SKILL.md +164 -0
  128. package/.agent/skills/parallel-agents/SKILL.md +200 -0
  129. package/.agent/skills/performance-profiling/SKILL.md +134 -0
  130. package/.agent/skills/plan-validation/SKILL.md +192 -0
  131. package/.agent/skills/plan-writing/SKILL.md +183 -0
  132. package/.agent/skills/plan-writing/domain-enhancers.md +184 -0
  133. package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
  134. package/.agent/skills/plan-writing/plan-schema.md +119 -0
  135. package/.agent/skills/pr-toolkit/SKILL.md +174 -0
  136. package/.agent/skills/production-readiness/SKILL.md +126 -0
  137. package/.agent/skills/security-practices/SKILL.md +109 -0
  138. package/.agent/skills/shell-conventions/SKILL.md +92 -0
  139. package/.agent/skills/strategic-compact/SKILL.md +62 -0
  140. package/.agent/skills/testing-patterns/SKILL.md +141 -0
  141. package/.agent/skills/typescript-expert/SKILL.md +160 -0
  142. package/.agent/skills/ui-ux-pro-max/SKILL.md +137 -0
  143. package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
  144. package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
  145. package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
  146. package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
  147. package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
  148. package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  149. package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  150. package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  151. package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  152. package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  153. package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  154. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  155. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  156. package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  157. package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  158. package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  159. package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  160. package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  161. package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  162. package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
  163. package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
  164. package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  165. package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  166. package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  167. package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
  168. package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  169. package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
  170. package/.agent/skills/verification-loop/SKILL.md +89 -0
  171. package/.agent/skills/webapp-testing/SKILL.md +175 -0
  172. package/.agent/templates/adr-template.md +32 -0
  173. package/.agent/templates/bug-report.md +37 -0
  174. package/.agent/templates/feature-request.md +32 -0
  175. package/.agent/workflows/README.md +101 -0
  176. package/.agent/workflows/brainstorm.md +86 -0
  177. package/.agent/workflows/create.md +85 -0
  178. package/.agent/workflows/debug.md +83 -0
  179. package/.agent/workflows/deploy.md +114 -0
  180. package/.agent/workflows/enhance.md +85 -0
  181. package/.agent/workflows/orchestrate.md +106 -0
  182. package/.agent/workflows/plan.md +105 -0
  183. package/.agent/workflows/pr-fix.md +163 -0
  184. package/.agent/workflows/pr-merge.md +117 -0
  185. package/.agent/workflows/pr-review.md +178 -0
  186. package/.agent/workflows/pr-split.md +118 -0
  187. package/.agent/workflows/pr.md +184 -0
  188. package/.agent/workflows/preflight.md +107 -0
  189. package/.agent/workflows/preview.md +95 -0
  190. package/.agent/workflows/quality-gate.md +103 -0
  191. package/.agent/workflows/retrospective.md +100 -0
  192. package/.agent/workflows/review.md +104 -0
  193. package/.agent/workflows/status.md +89 -0
  194. package/.agent/workflows/test.md +98 -0
  195. package/.agent/workflows/ui-ux-pro-max.md +93 -0
  196. package/.agent/workflows/upgrade.md +97 -0
  197. package/LICENSE +21 -0
  198. package/README.md +218 -0
  199. package/bin/kit.js +773 -0
  200. package/lib/agent-registry.js +228 -0
  201. package/lib/agent-reputation.js +343 -0
  202. package/lib/circuit-breaker.js +195 -0
  203. package/lib/cli-commands.js +322 -0
  204. package/lib/config-validator.js +274 -0
  205. package/lib/conflict-detector.js +252 -0
  206. package/lib/constants.js +47 -0
  207. package/lib/engineering-manager.js +336 -0
  208. package/lib/error-budget.js +370 -0
  209. package/lib/hook-system.js +256 -0
  210. package/lib/ide-generator.js +434 -0
  211. package/lib/identity.js +240 -0
  212. package/lib/io.js +146 -0
  213. package/lib/learning-engine.js +163 -0
  214. package/lib/loading-engine.js +421 -0
  215. package/lib/logger.js +118 -0
  216. package/lib/marketplace.js +321 -0
  217. package/lib/plugin-system.js +604 -0
  218. package/lib/plugin-verifier.js +197 -0
  219. package/lib/rate-limiter.js +113 -0
  220. package/lib/security-scanner.js +312 -0
  221. package/lib/self-healing.js +468 -0
  222. package/lib/session-manager.js +264 -0
  223. package/lib/skill-sandbox.js +244 -0
  224. package/lib/task-governance.js +522 -0
  225. package/lib/task-model.js +332 -0
  226. package/lib/updater.js +240 -0
  227. package/lib/verify.js +279 -0
  228. package/lib/workflow-engine.js +373 -0
  229. package/lib/workflow-events.js +166 -0
  230. package/lib/workflow-persistence.js +160 -0
  231. package/package.json +57 -0
@@ -0,0 +1,604 @@
1
+ /**
2
+ * Devran AI Kit — Plugin System
3
+ *
4
+ * Full plugin lifecycle: install, remove, validate, and manage
5
+ * plugins that can contribute agents, skills, workflows, hooks,
6
+ * and engine configurations.
7
+ *
8
+ * @module lib/plugin-system
9
+ * @author Emre Dursun
10
+ * @since v3.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const { AGENT_DIR, ENGINE_DIR, PLUGINS_DIR, HOOKS_DIR } = require('./constants');
19
+ const { writeJsonAtomic, safeCopyDirSync } = require('./io');
20
+ const { createLogger } = require('./logger');
21
+ const log = createLogger('plugin-system');
22
+ const PLUGINS_REGISTRY = 'plugins-registry.json';
23
+ const HOOKS_FILE = 'hooks.json';
24
+
25
+ /** Required fields in plugin.json */
26
+ const REQUIRED_PLUGIN_FIELDS = ['name', 'version', 'author', 'description'];
27
+
28
+ /**
29
+ * @typedef {object} PluginManifest
30
+ * @property {string} name - Plugin name
31
+ * @property {string} version - Plugin version
32
+ * @property {string} author - Plugin author
33
+ * @property {string} description - Plugin description
34
+ * @property {string[]} [agents] - Agent .md file names
35
+ * @property {string[]} [skills] - Skill directory names
36
+ * @property {string[]} [workflows] - Workflow .md file names
37
+ * @property {object[]} [hooks] - Lifecycle hook definitions
38
+ * @property {object} [engineConfigs] - Engine config patches
39
+ */
40
+
41
+ /**
42
+ * @typedef {object} PluginRegistryEntry
43
+ * @property {string} name - Plugin name
44
+ * @property {string} version - Plugin version
45
+ * @property {string} author - Plugin author
46
+ * @property {string} installedAt - ISO timestamp
47
+ * @property {string} sourcePath - Original install source
48
+ * @property {object} installed - Installed asset counts
49
+ */
50
+
51
+ /**
52
+ * Resolves the plugins directory path.
53
+ *
54
+ * @param {string} projectRoot - Root directory of the project
55
+ * @returns {string}
56
+ */
57
+ function resolvePluginsDir(projectRoot) {
58
+ return path.join(projectRoot, AGENT_DIR, PLUGINS_DIR);
59
+ }
60
+
61
+ /**
62
+ * Resolves the plugins registry path.
63
+ *
64
+ * @param {string} projectRoot - Root directory of the project
65
+ * @returns {string}
66
+ */
67
+ function resolveRegistryPath(projectRoot) {
68
+ return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, PLUGINS_REGISTRY);
69
+ }
70
+
71
+ /**
72
+ * Loads the plugin registry.
73
+ *
74
+ * @param {string} projectRoot - Root directory of the project
75
+ * @returns {{ plugins: PluginRegistryEntry[] }}
76
+ */
77
+ function loadRegistry(projectRoot) {
78
+ const registryPath = resolveRegistryPath(projectRoot);
79
+
80
+ if (!fs.existsSync(registryPath)) {
81
+ return { plugins: [] };
82
+ }
83
+
84
+ try {
85
+ return JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
86
+ } catch {
87
+ return { plugins: [] };
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Writes the plugin registry atomically.
93
+ *
94
+ * @param {string} projectRoot - Root directory of the project
95
+ * @param {object} registry - Registry data
96
+ * @returns {void}
97
+ */
98
+ function writeRegistry(projectRoot, registry) {
99
+ const registryPath = resolveRegistryPath(projectRoot);
100
+ writeJsonAtomic(registryPath, registry);
101
+ }
102
+
103
+ /**
104
+ * Validates a plugin manifest (plugin.json).
105
+ *
106
+ * @param {string} pluginPath - Path to the plugin directory
107
+ * @returns {{ valid: boolean, errors: string[], manifest?: PluginManifest }}
108
+ */
109
+ function validatePlugin(pluginPath) {
110
+ const manifestPath = path.join(pluginPath, 'plugin.json');
111
+ /** @type {string[]} */
112
+ const errors = [];
113
+
114
+ if (!fs.existsSync(manifestPath)) {
115
+ return { valid: false, errors: ['Missing plugin.json'] };
116
+ }
117
+
118
+ let manifest;
119
+ try {
120
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
121
+ } catch {
122
+ return { valid: false, errors: ['Invalid JSON in plugin.json'] };
123
+ }
124
+
125
+ // Check required fields
126
+ for (const field of REQUIRED_PLUGIN_FIELDS) {
127
+ if (!manifest[field] || typeof manifest[field] !== 'string' || manifest[field].trim().length === 0) {
128
+ errors.push(`Missing or empty required field: ${field}`);
129
+ }
130
+ }
131
+
132
+ // Validate name format (lowercase, hyphens allowed)
133
+ if (manifest.name && !/^[a-z0-9][a-z0-9-]*$/.test(manifest.name)) {
134
+ errors.push('Plugin name must be lowercase alphanumeric with hyphens');
135
+ }
136
+
137
+ // Validate referenced files exist
138
+ for (const agentFile of (manifest.agents || [])) {
139
+ if (!fs.existsSync(path.join(pluginPath, 'agents', agentFile))) {
140
+ errors.push(`Referenced agent file not found: agents/${agentFile}`);
141
+ }
142
+ }
143
+
144
+ for (const skillDir of (manifest.skills || [])) {
145
+ const skillPath = path.join(pluginPath, 'skills', skillDir);
146
+ if (!fs.existsSync(skillPath)) {
147
+ errors.push(`Referenced skill directory not found: skills/${skillDir}`);
148
+ } else {
149
+ const skillMd = path.join(skillPath, 'SKILL.md');
150
+ if (!fs.existsSync(skillMd)) {
151
+ errors.push(`Skill "${skillDir}" missing required SKILL.md`);
152
+ }
153
+ }
154
+ }
155
+
156
+ for (const workflowFile of (manifest.workflows || [])) {
157
+ if (!fs.existsSync(path.join(pluginPath, 'workflows', workflowFile))) {
158
+ errors.push(`Referenced workflow file not found: workflows/${workflowFile}`);
159
+ }
160
+ }
161
+
162
+ // Validate hook event names
163
+ const validEvents = ['session-start', 'session-end', 'pre-commit', 'secret-detection', 'phase-transition', 'sprint-checkpoint', 'plan-complete'];
164
+ for (const hook of (manifest.hooks || [])) {
165
+ if (!hook.event || !validEvents.includes(hook.event)) {
166
+ errors.push(`Invalid hook event: ${hook.event || 'undefined'}. Valid: ${validEvents.join(', ')}`);
167
+ }
168
+ }
169
+
170
+ return { valid: errors.length === 0, errors, manifest };
171
+ }
172
+
173
+ /**
174
+ * Checks for naming collisions with existing assets.
175
+ *
176
+ * @param {PluginManifest} manifest - Plugin manifest
177
+ * @param {string} projectRoot - Root directory of the project
178
+ * @returns {string[]} Collision warnings
179
+ */
180
+ function checkCollisions(manifest, projectRoot) {
181
+ /** @type {string[]} */
182
+ const collisions = [];
183
+
184
+ for (const agentFile of (manifest.agents || [])) {
185
+ const destPath = path.join(projectRoot, AGENT_DIR, 'agents', agentFile);
186
+ if (fs.existsSync(destPath)) {
187
+ collisions.push(`Agent collision: ${agentFile} already exists`);
188
+ }
189
+ }
190
+
191
+ for (const skillDir of (manifest.skills || [])) {
192
+ const destPath = path.join(projectRoot, AGENT_DIR, 'skills', skillDir);
193
+ if (fs.existsSync(destPath)) {
194
+ collisions.push(`Skill collision: ${skillDir} already exists`);
195
+ }
196
+ }
197
+
198
+ for (const workflowFile of (manifest.workflows || [])) {
199
+ const destPath = path.join(projectRoot, AGENT_DIR, 'workflows', workflowFile);
200
+ if (fs.existsSync(destPath)) {
201
+ collisions.push(`Workflow collision: ${workflowFile} already exists`);
202
+ }
203
+ }
204
+
205
+ return collisions;
206
+ }
207
+
208
+ /**
209
+ * Validates file paths in a plugin manifest for path traversal (E-2).
210
+ * Defense-in-depth: prevents malicious plugins from escaping .agent/ sandbox.
211
+ *
212
+ * @param {PluginManifest} manifest - Plugin manifest
213
+ * @returns {string[]} Violations found (empty = safe)
214
+ */
215
+ function validateFilePaths(manifest) {
216
+ /** @type {string[]} */
217
+ const violations = [];
218
+ const pathFields = [
219
+ ...(manifest.agents || []),
220
+ ...(manifest.skills || []),
221
+ ...(manifest.workflows || []),
222
+ ];
223
+
224
+ for (const filePath of pathFields) {
225
+ if (typeof filePath !== 'string') {
226
+ violations.push(`Invalid path type: ${typeof filePath}`);
227
+ continue;
228
+ }
229
+ if (path.isAbsolute(filePath)) {
230
+ violations.push(`Security: Absolute path not allowed: ${filePath}`);
231
+ }
232
+ if (filePath.includes('..')) {
233
+ violations.push(`Security: Path traversal not allowed: ${filePath}`);
234
+ }
235
+ const normalized = path.normalize(filePath);
236
+ if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
237
+ violations.push(`Security: Path escapes sandbox: ${filePath}`);
238
+ }
239
+ }
240
+
241
+ return violations;
242
+ }
243
+
244
+ /**
245
+ * Installs a plugin from a local directory.
246
+ *
247
+ * @param {string} pluginPath - Path to the plugin source directory
248
+ * @param {string} projectRoot - Root directory of the project
249
+ * @returns {{ success: boolean, errors: string[], installed: object }}
250
+ */
251
+ function installPlugin(pluginPath, projectRoot) {
252
+ // Validate plugin
253
+ const validation = validatePlugin(pluginPath);
254
+ if (!validation.valid) {
255
+ return { success: false, errors: validation.errors, installed: {} };
256
+ }
257
+
258
+ const manifest = validation.manifest;
259
+
260
+ // Path traversal defense-in-depth (E-2)
261
+ const pathViolations = validateFilePaths(manifest);
262
+ if (pathViolations.length > 0) {
263
+ return { success: false, errors: pathViolations, installed: {} };
264
+ }
265
+
266
+ // Check if already installed (before collision check)
267
+ const registry = loadRegistry(projectRoot);
268
+ if (registry.plugins.find((p) => p.name === manifest.name)) {
269
+ return { success: false, errors: [`Plugin "${manifest.name}" is already installed`], installed: {} };
270
+ }
271
+
272
+ // Check for collisions
273
+ const collisions = checkCollisions(manifest, projectRoot);
274
+ if (collisions.length > 0) {
275
+ return { success: false, errors: collisions, installed: {} };
276
+ }
277
+
278
+ const installed = { agents: 0, skills: 0, workflows: 0, hooks: 0, configs: 0 };
279
+
280
+ // Install agents
281
+ for (const agentFile of (manifest.agents || [])) {
282
+ const src = path.join(pluginPath, 'agents', agentFile);
283
+ const dest = path.join(projectRoot, AGENT_DIR, 'agents', agentFile);
284
+ copyFileSync(src, dest);
285
+ installed.agents++;
286
+ }
287
+
288
+ // Install skills
289
+ for (const skillDir of (manifest.skills || [])) {
290
+ const src = path.join(pluginPath, 'skills', skillDir);
291
+ const dest = path.join(projectRoot, AGENT_DIR, 'skills', skillDir);
292
+ safeCopyDirSync(src, dest);
293
+ installed.skills++;
294
+ }
295
+
296
+ // Install workflows
297
+ for (const workflowFile of (manifest.workflows || [])) {
298
+ const src = path.join(pluginPath, 'workflows', workflowFile);
299
+ const dest = path.join(projectRoot, AGENT_DIR, 'workflows', workflowFile);
300
+ copyFileSync(src, dest);
301
+ installed.workflows++;
302
+ }
303
+
304
+ // Merge hooks
305
+ if (manifest.hooks && manifest.hooks.length > 0) {
306
+ mergeHooks(manifest.hooks, manifest.name, projectRoot);
307
+ installed.hooks = manifest.hooks.length;
308
+ }
309
+
310
+ // Apply engine configs
311
+ if (manifest.engineConfigs && Object.keys(manifest.engineConfigs).length > 0) {
312
+ applyEngineConfigs(manifest.engineConfigs, manifest.name, projectRoot);
313
+ installed.configs = Object.keys(manifest.engineConfigs).length;
314
+ }
315
+
316
+ // Store plugin copy in plugins dir for uninstall reference
317
+ const pluginStoreDir = path.join(resolvePluginsDir(projectRoot), manifest.name);
318
+ if (!fs.existsSync(pluginStoreDir)) {
319
+ fs.mkdirSync(pluginStoreDir, { recursive: true });
320
+ }
321
+ copyFileSync(
322
+ path.join(pluginPath, 'plugin.json'),
323
+ path.join(pluginStoreDir, 'plugin.json')
324
+ );
325
+
326
+ // Update registry
327
+ registry.plugins.push({
328
+ name: manifest.name,
329
+ version: manifest.version,
330
+ author: manifest.author,
331
+ installedAt: new Date().toISOString(),
332
+ sourcePath: pluginPath,
333
+ installed,
334
+ });
335
+
336
+ writeRegistry(projectRoot, registry);
337
+
338
+ return { success: true, errors: [], installed };
339
+ }
340
+
341
+ /**
342
+ * Removes an installed plugin.
343
+ *
344
+ * @param {string} pluginName - Name of the plugin to remove
345
+ * @param {string} projectRoot - Root directory of the project
346
+ * @returns {{ success: boolean, error?: string, removed: object }}
347
+ */
348
+ function removePlugin(pluginName, projectRoot) {
349
+ const registry = loadRegistry(projectRoot);
350
+ const pluginIndex = registry.plugins.findIndex((p) => p.name === pluginName);
351
+
352
+ if (pluginIndex === -1) {
353
+ return { success: false, error: `Plugin "${pluginName}" is not installed`, removed: {} };
354
+ }
355
+
356
+ // Load plugin manifest from stored copy
357
+ const pluginStoreDir = path.join(resolvePluginsDir(projectRoot), pluginName);
358
+ const manifestPath = path.join(pluginStoreDir, 'plugin.json');
359
+
360
+ if (!fs.existsSync(manifestPath)) {
361
+ // If stored copy is missing, still remove from registry
362
+ registry.plugins.splice(pluginIndex, 1);
363
+ writeRegistry(projectRoot, registry);
364
+ return { success: true, removed: { note: 'Manifest not found — registry cleaned only' } };
365
+ }
366
+
367
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
368
+ const removed = { agents: 0, skills: 0, workflows: 0, hooks: 0 };
369
+
370
+ // Remove agents
371
+ for (const agentFile of (manifest.agents || [])) {
372
+ const dest = path.join(projectRoot, AGENT_DIR, 'agents', agentFile);
373
+ if (fs.existsSync(dest)) {
374
+ fs.unlinkSync(dest);
375
+ removed.agents++;
376
+ }
377
+ }
378
+
379
+ // Remove skills
380
+ for (const skillDir of (manifest.skills || [])) {
381
+ const dest = path.join(projectRoot, AGENT_DIR, 'skills', skillDir);
382
+ if (fs.existsSync(dest)) {
383
+ fs.rmSync(dest, { recursive: true });
384
+ removed.skills++;
385
+ }
386
+ }
387
+
388
+ // Remove workflows
389
+ for (const workflowFile of (manifest.workflows || [])) {
390
+ const dest = path.join(projectRoot, AGENT_DIR, 'workflows', workflowFile);
391
+ if (fs.existsSync(dest)) {
392
+ fs.unlinkSync(dest);
393
+ removed.workflows++;
394
+ }
395
+ }
396
+
397
+ // Unmerge hooks
398
+ if (manifest.hooks && manifest.hooks.length > 0) {
399
+ unmergeHooks(pluginName, projectRoot);
400
+ removed.hooks = manifest.hooks.length;
401
+ }
402
+
403
+ // Clean up stored plugin copy
404
+ if (fs.existsSync(pluginStoreDir)) {
405
+ fs.rmSync(pluginStoreDir, { recursive: true });
406
+ }
407
+
408
+ // Update registry
409
+ registry.plugins.splice(pluginIndex, 1);
410
+ writeRegistry(projectRoot, registry);
411
+
412
+ return { success: true, removed };
413
+ }
414
+
415
+ /**
416
+ * Lists all installed plugins.
417
+ *
418
+ * @param {string} projectRoot - Root directory of the project
419
+ * @returns {PluginRegistryEntry[]}
420
+ */
421
+ function listPlugins(projectRoot) {
422
+ return loadRegistry(projectRoot).plugins;
423
+ }
424
+
425
+ /**
426
+ * Gets hook definitions from a plugin's stored manifest.
427
+ *
428
+ * @param {string} pluginName - Plugin name
429
+ * @param {string} projectRoot - Root directory of the project
430
+ * @returns {object[]}
431
+ */
432
+ function getPluginHooks(pluginName, projectRoot) {
433
+ const manifestPath = path.join(resolvePluginsDir(projectRoot), pluginName, 'plugin.json');
434
+
435
+ if (!fs.existsSync(manifestPath)) {
436
+ return [];
437
+ }
438
+
439
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
440
+ return manifest.hooks || [];
441
+ }
442
+
443
+ /**
444
+ * Merges plugin hooks into the project's hooks.json.
445
+ *
446
+ * @param {object[]} pluginHooks - Hook definitions from plugin
447
+ * @param {string} pluginName - Plugin name for source tagging
448
+ * @param {string} projectRoot - Root directory
449
+ * @returns {void}
450
+ */
451
+ function mergeHooks(pluginHooks, pluginName, projectRoot) {
452
+ const hooksPath = path.join(projectRoot, AGENT_DIR, HOOKS_DIR, HOOKS_FILE);
453
+ let hooksConfig = { hooks: [] };
454
+
455
+ if (fs.existsSync(hooksPath)) {
456
+ hooksConfig = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
457
+ }
458
+
459
+ for (const pluginHook of pluginHooks) {
460
+ const existingHookIndex = hooksConfig.hooks.findIndex((h) => h.event === pluginHook.event);
461
+
462
+ const taggedActions = (pluginHook.actions || []).map((action) => ({
463
+ ...action,
464
+ source: `plugin:${pluginName}`,
465
+ }));
466
+
467
+ if (existingHookIndex !== -1) {
468
+ // Append to existing event
469
+ hooksConfig.hooks[existingHookIndex].actions.push(...taggedActions);
470
+ } else {
471
+ // Create new event entry
472
+ hooksConfig.hooks.push({
473
+ event: pluginHook.event,
474
+ description: pluginHook.description || `Added by plugin: ${pluginName}`,
475
+ actions: taggedActions,
476
+ });
477
+ }
478
+ }
479
+
480
+ writeJsonAtomic(hooksPath, hooksConfig);
481
+ }
482
+
483
+ /**
484
+ * Removes plugin-contributed hooks from hooks.json.
485
+ *
486
+ * @param {string} pluginName - Plugin name to filter out
487
+ * @param {string} projectRoot - Root directory
488
+ * @returns {void}
489
+ */
490
+ function unmergeHooks(pluginName, projectRoot) {
491
+ const hooksPath = path.join(projectRoot, AGENT_DIR, HOOKS_DIR, HOOKS_FILE);
492
+
493
+ if (!fs.existsSync(hooksPath)) {
494
+ return;
495
+ }
496
+
497
+ const hooksConfig = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
498
+ const sourceTag = `plugin:${pluginName}`;
499
+
500
+ for (const hook of hooksConfig.hooks) {
501
+ hook.actions = (hook.actions || []).filter((a) => a.source !== sourceTag);
502
+ }
503
+
504
+ // Remove hooks with no actions remaining
505
+ hooksConfig.hooks = hooksConfig.hooks.filter((h) => h.actions.length > 0);
506
+
507
+ writeJsonAtomic(hooksPath, hooksConfig);
508
+ }
509
+
510
+ /**
511
+ * Recursively sanitizes a value by stripping prototype-polluting keys
512
+ * at all nesting levels. Returns a clean copy without mutating the input.
513
+ *
514
+ * @param {*} val - Value to sanitize
515
+ * @returns {*} Sanitized copy
516
+ */
517
+ function sanitizeValue(val) {
518
+ if (val === null || typeof val !== 'object') {
519
+ return val;
520
+ }
521
+ if (Array.isArray(val)) {
522
+ return val.map(sanitizeValue);
523
+ }
524
+ const clean = {};
525
+ for (const [k, v] of Object.entries(val)) {
526
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') {
527
+ continue;
528
+ }
529
+ clean[k] = sanitizeValue(v);
530
+ }
531
+ return clean;
532
+ }
533
+
534
+ /**
535
+ * Applies engine config patches from a plugin.
536
+ *
537
+ * @param {object} configs - Key-value config patches
538
+ * @param {string} pluginName - Plugin name for tracking
539
+ * @param {string} projectRoot - Root directory
540
+ * @returns {void}
541
+ */
542
+ function applyEngineConfigs(configs, pluginName, projectRoot) {
543
+ for (const [configFile, patches] of Object.entries(configs)) {
544
+ // Reject path traversal in config file names
545
+ if (configFile.includes('/') || configFile.includes('\\') || configFile.includes('..')) {
546
+ continue;
547
+ }
548
+
549
+ const configPath = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, configFile);
550
+
551
+ if (!fs.existsSync(configPath)) {
552
+ continue;
553
+ }
554
+
555
+ let config;
556
+ try {
557
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
558
+ } catch {
559
+ continue;
560
+ }
561
+
562
+ // Deep merge patches with recursive prototype pollution guard (H-5)
563
+ for (const [key, value] of Object.entries(patches)) {
564
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
565
+ continue;
566
+ }
567
+ config[key] = sanitizeValue(value);
568
+ }
569
+
570
+ // Track which plugin patched this config (immutable construction)
571
+ const pluginPatches = {
572
+ ...(config._pluginPatches || {}),
573
+ [pluginName]: Object.keys(patches),
574
+ };
575
+ config._pluginPatches = pluginPatches;
576
+
577
+ writeJsonAtomic(configPath, config);
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Copies a file with directory creation.
583
+ *
584
+ * @param {string} src - Source path
585
+ * @param {string} dest - Destination path
586
+ * @returns {void}
587
+ */
588
+ function copyFileSync(src, dest) {
589
+ const dir = path.dirname(dest);
590
+ if (!fs.existsSync(dir)) {
591
+ fs.mkdirSync(dir, { recursive: true });
592
+ }
593
+ fs.copyFileSync(src, dest);
594
+ }
595
+
596
+
597
+
598
+ module.exports = {
599
+ validatePlugin,
600
+ installPlugin,
601
+ removePlugin,
602
+ listPlugins,
603
+ getPluginHooks,
604
+ };