neuro-cli 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/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Plugin Bundle System
|
|
3
|
+
// Packages Skills + Hooks + MCP servers into installable units
|
|
4
|
+
// ============================================================
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, copyFileSync, statSync, } from 'fs';
|
|
6
|
+
import { join, basename, dirname, resolve, extname } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { pipeline as pipelineCallback } from 'stream';
|
|
11
|
+
import { promisify } from 'util';
|
|
12
|
+
const pipelineAsync = promisify(pipelineCallback);
|
|
13
|
+
// ---- Constants ----
|
|
14
|
+
const BUNDLES_DIR = join(homedir(), '.neuro', 'bundles');
|
|
15
|
+
const REGISTRY_FILE = join(homedir(), '.neuro', 'bundle-registry.json');
|
|
16
|
+
const MANIFEST_FILE = 'bundle.json';
|
|
17
|
+
const SKILLS_DIR = 'skills';
|
|
18
|
+
const HOOKS_DIR = 'hooks';
|
|
19
|
+
const MCP_DIR = 'mcp';
|
|
20
|
+
const TOOLS_DIR = 'tools';
|
|
21
|
+
// ---- Helper Functions ----
|
|
22
|
+
function ensureDir(dir) {
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function computeChecksum(filePath) {
|
|
28
|
+
const content = readFileSync(filePath);
|
|
29
|
+
return createHash('sha256').update(content).digest('hex');
|
|
30
|
+
}
|
|
31
|
+
function computeDirectoryChecksum(dirPath) {
|
|
32
|
+
const hash = createHash('sha256');
|
|
33
|
+
function walk(dir) {
|
|
34
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
// Sort for deterministic ordering
|
|
36
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const fullPath = join(dir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
hash.update(`dir:${entry.name}`);
|
|
41
|
+
walk(fullPath);
|
|
42
|
+
}
|
|
43
|
+
else if (entry.isFile()) {
|
|
44
|
+
const content = readFileSync(fullPath);
|
|
45
|
+
hash.update(`file:${entry.name}:${content.length}:`);
|
|
46
|
+
hash.update(content);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
walk(dirPath);
|
|
51
|
+
return hash.digest('hex');
|
|
52
|
+
}
|
|
53
|
+
function readJSONFile(filePath) {
|
|
54
|
+
if (!existsSync(filePath))
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function writeJSONFile(filePath, data) {
|
|
64
|
+
ensureDir(dirname(filePath));
|
|
65
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
function copyRecursive(src, dest) {
|
|
68
|
+
ensureDir(dest);
|
|
69
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const srcPath = join(src, entry.name);
|
|
72
|
+
const destPath = join(dest, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
copyRecursive(srcPath, destPath);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
copyFileSync(srcPath, destPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function execCommand(command, options) {
|
|
82
|
+
try {
|
|
83
|
+
const stdout = execSync(command, {
|
|
84
|
+
encoding: 'utf-8',
|
|
85
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
86
|
+
timeout: options?.timeout ?? 30000,
|
|
87
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
88
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
89
|
+
});
|
|
90
|
+
return { stdout: stdout.trim(), stderr: '', exitCode: 0 };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
stdout: error.stdout?.toString().trim() ?? '',
|
|
95
|
+
stderr: error.stderr?.toString().trim() ?? '',
|
|
96
|
+
exitCode: error.status ?? 1,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ---- PluginBundleManager Class ----
|
|
101
|
+
export class PluginBundleManager {
|
|
102
|
+
bundlesDir;
|
|
103
|
+
registryFile;
|
|
104
|
+
installedBundles = new Map();
|
|
105
|
+
constructor(bundlesDir) {
|
|
106
|
+
this.bundlesDir = bundlesDir ?? BUNDLES_DIR;
|
|
107
|
+
this.registryFile = REGISTRY_FILE;
|
|
108
|
+
ensureDir(this.bundlesDir);
|
|
109
|
+
this.loadInstalledBundles();
|
|
110
|
+
}
|
|
111
|
+
// ---- Installation ----
|
|
112
|
+
/**
|
|
113
|
+
* Install a plugin bundle from URL, path, or registry
|
|
114
|
+
*/
|
|
115
|
+
async installBundle(source) {
|
|
116
|
+
const warnings = [];
|
|
117
|
+
// Determine source type
|
|
118
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
119
|
+
return this.installFromURL(source);
|
|
120
|
+
}
|
|
121
|
+
if (source.startsWith('registry:') || !source.includes('/') && !source.includes('\\')) {
|
|
122
|
+
const bundleId = source.replace(/^registry:/, '');
|
|
123
|
+
return this.installFromRegistry(bundleId);
|
|
124
|
+
}
|
|
125
|
+
// Local path
|
|
126
|
+
return this.installFromPath(source, warnings);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Uninstall a plugin bundle
|
|
130
|
+
*/
|
|
131
|
+
uninstallBundle(bundleId) {
|
|
132
|
+
const bundle = this.installedBundles.get(bundleId);
|
|
133
|
+
if (!bundle) {
|
|
134
|
+
return { success: false, error: `Bundle "${bundleId}" is not installed` };
|
|
135
|
+
}
|
|
136
|
+
// Remove bundle directory
|
|
137
|
+
try {
|
|
138
|
+
if (existsSync(bundle.installPath)) {
|
|
139
|
+
rmSync(bundle.installPath, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
return { success: false, error: `Failed to remove bundle files: ${error instanceof Error ? error.message : String(error)}` };
|
|
144
|
+
}
|
|
145
|
+
// Remove from installed bundles
|
|
146
|
+
this.installedBundles.delete(bundleId);
|
|
147
|
+
this.saveInstalledBundles();
|
|
148
|
+
// Run uninstall hooks if present
|
|
149
|
+
this.runLifecycleHook(bundle, 'uninstall');
|
|
150
|
+
return { success: true };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* List all installed bundles
|
|
154
|
+
*/
|
|
155
|
+
listBundles() {
|
|
156
|
+
return Array.from(this.installedBundles.values()).map(bundle => ({
|
|
157
|
+
id: bundle.manifest.id,
|
|
158
|
+
name: bundle.manifest.name,
|
|
159
|
+
version: bundle.manifest.version,
|
|
160
|
+
description: bundle.manifest.description,
|
|
161
|
+
author: bundle.manifest.author,
|
|
162
|
+
enabled: bundle.enabled,
|
|
163
|
+
source: bundle.source,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get bundle details
|
|
168
|
+
*/
|
|
169
|
+
getBundleInfo(bundleId) {
|
|
170
|
+
return this.installedBundles.get(bundleId) ?? null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Update a bundle to the latest version
|
|
174
|
+
*/
|
|
175
|
+
async updateBundle(bundleId) {
|
|
176
|
+
const bundle = this.installedBundles.get(bundleId);
|
|
177
|
+
if (!bundle) {
|
|
178
|
+
return { success: false, error: `Bundle "${bundleId}" is not installed` };
|
|
179
|
+
}
|
|
180
|
+
const previousVersion = bundle.manifest.version;
|
|
181
|
+
// Re-install from the original source
|
|
182
|
+
const installResult = await this.installBundle(bundle.source);
|
|
183
|
+
if (!installResult.success) {
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
previousVersion,
|
|
187
|
+
error: installResult.error ?? 'Update failed',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const newBundle = this.installedBundles.get(bundleId);
|
|
191
|
+
const newVersion = newBundle?.manifest.version ?? previousVersion;
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
previousVersion,
|
|
195
|
+
newVersion,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Validate a bundle before install
|
|
200
|
+
*/
|
|
201
|
+
validateBundle(bundlePath) {
|
|
202
|
+
const errors = [];
|
|
203
|
+
const warnings = [];
|
|
204
|
+
// Check path exists
|
|
205
|
+
if (!existsSync(bundlePath)) {
|
|
206
|
+
errors.push({
|
|
207
|
+
code: 'PATH_NOT_FOUND',
|
|
208
|
+
message: `Bundle path does not exist: ${bundlePath}`,
|
|
209
|
+
});
|
|
210
|
+
return { valid: false, errors, warnings };
|
|
211
|
+
}
|
|
212
|
+
// Check manifest exists
|
|
213
|
+
const manifestPath = join(bundlePath, MANIFEST_FILE);
|
|
214
|
+
if (!existsSync(manifestPath)) {
|
|
215
|
+
errors.push({
|
|
216
|
+
code: 'MANIFEST_MISSING',
|
|
217
|
+
message: `No ${MANIFEST_FILE} found in bundle`,
|
|
218
|
+
path: MANIFEST_FILE,
|
|
219
|
+
});
|
|
220
|
+
return { valid: false, errors, warnings };
|
|
221
|
+
}
|
|
222
|
+
// Parse manifest
|
|
223
|
+
let manifest;
|
|
224
|
+
try {
|
|
225
|
+
const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
226
|
+
manifest = raw;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
errors.push({
|
|
230
|
+
code: 'MANIFEST_INVALID_JSON',
|
|
231
|
+
message: `Failed to parse ${MANIFEST_FILE}: ${error instanceof Error ? error.message : String(error)}`,
|
|
232
|
+
path: MANIFEST_FILE,
|
|
233
|
+
});
|
|
234
|
+
return { valid: false, errors, warnings };
|
|
235
|
+
}
|
|
236
|
+
// Validate manifest fields
|
|
237
|
+
if (!manifest.id || typeof manifest.id !== 'string') {
|
|
238
|
+
errors.push({
|
|
239
|
+
code: 'MANIFEST_MISSING_ID',
|
|
240
|
+
message: 'Bundle manifest must have a valid "id" field',
|
|
241
|
+
path: MANIFEST_FILE,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else if (!/^[a-zA-Z0-9_-]+$/.test(manifest.id)) {
|
|
245
|
+
errors.push({
|
|
246
|
+
code: 'MANIFEST_INVALID_ID',
|
|
247
|
+
message: 'Bundle ID must contain only alphanumeric characters, hyphens, and underscores',
|
|
248
|
+
path: MANIFEST_FILE,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (!manifest.name || typeof manifest.name !== 'string') {
|
|
252
|
+
errors.push({
|
|
253
|
+
code: 'MANIFEST_MISSING_NAME',
|
|
254
|
+
message: 'Bundle manifest must have a "name" field',
|
|
255
|
+
path: MANIFEST_FILE,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (!manifest.version || typeof manifest.version !== 'string') {
|
|
259
|
+
errors.push({
|
|
260
|
+
code: 'MANIFEST_MISSING_VERSION',
|
|
261
|
+
message: 'Bundle manifest must have a "version" field',
|
|
262
|
+
path: MANIFEST_FILE,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else if (!/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
|
266
|
+
warnings.push({
|
|
267
|
+
code: 'MANIFEST_INVALID_SEMVER',
|
|
268
|
+
message: `Version "${manifest.version}" is not valid semver`,
|
|
269
|
+
path: MANIFEST_FILE,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (!manifest.description || typeof manifest.description !== 'string') {
|
|
273
|
+
errors.push({
|
|
274
|
+
code: 'MANIFEST_MISSING_DESCRIPTION',
|
|
275
|
+
message: 'Bundle manifest must have a "description" field',
|
|
276
|
+
path: MANIFEST_FILE,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (!manifest.author || typeof manifest.author !== 'string') {
|
|
280
|
+
errors.push({
|
|
281
|
+
code: 'MANIFEST_MISSING_AUTHOR',
|
|
282
|
+
message: 'Bundle manifest must have an "author" field',
|
|
283
|
+
path: MANIFEST_FILE,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (!manifest.compatibility?.neuroVersion) {
|
|
287
|
+
warnings.push({
|
|
288
|
+
code: 'MANIFEST_MISSING_COMPATIBILITY',
|
|
289
|
+
message: 'No "compatibility.neuroVersion" specified - bundle may not work with all NeuroCLI versions',
|
|
290
|
+
path: MANIFEST_FILE,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// Validate referenced files exist
|
|
294
|
+
this.validateBundleReferences(bundlePath, manifest.skills, SKILLS_DIR, errors, warnings);
|
|
295
|
+
this.validateBundleReferences(bundlePath, manifest.hooks, HOOKS_DIR, errors, warnings);
|
|
296
|
+
this.validateBundleReferences(bundlePath, manifest.mcpServers, MCP_DIR, errors, warnings);
|
|
297
|
+
this.validateBundleReferences(bundlePath, manifest.tools, TOOLS_DIR, errors, warnings);
|
|
298
|
+
// Validate permissions
|
|
299
|
+
const validPermissions = [
|
|
300
|
+
'file:read', 'file:write', 'file:delete',
|
|
301
|
+
'bash:execute', 'network:access',
|
|
302
|
+
'env:read', 'env:write',
|
|
303
|
+
'git:access', 'mcp:connect',
|
|
304
|
+
];
|
|
305
|
+
if (manifest.permissions) {
|
|
306
|
+
for (const perm of manifest.permissions) {
|
|
307
|
+
if (!validPermissions.includes(perm)) {
|
|
308
|
+
warnings.push({
|
|
309
|
+
code: 'UNKNOWN_PERMISSION',
|
|
310
|
+
message: `Unknown permission: "${perm}"`,
|
|
311
|
+
path: MANIFEST_FILE,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Check for dangerous permissions
|
|
317
|
+
if (manifest.permissions?.includes('bash:execute')) {
|
|
318
|
+
warnings.push({
|
|
319
|
+
code: 'DANGEROUS_PERMISSION',
|
|
320
|
+
message: 'Bundle requests "bash:execute" permission - this allows arbitrary command execution',
|
|
321
|
+
path: MANIFEST_FILE,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
if (manifest.permissions?.includes('network:access')) {
|
|
325
|
+
warnings.push({
|
|
326
|
+
code: 'SENSITIVE_PERMISSION',
|
|
327
|
+
message: 'Bundle requests "network:access" permission - this allows network connections',
|
|
328
|
+
path: MANIFEST_FILE,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// Check for hooks directory and executable scripts
|
|
332
|
+
const hooksDir = join(bundlePath, HOOKS_DIR);
|
|
333
|
+
if (existsSync(hooksDir)) {
|
|
334
|
+
const hookFiles = readdirSync(hooksDir);
|
|
335
|
+
for (const hookFile of hookFiles) {
|
|
336
|
+
const hookPath = join(hooksDir, hookFile);
|
|
337
|
+
const stat = statSync(hookPath);
|
|
338
|
+
if (stat.isFile() && !(stat.mode & 0o111)) {
|
|
339
|
+
warnings.push({
|
|
340
|
+
code: 'HOOK_NOT_EXECUTABLE',
|
|
341
|
+
message: `Hook script "${hookFile}" is not executable`,
|
|
342
|
+
path: join(HOOKS_DIR, hookFile),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Check MCP configs are valid JSON
|
|
348
|
+
const mcpDir = join(bundlePath, MCP_DIR);
|
|
349
|
+
if (existsSync(mcpDir)) {
|
|
350
|
+
const mcpFiles = readdirSync(mcpDir).filter(f => f.endsWith('.json'));
|
|
351
|
+
for (const mcpFile of mcpFiles) {
|
|
352
|
+
const mcpPath = join(mcpDir, mcpFile);
|
|
353
|
+
try {
|
|
354
|
+
JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
errors.push({
|
|
358
|
+
code: 'INVALID_MCP_CONFIG',
|
|
359
|
+
message: `MCP config "${mcpFile}" is not valid JSON`,
|
|
360
|
+
path: join(MCP_DIR, mcpFile),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
valid: errors.length === 0,
|
|
367
|
+
errors,
|
|
368
|
+
warnings,
|
|
369
|
+
manifest: errors.length === 0 ? manifest : undefined,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Create a new plugin bundle
|
|
374
|
+
*/
|
|
375
|
+
createBundle(options) {
|
|
376
|
+
const { outputDir, manifest } = options;
|
|
377
|
+
const bundleDir = join(outputDir, manifest.id);
|
|
378
|
+
try {
|
|
379
|
+
// Create bundle structure
|
|
380
|
+
ensureDir(bundleDir);
|
|
381
|
+
ensureDir(join(bundleDir, SKILLS_DIR));
|
|
382
|
+
ensureDir(join(bundleDir, HOOKS_DIR));
|
|
383
|
+
ensureDir(join(bundleDir, MCP_DIR));
|
|
384
|
+
ensureDir(join(bundleDir, TOOLS_DIR));
|
|
385
|
+
// Write manifest
|
|
386
|
+
const fullManifest = {
|
|
387
|
+
...manifest,
|
|
388
|
+
createdAt: new Date().toISOString(),
|
|
389
|
+
updatedAt: new Date().toISOString(),
|
|
390
|
+
};
|
|
391
|
+
writeJSONFile(join(bundleDir, MANIFEST_FILE), fullManifest);
|
|
392
|
+
// Write skills
|
|
393
|
+
if (options.skills) {
|
|
394
|
+
for (const [skillName, content] of Object.entries(options.skills)) {
|
|
395
|
+
const skillDir = join(bundleDir, SKILLS_DIR, skillName);
|
|
396
|
+
ensureDir(skillDir);
|
|
397
|
+
writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Write hooks
|
|
401
|
+
if (options.hooks) {
|
|
402
|
+
for (const [filename, content] of Object.entries(options.hooks)) {
|
|
403
|
+
const hookPath = join(bundleDir, HOOKS_DIR, filename);
|
|
404
|
+
writeFileSync(hookPath, content, 'utf-8');
|
|
405
|
+
// Make executable
|
|
406
|
+
try {
|
|
407
|
+
const stat = statSync(hookPath);
|
|
408
|
+
chmodSync(hookPath, stat.mode | 0o111);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Best-effort chmod
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Write MCP server configs
|
|
416
|
+
if (options.mcpServers) {
|
|
417
|
+
for (const [filename, config] of Object.entries(options.mcpServers)) {
|
|
418
|
+
writeJSONFile(join(bundleDir, MCP_DIR, filename), config);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Write tools
|
|
422
|
+
if (options.tools) {
|
|
423
|
+
for (const [filename, content] of Object.entries(options.tools)) {
|
|
424
|
+
writeFileSync(join(bundleDir, TOOLS_DIR, filename), content, 'utf-8');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { success: true, path: bundleDir };
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
return { success: false, error: `Failed to create bundle: ${error instanceof Error ? error.message : String(error)}` };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Publish a bundle to the registry
|
|
435
|
+
*/
|
|
436
|
+
publishBundle(bundlePath) {
|
|
437
|
+
// Validate first
|
|
438
|
+
const validation = this.validateBundle(bundlePath);
|
|
439
|
+
if (!validation.valid) {
|
|
440
|
+
return {
|
|
441
|
+
success: false,
|
|
442
|
+
error: `Bundle validation failed: ${validation.errors.map(e => e.message).join('; ')}`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const manifest = validation.manifest;
|
|
446
|
+
// Add to local registry
|
|
447
|
+
const registry = this.loadRegistry();
|
|
448
|
+
const checksum = computeDirectoryChecksum(bundlePath);
|
|
449
|
+
const entry = {
|
|
450
|
+
id: manifest.id,
|
|
451
|
+
name: manifest.name,
|
|
452
|
+
version: manifest.version,
|
|
453
|
+
description: manifest.description,
|
|
454
|
+
author: manifest.author,
|
|
455
|
+
url: `local://${bundlePath}`,
|
|
456
|
+
checksum,
|
|
457
|
+
keywords: manifest.keywords,
|
|
458
|
+
downloads: 0,
|
|
459
|
+
updatedAt: new Date().toISOString(),
|
|
460
|
+
};
|
|
461
|
+
// Check if already published
|
|
462
|
+
const existingIndex = registry.findIndex(e => e.id === manifest.id);
|
|
463
|
+
if (existingIndex >= 0) {
|
|
464
|
+
registry[existingIndex] = entry;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
registry.push(entry);
|
|
468
|
+
}
|
|
469
|
+
this.saveRegistry(registry);
|
|
470
|
+
return { success: true };
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Search available bundles
|
|
474
|
+
*/
|
|
475
|
+
searchBundles(query) {
|
|
476
|
+
const registry = this.loadRegistry();
|
|
477
|
+
const queryLower = query.toLowerCase();
|
|
478
|
+
const terms = queryLower.split(/\s+/);
|
|
479
|
+
return registry
|
|
480
|
+
.filter(entry => {
|
|
481
|
+
const searchText = [
|
|
482
|
+
entry.id,
|
|
483
|
+
entry.name,
|
|
484
|
+
entry.description,
|
|
485
|
+
entry.author,
|
|
486
|
+
...(entry.keywords ?? []),
|
|
487
|
+
]
|
|
488
|
+
.join(' ')
|
|
489
|
+
.toLowerCase();
|
|
490
|
+
return terms.every(term => searchText.includes(term));
|
|
491
|
+
})
|
|
492
|
+
.map(entry => ({
|
|
493
|
+
id: entry.id,
|
|
494
|
+
name: entry.name,
|
|
495
|
+
version: entry.version,
|
|
496
|
+
description: entry.description,
|
|
497
|
+
author: entry.author,
|
|
498
|
+
keywords: entry.keywords,
|
|
499
|
+
downloads: entry.downloads,
|
|
500
|
+
url: entry.url,
|
|
501
|
+
}))
|
|
502
|
+
.sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0));
|
|
503
|
+
}
|
|
504
|
+
// ---- Enable / Disable ----
|
|
505
|
+
/**
|
|
506
|
+
* Enable a bundle
|
|
507
|
+
*/
|
|
508
|
+
enableBundle(bundleId) {
|
|
509
|
+
const bundle = this.installedBundles.get(bundleId);
|
|
510
|
+
if (!bundle) {
|
|
511
|
+
return { success: false, error: `Bundle "${bundleId}" is not installed` };
|
|
512
|
+
}
|
|
513
|
+
bundle.enabled = true;
|
|
514
|
+
this.saveInstalledBundles();
|
|
515
|
+
// Run install hooks if present
|
|
516
|
+
this.runLifecycleHook(bundle, 'enable');
|
|
517
|
+
return { success: true };
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Disable a bundle
|
|
521
|
+
*/
|
|
522
|
+
disableBundle(bundleId) {
|
|
523
|
+
const bundle = this.installedBundles.get(bundleId);
|
|
524
|
+
if (!bundle) {
|
|
525
|
+
return { success: false, error: `Bundle "${bundleId}" is not installed` };
|
|
526
|
+
}
|
|
527
|
+
bundle.enabled = false;
|
|
528
|
+
this.saveInstalledBundles();
|
|
529
|
+
this.runLifecycleHook(bundle, 'disable');
|
|
530
|
+
return { success: true };
|
|
531
|
+
}
|
|
532
|
+
// ---- Skill / Hook / MCP Resolution ----
|
|
533
|
+
/**
|
|
534
|
+
* Get all skill paths from enabled bundles
|
|
535
|
+
*/
|
|
536
|
+
getBundleSkillPaths() {
|
|
537
|
+
const results = [];
|
|
538
|
+
for (const [bundleId, bundle] of this.installedBundles) {
|
|
539
|
+
if (!bundle.enabled)
|
|
540
|
+
continue;
|
|
541
|
+
for (const skillRef of bundle.manifest.skills) {
|
|
542
|
+
const skillPath = join(bundle.installPath, skillRef);
|
|
543
|
+
if (existsSync(skillPath)) {
|
|
544
|
+
results.push({
|
|
545
|
+
bundleId,
|
|
546
|
+
skillPath,
|
|
547
|
+
skillName: basename(skillPath),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return results;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Get all hook scripts from enabled bundles
|
|
556
|
+
*/
|
|
557
|
+
getBundleHooks() {
|
|
558
|
+
const results = [];
|
|
559
|
+
for (const [bundleId, bundle] of this.installedBundles) {
|
|
560
|
+
if (!bundle.enabled)
|
|
561
|
+
continue;
|
|
562
|
+
for (const hookRef of bundle.manifest.hooks) {
|
|
563
|
+
const hookPath = join(bundle.installPath, hookRef);
|
|
564
|
+
if (existsSync(hookPath)) {
|
|
565
|
+
results.push({
|
|
566
|
+
bundleId,
|
|
567
|
+
hookPath,
|
|
568
|
+
hookName: basename(hookRef),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return results;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get all MCP server configs from enabled bundles
|
|
577
|
+
*/
|
|
578
|
+
getBundleMCPServers() {
|
|
579
|
+
const results = [];
|
|
580
|
+
for (const [bundleId, bundle] of this.installedBundles) {
|
|
581
|
+
if (!bundle.enabled)
|
|
582
|
+
continue;
|
|
583
|
+
for (const mcpRef of bundle.manifest.mcpServers) {
|
|
584
|
+
const configPath = join(bundle.installPath, mcpRef);
|
|
585
|
+
if (existsSync(configPath)) {
|
|
586
|
+
const config = readJSONFile(configPath);
|
|
587
|
+
if (config) {
|
|
588
|
+
results.push({
|
|
589
|
+
bundleId,
|
|
590
|
+
configPath,
|
|
591
|
+
configName: basename(mcpRef),
|
|
592
|
+
config,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return results;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get all tool scripts from enabled bundles
|
|
602
|
+
*/
|
|
603
|
+
getBundleTools() {
|
|
604
|
+
const results = [];
|
|
605
|
+
for (const [bundleId, bundle] of this.installedBundles) {
|
|
606
|
+
if (!bundle.enabled)
|
|
607
|
+
continue;
|
|
608
|
+
for (const toolRef of bundle.manifest.tools) {
|
|
609
|
+
const toolPath = join(bundle.installPath, toolRef);
|
|
610
|
+
if (existsSync(toolPath)) {
|
|
611
|
+
results.push({
|
|
612
|
+
bundleId,
|
|
613
|
+
toolPath,
|
|
614
|
+
toolName: basename(toolRef, extname(toolRef)),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return results;
|
|
620
|
+
}
|
|
621
|
+
// ---- Private: Installation Methods ----
|
|
622
|
+
async installFromPath(sourcePath, warnings) {
|
|
623
|
+
const resolvedPath = resolve(sourcePath);
|
|
624
|
+
// Validate the bundle
|
|
625
|
+
const validation = this.validateBundle(resolvedPath);
|
|
626
|
+
if (!validation.valid) {
|
|
627
|
+
return {
|
|
628
|
+
success: false,
|
|
629
|
+
warnings: validation.warnings.map(w => w.message),
|
|
630
|
+
error: `Validation failed: ${validation.errors.map(e => e.message).join('; ')}`,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
warnings.push(...validation.warnings.map(w => w.message));
|
|
634
|
+
const manifest = validation.manifest;
|
|
635
|
+
// Check if already installed
|
|
636
|
+
const existing = this.installedBundles.get(manifest.id);
|
|
637
|
+
if (existing) {
|
|
638
|
+
// Uninstall old version
|
|
639
|
+
this.uninstallBundle(manifest.id);
|
|
640
|
+
}
|
|
641
|
+
// Copy bundle to install directory
|
|
642
|
+
const installPath = join(this.bundlesDir, manifest.id);
|
|
643
|
+
try {
|
|
644
|
+
if (existsSync(installPath)) {
|
|
645
|
+
rmSync(installPath, { recursive: true, force: true });
|
|
646
|
+
}
|
|
647
|
+
copyRecursive(resolvedPath, installPath);
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
return {
|
|
651
|
+
success: false,
|
|
652
|
+
warnings,
|
|
653
|
+
error: `Failed to copy bundle: ${error instanceof Error ? error.message : String(error)}`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
// Compute checksum
|
|
657
|
+
const checksum = computeDirectoryChecksum(installPath);
|
|
658
|
+
// Create bundle record
|
|
659
|
+
const bundle = {
|
|
660
|
+
manifest,
|
|
661
|
+
installPath,
|
|
662
|
+
installedAt: new Date().toISOString(),
|
|
663
|
+
source: resolvedPath,
|
|
664
|
+
checksum,
|
|
665
|
+
enabled: true,
|
|
666
|
+
};
|
|
667
|
+
// Run install hooks
|
|
668
|
+
this.runLifecycleHook(bundle, 'install');
|
|
669
|
+
// Save
|
|
670
|
+
this.installedBundles.set(manifest.id, bundle);
|
|
671
|
+
this.saveInstalledBundles();
|
|
672
|
+
return { success: true, bundle, warnings };
|
|
673
|
+
}
|
|
674
|
+
async installFromURL(url) {
|
|
675
|
+
// Download the bundle
|
|
676
|
+
const tempDir = join(this.bundlesDir, '.temp', `download_${Date.now()}`);
|
|
677
|
+
ensureDir(tempDir);
|
|
678
|
+
try {
|
|
679
|
+
// Use curl or wget to download
|
|
680
|
+
const isTarball = url.endsWith('.tar.gz') || url.endsWith('.tgz');
|
|
681
|
+
const isZip = url.endsWith('.zip');
|
|
682
|
+
const downloadPath = join(tempDir, isTarball ? 'bundle.tar.gz' : isZip ? 'bundle.zip' : 'bundle.tar.gz');
|
|
683
|
+
const downloadResult = execCommand(`curl -fsSL -o "${downloadPath}" "${url}"`, { timeout: 60000 });
|
|
684
|
+
if (downloadResult.exitCode !== 0) {
|
|
685
|
+
return {
|
|
686
|
+
success: false,
|
|
687
|
+
error: `Failed to download bundle from ${url}: ${downloadResult.stderr}`,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// Extract
|
|
691
|
+
const extractDir = join(tempDir, 'extracted');
|
|
692
|
+
ensureDir(extractDir);
|
|
693
|
+
if (isTarball) {
|
|
694
|
+
const extractResult = execCommand(`tar -xzf "${downloadPath}" -C "${extractDir}"`);
|
|
695
|
+
if (extractResult.exitCode !== 0) {
|
|
696
|
+
return { success: false, error: `Failed to extract tarball: ${extractResult.stderr}` };
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else if (isZip) {
|
|
700
|
+
const extractResult = execCommand(`unzip -q -o "${downloadPath}" -d "${extractDir}"`);
|
|
701
|
+
if (extractResult.exitCode !== 0) {
|
|
702
|
+
return { success: false, error: `Failed to extract zip: ${extractResult.stderr}` };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// Try tarball by default
|
|
707
|
+
const extractResult = execCommand(`tar -xzf "${downloadPath}" -C "${extractDir}"`);
|
|
708
|
+
if (extractResult.exitCode !== 0) {
|
|
709
|
+
return { success: false, error: `Failed to extract archive: ${extractResult.stderr}` };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Find the bundle.json in the extracted directory
|
|
713
|
+
const bundlePath = this.findBundleRoot(extractDir);
|
|
714
|
+
if (!bundlePath) {
|
|
715
|
+
return { success: false, error: `No ${MANIFEST_FILE} found in downloaded archive` };
|
|
716
|
+
}
|
|
717
|
+
// Install from the extracted path
|
|
718
|
+
const result = await this.installFromPath(bundlePath, []);
|
|
719
|
+
// Cleanup temp
|
|
720
|
+
try {
|
|
721
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
// Best-effort cleanup
|
|
725
|
+
}
|
|
726
|
+
// Update source URL in installed bundle
|
|
727
|
+
if (result.success && result.bundle) {
|
|
728
|
+
result.bundle.source = url;
|
|
729
|
+
this.installedBundles.set(result.bundle.manifest.id, result.bundle);
|
|
730
|
+
this.saveInstalledBundles();
|
|
731
|
+
}
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
// Cleanup temp
|
|
736
|
+
try {
|
|
737
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
738
|
+
}
|
|
739
|
+
catch { }
|
|
740
|
+
return {
|
|
741
|
+
success: false,
|
|
742
|
+
error: `Failed to install from URL: ${error instanceof Error ? error.message : String(error)}`,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async installFromRegistry(bundleId) {
|
|
747
|
+
const registry = this.loadRegistry();
|
|
748
|
+
const entry = registry.find(e => e.id === bundleId);
|
|
749
|
+
if (!entry) {
|
|
750
|
+
return { success: false, error: `Bundle "${bundleId}" not found in registry` };
|
|
751
|
+
}
|
|
752
|
+
// If it's a local path reference
|
|
753
|
+
if (entry.url.startsWith('local://')) {
|
|
754
|
+
const localPath = entry.url.replace('local://', '');
|
|
755
|
+
return this.installFromPath(localPath, []);
|
|
756
|
+
}
|
|
757
|
+
// Install from URL
|
|
758
|
+
return this.installFromURL(entry.url);
|
|
759
|
+
}
|
|
760
|
+
// ---- Private: Helper Methods ----
|
|
761
|
+
findBundleRoot(searchDir) {
|
|
762
|
+
// Check if bundle.json is at the root
|
|
763
|
+
if (existsSync(join(searchDir, MANIFEST_FILE))) {
|
|
764
|
+
return searchDir;
|
|
765
|
+
}
|
|
766
|
+
// Check one level down (common for GitHub archives)
|
|
767
|
+
try {
|
|
768
|
+
const entries = readdirSync(searchDir, { withFileTypes: true });
|
|
769
|
+
for (const entry of entries) {
|
|
770
|
+
if (entry.isDirectory()) {
|
|
771
|
+
const subDir = join(searchDir, entry.name);
|
|
772
|
+
if (existsSync(join(subDir, MANIFEST_FILE))) {
|
|
773
|
+
return subDir;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
// Ignore
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
validateBundleReferences(bundlePath, references, expectedDir, errors, warnings) {
|
|
784
|
+
if (!references || references.length === 0)
|
|
785
|
+
return;
|
|
786
|
+
for (const ref of references) {
|
|
787
|
+
const fullPath = join(bundlePath, ref);
|
|
788
|
+
if (!existsSync(fullPath)) {
|
|
789
|
+
// Check if it exists in the expected directory
|
|
790
|
+
const altPath = join(bundlePath, expectedDir, basename(ref));
|
|
791
|
+
if (existsSync(altPath)) {
|
|
792
|
+
warnings.push({
|
|
793
|
+
code: 'REFERENCE_PATH_MISMATCH',
|
|
794
|
+
message: `Referenced file "${ref}" not found at declared path, but exists at "${expectedDir}/${basename(ref)}"`,
|
|
795
|
+
path: ref,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
errors.push({
|
|
800
|
+
code: 'REFERENCE_FILE_MISSING',
|
|
801
|
+
message: `Referenced file "${ref}" does not exist in bundle`,
|
|
802
|
+
path: ref,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
runLifecycleHook(bundle, lifecycle) {
|
|
809
|
+
const hooksDir = join(bundle.installPath, HOOKS_DIR);
|
|
810
|
+
if (!existsSync(hooksDir))
|
|
811
|
+
return;
|
|
812
|
+
try {
|
|
813
|
+
const hookFiles = readdirSync(hooksDir);
|
|
814
|
+
for (const hookFile of hookFiles) {
|
|
815
|
+
// Check if the hook file name contains the lifecycle event
|
|
816
|
+
const hookName = hookFile.toLowerCase();
|
|
817
|
+
if (hookName.includes(lifecycle)) {
|
|
818
|
+
const hookPath = join(hooksDir, hookFile);
|
|
819
|
+
const stat = statSync(hookPath);
|
|
820
|
+
if (stat.isFile()) {
|
|
821
|
+
try {
|
|
822
|
+
execCommand(`"${hookPath}"`, {
|
|
823
|
+
cwd: bundle.installPath,
|
|
824
|
+
timeout: 30000,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
// Hook execution failure should not block operations
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// Ignore hook execution errors
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// ---- Persistence ----
|
|
839
|
+
loadInstalledBundles() {
|
|
840
|
+
const indexFile = join(this.bundlesDir, 'installed.json');
|
|
841
|
+
const data = readJSONFile(indexFile);
|
|
842
|
+
if (!data)
|
|
843
|
+
return;
|
|
844
|
+
for (const entry of data) {
|
|
845
|
+
const manifestPath = join(this.bundlesDir, entry.id, MANIFEST_FILE);
|
|
846
|
+
const manifest = readJSONFile(manifestPath);
|
|
847
|
+
if (manifest) {
|
|
848
|
+
this.installedBundles.set(entry.id, {
|
|
849
|
+
manifest,
|
|
850
|
+
installPath: join(this.bundlesDir, entry.id),
|
|
851
|
+
installedAt: entry.installedAt,
|
|
852
|
+
source: entry.source,
|
|
853
|
+
checksum: entry.checksum,
|
|
854
|
+
enabled: entry.enabled,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
saveInstalledBundles() {
|
|
860
|
+
const indexFile = join(this.bundlesDir, 'installed.json');
|
|
861
|
+
const data = Array.from(this.installedBundles.values()).map(bundle => ({
|
|
862
|
+
id: bundle.manifest.id,
|
|
863
|
+
source: bundle.source,
|
|
864
|
+
installedAt: bundle.installedAt,
|
|
865
|
+
checksum: bundle.checksum,
|
|
866
|
+
enabled: bundle.enabled,
|
|
867
|
+
}));
|
|
868
|
+
writeJSONFile(indexFile, data);
|
|
869
|
+
}
|
|
870
|
+
loadRegistry() {
|
|
871
|
+
return readJSONFile(this.registryFile) ?? [];
|
|
872
|
+
}
|
|
873
|
+
saveRegistry(registry) {
|
|
874
|
+
writeJSONFile(this.registryFile, registry);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// ---- chmodSync polyfill (Node.js built-in, but let's be safe) ----
|
|
878
|
+
function chmodSync(path, mode) {
|
|
879
|
+
try {
|
|
880
|
+
const { chmodSync: nodeChmod } = require('fs');
|
|
881
|
+
nodeChmod(path, mode);
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
// Ignore on platforms that don't support chmod
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
//# sourceMappingURL=plugin-bundle.js.map
|