@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.
- package/.agent/CheatSheet.md +350 -0
- package/.agent/README.md +76 -0
- package/.agent/agents/README.md +155 -0
- package/.agent/agents/architect.md +185 -0
- package/.agent/agents/backend-specialist.md +276 -0
- package/.agent/agents/build-error-resolver.md +207 -0
- package/.agent/agents/code-reviewer.md +162 -0
- package/.agent/agents/database-architect.md +138 -0
- package/.agent/agents/devops-engineer.md +144 -0
- package/.agent/agents/doc-updater.md +229 -0
- package/.agent/agents/e2e-runner.md +145 -0
- package/.agent/agents/explorer-agent.md +143 -0
- package/.agent/agents/frontend-specialist.md +144 -0
- package/.agent/agents/go-reviewer.md +128 -0
- package/.agent/agents/knowledge-agent.md +197 -0
- package/.agent/agents/mobile-developer.md +150 -0
- package/.agent/agents/performance-optimizer.md +175 -0
- package/.agent/agents/planner.md +133 -0
- package/.agent/agents/pr-reviewer.md +148 -0
- package/.agent/agents/python-reviewer.md +123 -0
- package/.agent/agents/refactor-cleaner.md +201 -0
- package/.agent/agents/reliability-engineer.md +156 -0
- package/.agent/agents/security-reviewer.md +141 -0
- package/.agent/agents/sprint-orchestrator.md +124 -0
- package/.agent/agents/tdd-guide.md +179 -0
- package/.agent/agents/typescript-reviewer.md +110 -0
- package/.agent/checklists/README.md +102 -0
- package/.agent/checklists/pre-commit.md +93 -0
- package/.agent/checklists/session-end.md +99 -0
- package/.agent/checklists/session-start.md +102 -0
- package/.agent/checklists/task-complete.md +81 -0
- package/.agent/commands/README.md +130 -0
- package/.agent/commands/adr.md +29 -0
- package/.agent/commands/ask.md +28 -0
- package/.agent/commands/build.md +30 -0
- package/.agent/commands/changelog.md +40 -0
- package/.agent/commands/checkpoint.md +28 -0
- package/.agent/commands/code-review.md +65 -0
- package/.agent/commands/compact.md +28 -0
- package/.agent/commands/cook.md +30 -0
- package/.agent/commands/db.md +30 -0
- package/.agent/commands/debug.md +31 -0
- package/.agent/commands/deploy.md +37 -0
- package/.agent/commands/design.md +29 -0
- package/.agent/commands/doc.md +30 -0
- package/.agent/commands/eval.md +30 -0
- package/.agent/commands/fix.md +32 -0
- package/.agent/commands/git.md +32 -0
- package/.agent/commands/help.md +273 -0
- package/.agent/commands/implement.md +30 -0
- package/.agent/commands/integrate.md +32 -0
- package/.agent/commands/learn.md +29 -0
- package/.agent/commands/perf.md +31 -0
- package/.agent/commands/plan.md +56 -0
- package/.agent/commands/pr-describe.md +65 -0
- package/.agent/commands/pr-fix.md +45 -0
- package/.agent/commands/pr-merge.md +45 -0
- package/.agent/commands/pr-review.md +50 -0
- package/.agent/commands/pr-split.md +54 -0
- package/.agent/commands/pr-status.md +56 -0
- package/.agent/commands/pr.md +58 -0
- package/.agent/commands/refactor.md +32 -0
- package/.agent/commands/research.md +28 -0
- package/.agent/commands/scout.md +30 -0
- package/.agent/commands/security-scan.md +33 -0
- package/.agent/commands/setup.md +31 -0
- package/.agent/commands/status.md +59 -0
- package/.agent/commands/tdd.md +73 -0
- package/.agent/commands/verify.md +58 -0
- package/.agent/contexts/brainstorm.md +26 -0
- package/.agent/contexts/debug.md +28 -0
- package/.agent/contexts/implement.md +29 -0
- package/.agent/contexts/plan-quality-log.md +30 -0
- package/.agent/contexts/review.md +27 -0
- package/.agent/contexts/ship.md +28 -0
- package/.agent/decisions/001-trust-grade-governance.md +46 -0
- package/.agent/decisions/002-cross-ide-generation.md +15 -0
- package/.agent/engine/identity.json +4 -0
- package/.agent/engine/loading-rules.json +193 -0
- package/.agent/engine/marketplace-index.json +29 -0
- package/.agent/engine/mcp-servers/filesystem.json +9 -0
- package/.agent/engine/mcp-servers/github.json +11 -0
- package/.agent/engine/mcp-servers/postgres.json +11 -0
- package/.agent/engine/mcp-servers/supabase.json +11 -0
- package/.agent/engine/mcp-servers/vercel.json +11 -0
- package/.agent/engine/reliability-config.json +14 -0
- package/.agent/engine/sdlc-map.json +50 -0
- package/.agent/engine/workflow-state.json +167 -0
- package/.agent/hooks/README.md +101 -0
- package/.agent/hooks/hooks.json +104 -0
- package/.agent/hooks/templates/session-end.md +110 -0
- package/.agent/hooks/templates/session-start.md +95 -0
- package/.agent/manifest.json +466 -0
- package/.agent/rules/agent-upgrade-policy.md +56 -0
- package/.agent/rules/architecture.md +111 -0
- package/.agent/rules/coding-style.md +75 -0
- package/.agent/rules/documentation.md +74 -0
- package/.agent/rules/git-workflow.md +140 -0
- package/.agent/rules/quality-gate.md +117 -0
- package/.agent/rules/security.md +67 -0
- package/.agent/rules/sprint-tracking.md +103 -0
- package/.agent/rules/testing.md +80 -0
- package/.agent/rules/workflow-standards.md +30 -0
- package/.agent/rules.md +293 -0
- package/.agent/session-context.md +69 -0
- package/.agent/session-state.json +27 -0
- package/.agent/skills/README.md +135 -0
- package/.agent/skills/api-patterns/SKILL.md +117 -0
- package/.agent/skills/app-builder/SKILL.md +202 -0
- package/.agent/skills/architecture/SKILL.md +101 -0
- package/.agent/skills/behavioral-modes/SKILL.md +295 -0
- package/.agent/skills/brainstorming/SKILL.md +156 -0
- package/.agent/skills/clean-code/SKILL.md +142 -0
- package/.agent/skills/context-budget/SKILL.md +78 -0
- package/.agent/skills/continuous-learning/SKILL.md +145 -0
- package/.agent/skills/database-design/SKILL.md +303 -0
- package/.agent/skills/debugging-strategies/SKILL.md +158 -0
- package/.agent/skills/deployment-procedures/SKILL.md +191 -0
- package/.agent/skills/docker-patterns/SKILL.md +161 -0
- package/.agent/skills/eval-harness/SKILL.md +89 -0
- package/.agent/skills/frontend-patterns/SKILL.md +141 -0
- package/.agent/skills/git-workflow/SKILL.md +159 -0
- package/.agent/skills/i18n-localization/SKILL.md +191 -0
- package/.agent/skills/intelligent-routing/SKILL.md +180 -0
- package/.agent/skills/mcp-integration/SKILL.md +240 -0
- package/.agent/skills/mobile-design/SKILL.md +191 -0
- package/.agent/skills/nodejs-patterns/SKILL.md +164 -0
- package/.agent/skills/parallel-agents/SKILL.md +200 -0
- package/.agent/skills/performance-profiling/SKILL.md +134 -0
- package/.agent/skills/plan-validation/SKILL.md +192 -0
- package/.agent/skills/plan-writing/SKILL.md +183 -0
- package/.agent/skills/plan-writing/domain-enhancers.md +184 -0
- package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
- package/.agent/skills/plan-writing/plan-schema.md +119 -0
- package/.agent/skills/pr-toolkit/SKILL.md +174 -0
- package/.agent/skills/production-readiness/SKILL.md +126 -0
- package/.agent/skills/security-practices/SKILL.md +109 -0
- package/.agent/skills/shell-conventions/SKILL.md +92 -0
- package/.agent/skills/strategic-compact/SKILL.md +62 -0
- package/.agent/skills/testing-patterns/SKILL.md +141 -0
- package/.agent/skills/typescript-expert/SKILL.md +160 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +137 -0
- package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.agent/skills/verification-loop/SKILL.md +89 -0
- package/.agent/skills/webapp-testing/SKILL.md +175 -0
- package/.agent/templates/adr-template.md +32 -0
- package/.agent/templates/bug-report.md +37 -0
- package/.agent/templates/feature-request.md +32 -0
- package/.agent/workflows/README.md +101 -0
- package/.agent/workflows/brainstorm.md +86 -0
- package/.agent/workflows/create.md +85 -0
- package/.agent/workflows/debug.md +83 -0
- package/.agent/workflows/deploy.md +114 -0
- package/.agent/workflows/enhance.md +85 -0
- package/.agent/workflows/orchestrate.md +106 -0
- package/.agent/workflows/plan.md +105 -0
- package/.agent/workflows/pr-fix.md +163 -0
- package/.agent/workflows/pr-merge.md +117 -0
- package/.agent/workflows/pr-review.md +178 -0
- package/.agent/workflows/pr-split.md +118 -0
- package/.agent/workflows/pr.md +184 -0
- package/.agent/workflows/preflight.md +107 -0
- package/.agent/workflows/preview.md +95 -0
- package/.agent/workflows/quality-gate.md +103 -0
- package/.agent/workflows/retrospective.md +100 -0
- package/.agent/workflows/review.md +104 -0
- package/.agent/workflows/status.md +89 -0
- package/.agent/workflows/test.md +98 -0
- package/.agent/workflows/ui-ux-pro-max.md +93 -0
- package/.agent/workflows/upgrade.md +97 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/bin/kit.js +773 -0
- package/lib/agent-registry.js +228 -0
- package/lib/agent-reputation.js +343 -0
- package/lib/circuit-breaker.js +195 -0
- package/lib/cli-commands.js +322 -0
- package/lib/config-validator.js +274 -0
- package/lib/conflict-detector.js +252 -0
- package/lib/constants.js +47 -0
- package/lib/engineering-manager.js +336 -0
- package/lib/error-budget.js +370 -0
- package/lib/hook-system.js +256 -0
- package/lib/ide-generator.js +434 -0
- package/lib/identity.js +240 -0
- package/lib/io.js +146 -0
- package/lib/learning-engine.js +163 -0
- package/lib/loading-engine.js +421 -0
- package/lib/logger.js +118 -0
- package/lib/marketplace.js +321 -0
- package/lib/plugin-system.js +604 -0
- package/lib/plugin-verifier.js +197 -0
- package/lib/rate-limiter.js +113 -0
- package/lib/security-scanner.js +312 -0
- package/lib/self-healing.js +468 -0
- package/lib/session-manager.js +264 -0
- package/lib/skill-sandbox.js +244 -0
- package/lib/task-governance.js +522 -0
- package/lib/task-model.js +332 -0
- package/lib/updater.js +240 -0
- package/lib/verify.js +279 -0
- package/lib/workflow-engine.js +373 -0
- package/lib/workflow-events.js +166 -0
- package/lib/workflow-persistence.js +160 -0
- 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
|
+
};
|