antigravity-ai-kit 2.1.0 → 3.0.1

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