cray-code 1.0.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/README.md +316 -0
- package/dist/Tool.d.ts +217 -0
- package/dist/Tool.d.ts.map +1 -0
- package/dist/Tool.js +89 -0
- package/dist/Tool.js.map +1 -0
- package/dist/branding/logo.d.ts +8 -0
- package/dist/branding/logo.d.ts.map +1 -0
- package/dist/branding/logo.js +26 -0
- package/dist/branding/logo.js.map +1 -0
- package/dist/branding/theme.d.ts +27 -0
- package/dist/branding/theme.d.ts.map +1 -0
- package/dist/branding/theme.js +28 -0
- package/dist/branding/theme.js.map +1 -0
- package/dist/commands/registry.d.ts +32 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +759 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/components/MessageView.d.ts +12 -0
- package/dist/components/MessageView.d.ts.map +1 -0
- package/dist/components/MessageView.js +35 -0
- package/dist/components/MessageView.js.map +1 -0
- package/dist/components/PermissionPrompt.d.ts +11 -0
- package/dist/components/PermissionPrompt.d.ts.map +1 -0
- package/dist/components/PermissionPrompt.js +6 -0
- package/dist/components/PermissionPrompt.js.map +1 -0
- package/dist/components/PluginManager.d.ts +27 -0
- package/dist/components/PluginManager.d.ts.map +1 -0
- package/dist/components/PluginManager.js +391 -0
- package/dist/components/PluginManager.js.map +1 -0
- package/dist/components/ThinkingBlock.d.ts +27 -0
- package/dist/components/ThinkingBlock.d.ts.map +1 -0
- package/dist/components/ThinkingBlock.js +29 -0
- package/dist/components/ThinkingBlock.js.map +1 -0
- package/dist/components/ToolCallBlock.d.ts +14 -0
- package/dist/components/ToolCallBlock.d.ts.map +1 -0
- package/dist/components/ToolCallBlock.js +83 -0
- package/dist/components/ToolCallBlock.js.map +1 -0
- package/dist/components/TrustDialog.d.ts +20 -0
- package/dist/components/TrustDialog.d.ts.map +1 -0
- package/dist/components/TrustDialog.js +80 -0
- package/dist/components/TrustDialog.js.map +1 -0
- package/dist/context.d.ts +25 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +268 -0
- package/dist/context.js.map +1 -0
- package/dist/cray.d.ts +114 -0
- package/dist/cray.d.ts.map +1 -0
- package/dist/cray.js +338 -0
- package/dist/cray.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/registry.d.ts +106 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +695 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/query.d.ts +31 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +637 -0
- package/dist/query.js.map +1 -0
- package/dist/queryStream.d.ts +36 -0
- package/dist/queryStream.d.ts.map +1 -0
- package/dist/queryStream.js +704 -0
- package/dist/queryStream.js.map +1 -0
- package/dist/screens/ReplScreen.d.ts +22 -0
- package/dist/screens/ReplScreen.d.ts.map +1 -0
- package/dist/screens/ReplScreen.js +763 -0
- package/dist/screens/ReplScreen.js.map +1 -0
- package/dist/services/agentRunner.d.ts +39 -0
- package/dist/services/agentRunner.d.ts.map +1 -0
- package/dist/services/agentRunner.js +115 -0
- package/dist/services/agentRunner.js.map +1 -0
- package/dist/services/compact.d.ts +34 -0
- package/dist/services/compact.d.ts.map +1 -0
- package/dist/services/compact.js +179 -0
- package/dist/services/compact.js.map +1 -0
- package/dist/services/loadPluginCommands.d.ts +55 -0
- package/dist/services/loadPluginCommands.d.ts.map +1 -0
- package/dist/services/loadPluginCommands.js +219 -0
- package/dist/services/loadPluginCommands.js.map +1 -0
- package/dist/services/mcp/index.d.ts +3 -0
- package/dist/services/mcp/index.d.ts.map +1 -0
- package/dist/services/mcp/index.js +3 -0
- package/dist/services/mcp/index.js.map +1 -0
- package/dist/services/mcp/manager.d.ts +24 -0
- package/dist/services/mcp/manager.d.ts.map +1 -0
- package/dist/services/mcp/manager.js +138 -0
- package/dist/services/mcp/manager.js.map +1 -0
- package/dist/services/mcp/types.d.ts +52 -0
- package/dist/services/mcp/types.d.ts.map +1 -0
- package/dist/services/mcp/types.js +5 -0
- package/dist/services/mcp/types.js.map +1 -0
- package/dist/services/memory.d.ts +38 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +181 -0
- package/dist/services/memory.js.map +1 -0
- package/dist/services/permissionPrompt.d.ts +38 -0
- package/dist/services/permissionPrompt.d.ts.map +1 -0
- package/dist/services/permissionPrompt.js +83 -0
- package/dist/services/permissionPrompt.js.map +1 -0
- package/dist/services/permissions.d.ts +15 -0
- package/dist/services/permissions.d.ts.map +1 -0
- package/dist/services/permissions.js +237 -0
- package/dist/services/permissions.js.map +1 -0
- package/dist/services/sessionStorage.d.ts +51 -0
- package/dist/services/sessionStorage.d.ts.map +1 -0
- package/dist/services/sessionStorage.js +266 -0
- package/dist/services/sessionStorage.js.map +1 -0
- package/dist/setup.d.ts +22 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +160 -0
- package/dist/setup.js.map +1 -0
- package/dist/skills/bundledSkills.d.ts +18 -0
- package/dist/skills/bundledSkills.d.ts.map +1 -0
- package/dist/skills/bundledSkills.js +277 -0
- package/dist/skills/bundledSkills.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +3 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/loadSkillsDir.d.ts +45 -0
- package/dist/skills/loadSkillsDir.d.ts.map +1 -0
- package/dist/skills/loadSkillsDir.js +233 -0
- package/dist/skills/loadSkillsDir.js.map +1 -0
- package/dist/state/AppState.d.ts +70 -0
- package/dist/state/AppState.d.ts.map +1 -0
- package/dist/state/AppState.js +106 -0
- package/dist/state/AppState.js.map +1 -0
- package/dist/tools/AgentTool.d.ts +62 -0
- package/dist/tools/AgentTool.d.ts.map +1 -0
- package/dist/tools/AgentTool.js +133 -0
- package/dist/tools/AgentTool.js.map +1 -0
- package/dist/tools/AskUserQuestionTool.d.ts +60 -0
- package/dist/tools/AskUserQuestionTool.d.ts.map +1 -0
- package/dist/tools/AskUserQuestionTool.js +52 -0
- package/dist/tools/AskUserQuestionTool.js.map +1 -0
- package/dist/tools/BashTool.d.ts +33 -0
- package/dist/tools/BashTool.d.ts.map +1 -0
- package/dist/tools/BashTool.js +211 -0
- package/dist/tools/BashTool.js.map +1 -0
- package/dist/tools/EditTool.d.ts +24 -0
- package/dist/tools/EditTool.d.ts.map +1 -0
- package/dist/tools/EditTool.js +102 -0
- package/dist/tools/EditTool.js.map +1 -0
- package/dist/tools/GlobTool.d.ts +17 -0
- package/dist/tools/GlobTool.d.ts.map +1 -0
- package/dist/tools/GlobTool.js +65 -0
- package/dist/tools/GlobTool.js.map +1 -0
- package/dist/tools/GrepTool.d.ts +30 -0
- package/dist/tools/GrepTool.d.ts.map +1 -0
- package/dist/tools/GrepTool.js +140 -0
- package/dist/tools/GrepTool.js.map +1 -0
- package/dist/tools/MCPTool.d.ts +24 -0
- package/dist/tools/MCPTool.d.ts.map +1 -0
- package/dist/tools/MCPTool.js +67 -0
- package/dist/tools/MCPTool.js.map +1 -0
- package/dist/tools/NotebookEditTool.d.ts +28 -0
- package/dist/tools/NotebookEditTool.d.ts.map +1 -0
- package/dist/tools/NotebookEditTool.js +213 -0
- package/dist/tools/NotebookEditTool.js.map +1 -0
- package/dist/tools/NotebookReadTool.d.ts +19 -0
- package/dist/tools/NotebookReadTool.d.ts.map +1 -0
- package/dist/tools/NotebookReadTool.js +191 -0
- package/dist/tools/NotebookReadTool.js.map +1 -0
- package/dist/tools/PlanTools.d.ts +17 -0
- package/dist/tools/PlanTools.d.ts.map +1 -0
- package/dist/tools/PlanTools.js +65 -0
- package/dist/tools/PlanTools.js.map +1 -0
- package/dist/tools/ReadTool.d.ts +21 -0
- package/dist/tools/ReadTool.d.ts.map +1 -0
- package/dist/tools/ReadTool.js +202 -0
- package/dist/tools/ReadTool.js.map +1 -0
- package/dist/tools/SkillTool.d.ts +32 -0
- package/dist/tools/SkillTool.d.ts.map +1 -0
- package/dist/tools/SkillTool.js +217 -0
- package/dist/tools/SkillTool.js.map +1 -0
- package/dist/tools/TodoWriteTool.d.ts +35 -0
- package/dist/tools/TodoWriteTool.d.ts.map +1 -0
- package/dist/tools/TodoWriteTool.js +58 -0
- package/dist/tools/TodoWriteTool.js.map +1 -0
- package/dist/tools/WebFetchTool.d.ts +17 -0
- package/dist/tools/WebFetchTool.d.ts.map +1 -0
- package/dist/tools/WebFetchTool.js +97 -0
- package/dist/tools/WebFetchTool.js.map +1 -0
- package/dist/tools/WebSearchTool.d.ts +18 -0
- package/dist/tools/WebSearchTool.d.ts.map +1 -0
- package/dist/tools/WebSearchTool.js +76 -0
- package/dist/tools/WebSearchTool.js.map +1 -0
- package/dist/tools/WriteTool.d.ts +17 -0
- package/dist/tools/WriteTool.d.ts.map +1 -0
- package/dist/tools/WriteTool.js +84 -0
- package/dist/tools/WriteTool.js.map +1 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools.d.ts +34 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +102 -0
- package/dist/tools.js.map +1 -0
- package/dist/types/events.d.ts +85 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +12 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message.d.ts +71 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +5 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/permission.d.ts +56 -0
- package/dist/types/permission.d.ts.map +1 -0
- package/dist/types/permission.js +46 -0
- package/dist/types/permission.js.map +1 -0
- package/dist/types/processUserInput.d.ts +18 -0
- package/dist/types/processUserInput.d.ts.map +1 -0
- package/dist/types/processUserInput.js +8 -0
- package/dist/types/processUserInput.js.map +1 -0
- package/dist/types/tool.d.ts +32 -0
- package/dist/types/tool.d.ts.map +1 -0
- package/dist/types/tool.js +5 -0
- package/dist/types/tool.js.map +1 -0
- package/dist/utils/compactBoundary.d.ts +11 -0
- package/dist/utils/compactBoundary.d.ts.map +1 -0
- package/dist/utils/compactBoundary.js +26 -0
- package/dist/utils/compactBoundary.js.map +1 -0
- package/dist/utils/configStore.d.ts +41 -0
- package/dist/utils/configStore.d.ts.map +1 -0
- package/dist/utils/configStore.js +111 -0
- package/dist/utils/configStore.js.map +1 -0
- package/dist/utils/forkedAgent.d.ts +40 -0
- package/dist/utils/forkedAgent.d.ts.map +1 -0
- package/dist/utils/forkedAgent.js +231 -0
- package/dist/utils/forkedAgent.js.map +1 -0
- package/dist/utils/messages.d.ts +14 -0
- package/dist/utils/messages.d.ts.map +1 -0
- package/dist/utils/messages.js +29 -0
- package/dist/utils/messages.js.map +1 -0
- package/dist/utils/sandbox.d.ts +22 -0
- package/dist/utils/sandbox.d.ts.map +1 -0
- package/dist/utils/sandbox.js +59 -0
- package/dist/utils/sandbox.js.map +1 -0
- package/dist/utils/sideQuestion.d.ts +29 -0
- package/dist/utils/sideQuestion.d.ts.map +1 -0
- package/dist/utils/sideQuestion.js +81 -0
- package/dist/utils/sideQuestion.js.map +1 -0
- package/install.ps1 +86 -0
- package/install.sh +92 -0
- package/package.json +68 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin System for Cray Code.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Claude Code's plugin architecture: plugins are directories under
|
|
5
|
+
* .cray/plugins/<name>/ containing:
|
|
6
|
+
* manifest.json — required: { name, version, description }
|
|
7
|
+
* commands/ — optional: *.md files = plugin commands
|
|
8
|
+
* skills/ — optional: <skill-name>/SKILL.md = plugin skills
|
|
9
|
+
* mcp.json — optional: { servers: { ... } }
|
|
10
|
+
* hooks.js — optional: { onSessionStart, onSessionEnd, ... }
|
|
11
|
+
*
|
|
12
|
+
* Plugins are auto-discovered at startup. Plugin skills/commands are
|
|
13
|
+
* converted to SkillInfo objects and merged into the global skill list.
|
|
14
|
+
*
|
|
15
|
+
* Commands:
|
|
16
|
+
* /plugin list — show installed plugins
|
|
17
|
+
* /plugin install <path> — install a plugin from a local directory
|
|
18
|
+
* /plugin remove <name> — remove an installed plugin
|
|
19
|
+
* /plugin enable <name> — enable a disabled plugin
|
|
20
|
+
* /plugin disable <name> — disable a plugin
|
|
21
|
+
* /plugin info <name> — show plugin details
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmdirSync } from 'fs';
|
|
24
|
+
import { join, basename } from 'path';
|
|
25
|
+
import { homedir } from 'os';
|
|
26
|
+
import { execFileSync } from 'child_process';
|
|
27
|
+
import { pathToFileURL } from 'url';
|
|
28
|
+
import { discoverAllPlugins } from '../services/loadPluginCommands.js';
|
|
29
|
+
// Re-export for external consumers
|
|
30
|
+
export { discoverAllPlugins };
|
|
31
|
+
const plugins = new Map();
|
|
32
|
+
const loadedFromPaths = new Set();
|
|
33
|
+
// ─── Path Helpers ─────────────────────────────────────────────────────
|
|
34
|
+
function getUserPluginDir() {
|
|
35
|
+
return join(homedir(), '.cray', 'plugins');
|
|
36
|
+
}
|
|
37
|
+
function getProjectPluginDir(cwd) {
|
|
38
|
+
return join(cwd, '.cray', 'plugins');
|
|
39
|
+
}
|
|
40
|
+
function getAllPluginSearchDirs(projectRoot) {
|
|
41
|
+
const dirs = [getUserPluginDir()];
|
|
42
|
+
if (projectRoot)
|
|
43
|
+
dirs.push(getProjectPluginDir(projectRoot));
|
|
44
|
+
return dirs;
|
|
45
|
+
}
|
|
46
|
+
// ─── Registration ─────────────────────────────────────────────────────
|
|
47
|
+
export function registerPlugin(plugin) {
|
|
48
|
+
plugins.set(plugin.manifest.name, plugin);
|
|
49
|
+
loadedFromPaths.add(plugin.dir);
|
|
50
|
+
}
|
|
51
|
+
export function unregisterPlugin(name) {
|
|
52
|
+
const p = plugins.get(name);
|
|
53
|
+
if (p)
|
|
54
|
+
loadedFromPaths.delete(p.dir);
|
|
55
|
+
return plugins.delete(name);
|
|
56
|
+
}
|
|
57
|
+
export function getEnabledPlugins() {
|
|
58
|
+
return [...plugins.values()].filter(p => p.isEnabled());
|
|
59
|
+
}
|
|
60
|
+
export function getAllPlugins() {
|
|
61
|
+
return [...plugins.values()];
|
|
62
|
+
}
|
|
63
|
+
export function getPlugin(name) {
|
|
64
|
+
return plugins.get(name);
|
|
65
|
+
}
|
|
66
|
+
// ─── Skill/Command/MCP Extraction ─────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Get all skills from enabled plugins.
|
|
69
|
+
* Plugin skills follow the same SkillInfo type as bundled/disk skills,
|
|
70
|
+
* so they automatically appear in the SkillTool's available list and
|
|
71
|
+
* the system prompt's skill listing.
|
|
72
|
+
*/
|
|
73
|
+
export function getPluginSkills() {
|
|
74
|
+
return getEnabledPlugins().flatMap(p => p.skills);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get all commands from enabled plugins.
|
|
78
|
+
*/
|
|
79
|
+
export function getPluginCommands() {
|
|
80
|
+
return getEnabledPlugins().flatMap(p => p.commands);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get MCP server configs from enabled plugins.
|
|
84
|
+
* Prefixed with plugin name to avoid collisions: e.g. "myplugin::myserver"
|
|
85
|
+
*/
|
|
86
|
+
export function getPluginMCPServers() {
|
|
87
|
+
const map = new Map();
|
|
88
|
+
for (const p of getEnabledPlugins()) {
|
|
89
|
+
for (const [name, cfg] of Object.entries(p.mcpServers)) {
|
|
90
|
+
map.set(`${p.manifest.name}::${name}`, cfg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
95
|
+
// ─── Hooks ────────────────────────────────────────────────────────────
|
|
96
|
+
export async function executeSessionStartHooks() {
|
|
97
|
+
for (const p of getEnabledPlugins()) {
|
|
98
|
+
try {
|
|
99
|
+
await p.hooks?.onSessionStart?.();
|
|
100
|
+
}
|
|
101
|
+
catch { /* non-fatal */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function executeSessionEndHooks() {
|
|
105
|
+
for (const p of getEnabledPlugins()) {
|
|
106
|
+
try {
|
|
107
|
+
await p.hooks?.onSessionEnd?.();
|
|
108
|
+
}
|
|
109
|
+
catch { /* non-fatal */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ─── Discovery and Loading ────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Load all installed plugins into the registry.
|
|
115
|
+
* Called during CrayCode.initialize().
|
|
116
|
+
*/
|
|
117
|
+
export async function loadAllPlugins(projectRoot) {
|
|
118
|
+
const dirs = getAllPluginSearchDirs(projectRoot);
|
|
119
|
+
const found = discoverAllPlugins(dirs);
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const plugin of found) {
|
|
122
|
+
// Skip duplicates
|
|
123
|
+
if (loadedFromPaths.has(plugin.dir))
|
|
124
|
+
continue;
|
|
125
|
+
if (plugins.has(plugin.manifest.name))
|
|
126
|
+
continue;
|
|
127
|
+
// Load hooks
|
|
128
|
+
let hooks;
|
|
129
|
+
const hooksPath = join(plugin.dir, 'hooks.js');
|
|
130
|
+
if (existsSync(hooksPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const hooksUrl = pathToFileURL(hooksPath).href;
|
|
133
|
+
const mod = await import(hooksUrl);
|
|
134
|
+
hooks = { ...mod };
|
|
135
|
+
}
|
|
136
|
+
catch { /* hooks.js load error */ }
|
|
137
|
+
}
|
|
138
|
+
const registered = { ...plugin, hooks };
|
|
139
|
+
registerPlugin(registered);
|
|
140
|
+
results.push(registered);
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Reload all plugins (use after install/remove/enable/disable).
|
|
146
|
+
*/
|
|
147
|
+
export async function reloadAllPlugins(projectRoot) {
|
|
148
|
+
plugins.clear();
|
|
149
|
+
loadedFromPaths.clear();
|
|
150
|
+
await loadAllPlugins(projectRoot);
|
|
151
|
+
}
|
|
152
|
+
// ─── Plugin Management ────────────────────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Parse a plugin identifier string.
|
|
155
|
+
* Supports three formats:
|
|
156
|
+
* - Local path: ./my-plugin or /absolute/path
|
|
157
|
+
* - Marketplace ref: chrome-devtools-mcp@claude-plugins-official
|
|
158
|
+
* - Marketplace slash: claude-plugins-official/chrome-devtools-mcp
|
|
159
|
+
*
|
|
160
|
+
* Returns { type: 'local' | 'marketplace', name?, marketplace?, source, refPath? }
|
|
161
|
+
*/
|
|
162
|
+
export function parsePluginRef(source) {
|
|
163
|
+
// Check for marketplace format: name@marketplace
|
|
164
|
+
const atMatch = source.match(/^(.+)@(.+)$/);
|
|
165
|
+
if (atMatch && !source.includes('/') && !source.includes('\\')) {
|
|
166
|
+
return {
|
|
167
|
+
type: 'marketplace',
|
|
168
|
+
name: atMatch[1],
|
|
169
|
+
marketplace: atMatch[2],
|
|
170
|
+
displayName: `${atMatch[2]} / ${atMatch[1]}`,
|
|
171
|
+
localPath: atMatch[1],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// Check for marketplace format: marketplace/name (with only one slash, no path)
|
|
175
|
+
const slashMatch = source.match(/^([^@\/\\]+)\/([^@\/\\]+)$/);
|
|
176
|
+
if (slashMatch) {
|
|
177
|
+
return {
|
|
178
|
+
type: 'marketplace',
|
|
179
|
+
name: slashMatch[2],
|
|
180
|
+
marketplace: slashMatch[1],
|
|
181
|
+
displayName: `${slashMatch[1]} / ${slashMatch[2]}`,
|
|
182
|
+
localPath: slashMatch[2],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Treat as local path
|
|
186
|
+
return {
|
|
187
|
+
type: 'local',
|
|
188
|
+
displayName: source,
|
|
189
|
+
localPath: basename(source),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export function installPlugin(source, projectRoot) {
|
|
193
|
+
const destDir = projectRoot ? getProjectPluginDir(projectRoot) : getUserPluginDir();
|
|
194
|
+
mkdirSync(destDir, { recursive: true });
|
|
195
|
+
const ref = parsePluginRef(source);
|
|
196
|
+
// ─── Local path install ──────────────────────────────────────
|
|
197
|
+
if (ref.type === 'local') {
|
|
198
|
+
// Check if it's a URL — git clone and install
|
|
199
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
200
|
+
return installPluginFromUrl(source, destDir);
|
|
201
|
+
}
|
|
202
|
+
const abs = join(process.cwd(), source);
|
|
203
|
+
// If the name looks like a plugin name (no path separators, no file extension),
|
|
204
|
+
// and the local directory doesn't exist, treat it as a marketplace plugin.
|
|
205
|
+
const looksLikePluginName = !source.includes('/') && !source.includes('\\') && !source.includes('.');
|
|
206
|
+
if (!existsSync(abs) && looksLikePluginName) {
|
|
207
|
+
// Fall through to marketplace install — resolve against known marketplaces
|
|
208
|
+
return installFromMarketplace(source, destDir);
|
|
209
|
+
}
|
|
210
|
+
if (!existsSync(abs)) {
|
|
211
|
+
return { success: false, message: `Source not found: ${abs}${looksLikePluginName ? `\n\nDid you mean a marketplace plugin? Try:\n /plugin install claude-plugins-official/${source}` : ''}` };
|
|
212
|
+
}
|
|
213
|
+
if (!statSync(abs).isDirectory()) {
|
|
214
|
+
return { success: false, message: `Source must be a directory: ${abs}` };
|
|
215
|
+
}
|
|
216
|
+
if (!existsSync(join(abs, 'manifest.json'))) {
|
|
217
|
+
return { success: false, message: `No manifest.json found in ${abs}. Plugins must have a manifest.` };
|
|
218
|
+
}
|
|
219
|
+
const name = ref.localPath;
|
|
220
|
+
const dest = join(destDir, name);
|
|
221
|
+
if (existsSync(dest)) {
|
|
222
|
+
return { success: false, message: `Plugin "${name}" is already installed. Remove it first (/plugin remove ${name}).` };
|
|
223
|
+
}
|
|
224
|
+
copyDirSync(abs, dest);
|
|
225
|
+
return { success: true, message: `Plugin "${name}" installed to ${dest}\nReload Cray Code to activate.`, pluginName: name };
|
|
226
|
+
}
|
|
227
|
+
// ─── Marketplace install ─────────────────────────────────────
|
|
228
|
+
const { name, marketplace, displayName } = ref;
|
|
229
|
+
const pkgName = ref.localPath; // Guaranteed string for marketplace refs
|
|
230
|
+
const marketplaceId = marketplace ?? '';
|
|
231
|
+
const skillName = name ?? pkgName;
|
|
232
|
+
const dest = join(destDir, pkgName);
|
|
233
|
+
if (existsSync(dest)) {
|
|
234
|
+
return { success: false, message: `Plugin "${pkgName}" is already installed. Remove it first (/plugin remove ${pkgName}).` };
|
|
235
|
+
}
|
|
236
|
+
// Try known marketplaces
|
|
237
|
+
const resolved = resolveMarketplacePlugin(marketplaceId, skillName);
|
|
238
|
+
if (resolved) {
|
|
239
|
+
// Create the plugin with skill stubs
|
|
240
|
+
mkdirSync(dest, { recursive: true });
|
|
241
|
+
writeFileSync(join(dest, 'manifest.json'), JSON.stringify({
|
|
242
|
+
name: skillName,
|
|
243
|
+
version: '1.0.0',
|
|
244
|
+
description: `Marketplace plugin: ${displayName}`,
|
|
245
|
+
author: marketplaceId,
|
|
246
|
+
repository: resolved,
|
|
247
|
+
}, null, 2), 'utf-8');
|
|
248
|
+
writeFileSync(join(dest, 'README.md'), `# ${skillName}\n\nInstalled from ${displayName}.\nSource: ${resolved}`, 'utf-8');
|
|
249
|
+
// Create basic skill stubs if it's a known plugin type
|
|
250
|
+
mkdirSync(join(dest, 'commands'), { recursive: true });
|
|
251
|
+
mkdirSync(join(dest, 'skills'), { recursive: true });
|
|
252
|
+
mkdirSync(join(dest, 'skills', skillName), { recursive: true });
|
|
253
|
+
writeFileSync(join(dest, 'skills', skillName, 'SKILL.md'), [
|
|
254
|
+
'---',
|
|
255
|
+
`description: "${skillName} — ${displayName}"`,
|
|
256
|
+
'allowed-tools: [read, glob, grep, bash, web_fetch]',
|
|
257
|
+
'context: inline',
|
|
258
|
+
'---',
|
|
259
|
+
'',
|
|
260
|
+
`# ${skillName}`,
|
|
261
|
+
'',
|
|
262
|
+
'This skill was installed from the marketplace.',
|
|
263
|
+
'',
|
|
264
|
+
'To customize: edit this SKILL.md, then reload Cray Code.',
|
|
265
|
+
].join('\n'), 'utf-8');
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
message: [
|
|
269
|
+
`Plugin "${pkgName}" installed from ${displayName}!`,
|
|
270
|
+
`Directory: ${dest}`,
|
|
271
|
+
'',
|
|
272
|
+
`Template files created:`,
|
|
273
|
+
` ${dest}/manifest.json`,
|
|
274
|
+
` ${dest}/skills/${skillName}/SKILL.md`,
|
|
275
|
+
'',
|
|
276
|
+
`Edit the SKILL.md to add your skill instructions,`,
|
|
277
|
+
`then add commands/*.md files for additional features.`,
|
|
278
|
+
'',
|
|
279
|
+
`Reload Cray Code to activate.`,
|
|
280
|
+
].join('\n'),
|
|
281
|
+
pluginName: pkgName,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// Marketplace not recognized — scaffold an empty plugin for the user
|
|
285
|
+
mkdirSync(dest, { recursive: true });
|
|
286
|
+
writeFileSync(join(dest, 'manifest.json'), JSON.stringify({
|
|
287
|
+
name: skillName,
|
|
288
|
+
version: '1.0.0',
|
|
289
|
+
description: `Plugin: ${displayName}`,
|
|
290
|
+
author: marketplaceId,
|
|
291
|
+
}, null, 2), 'utf-8');
|
|
292
|
+
mkdirSync(join(dest, 'commands'), { recursive: true });
|
|
293
|
+
mkdirSync(join(dest, 'skills'), { recursive: true });
|
|
294
|
+
mkdirSync(join(dest, 'skills', skillName), { recursive: true });
|
|
295
|
+
writeFileSync(join(dest, 'skills', skillName, 'SKILL.md'), [
|
|
296
|
+
'---',
|
|
297
|
+
`description: "${skillName} skill"`,
|
|
298
|
+
'allowed-tools: [read, glob, grep]',
|
|
299
|
+
'context: inline',
|
|
300
|
+
'---',
|
|
301
|
+
'',
|
|
302
|
+
`# ${skillName}`,
|
|
303
|
+
'',
|
|
304
|
+
`Marketplace: ${displayName}`,
|
|
305
|
+
'',
|
|
306
|
+
'## When to Use',
|
|
307
|
+
'',
|
|
308
|
+
'Describe when this skill should be invoked.',
|
|
309
|
+
'',
|
|
310
|
+
'## Instructions',
|
|
311
|
+
'',
|
|
312
|
+
'Detailed instructions go here.',
|
|
313
|
+
].join('\n'), 'utf-8');
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
message: [
|
|
317
|
+
`Plugin "${pkgName}" scaffolded from ${displayName}.`,
|
|
318
|
+
`Directory: ${dest}`,
|
|
319
|
+
'',
|
|
320
|
+
`Template files created:`,
|
|
321
|
+
` ${dest}/manifest.json`,
|
|
322
|
+
` ${dest}/skills/${skillName}/SKILL.md`,
|
|
323
|
+
'',
|
|
324
|
+
`Edit the SKILL.md and commands/*.md to add your plugin logic.`,
|
|
325
|
+
`Reload Cray Code to activate: restart cray.`,
|
|
326
|
+
].join('\n'),
|
|
327
|
+
pluginName: pkgName,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Install a plugin from a git URL by cloning into the plugins directory.
|
|
332
|
+
*/
|
|
333
|
+
function installPluginFromUrl(gitUrl, destDir) {
|
|
334
|
+
// Check if git is available
|
|
335
|
+
try {
|
|
336
|
+
execFileSync('git', ['--version'], { stdio: 'pipe' });
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return { success: false, message: 'Git is required for URL-based plugin installation.\nInstall it from: https://git-scm.com/' };
|
|
340
|
+
}
|
|
341
|
+
// Extract a safe directory name from the URL
|
|
342
|
+
const urlForName = gitUrl.replace(/\.git$/, '').replace(/\/$/, '');
|
|
343
|
+
const name = basename(urlForName) || 'installed-plugin';
|
|
344
|
+
const dest = join(destDir, name);
|
|
345
|
+
if (existsSync(dest)) {
|
|
346
|
+
return { success: false, message: `Plugin "${name}" already exists. Remove it first: /plugin remove ${name}` };
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
// Clone the repo (shallow to save bandwidth)
|
|
350
|
+
// Use execFileSync with argument array — no shell, no injection
|
|
351
|
+
execFileSync('git', ['clone', '--depth', '1', gitUrl, dest], {
|
|
352
|
+
cwd: destDir,
|
|
353
|
+
stdio: 'pipe',
|
|
354
|
+
timeout: 60_000,
|
|
355
|
+
});
|
|
356
|
+
// Check for manifest.json
|
|
357
|
+
if (!existsSync(join(dest, 'manifest.json'))) {
|
|
358
|
+
// Generate a manifest from repo metadata
|
|
359
|
+
let desc = `Plugin from ${gitUrl}`;
|
|
360
|
+
try {
|
|
361
|
+
const readme = join(dest, 'README.md');
|
|
362
|
+
if (existsSync(readme)) {
|
|
363
|
+
const firstLine = readFileSync(readme, 'utf-8').split('\n')[0];
|
|
364
|
+
if (firstLine)
|
|
365
|
+
desc = firstLine.replace(/^#+\s*/, '').trim();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch { /* use default */ }
|
|
369
|
+
writeFileSync(join(dest, 'manifest.json'), JSON.stringify({
|
|
370
|
+
name,
|
|
371
|
+
version: '1.0.0',
|
|
372
|
+
description: desc,
|
|
373
|
+
repository: gitUrl,
|
|
374
|
+
}, null, 2), 'utf-8');
|
|
375
|
+
}
|
|
376
|
+
// If no skills/ directory, create a default one
|
|
377
|
+
if (!existsSync(join(dest, 'skills'))) {
|
|
378
|
+
mkdirSync(join(dest, 'skills', name), { recursive: true });
|
|
379
|
+
writeFileSync(join(dest, 'skills', name, 'SKILL.md'), [
|
|
380
|
+
'---',
|
|
381
|
+
`description: "${name} — installed from ${gitUrl}"`,
|
|
382
|
+
'allowed-tools: [read, glob, grep, bash]',
|
|
383
|
+
'context: inline',
|
|
384
|
+
'---',
|
|
385
|
+
'',
|
|
386
|
+
`# ${name}`,
|
|
387
|
+
'',
|
|
388
|
+
'This plugin was installed from:',
|
|
389
|
+
` ${gitUrl}`,
|
|
390
|
+
'',
|
|
391
|
+
'Add your skill instructions to this file and reload Cray Code.',
|
|
392
|
+
].join('\n'), 'utf-8');
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
success: true,
|
|
396
|
+
message: [
|
|
397
|
+
`Plugin "${name}" installed from git!`,
|
|
398
|
+
`Directory: ${dest}`,
|
|
399
|
+
'',
|
|
400
|
+
`Reload Cray Code to activate.`,
|
|
401
|
+
].join('\n'),
|
|
402
|
+
pluginName: name,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
// Clean up failed clone
|
|
407
|
+
if (existsSync(dest)) {
|
|
408
|
+
try {
|
|
409
|
+
rmDirSync(dest);
|
|
410
|
+
}
|
|
411
|
+
catch { /* best-effort */ }
|
|
412
|
+
}
|
|
413
|
+
const msg = err.stderr ? String(err.stderr) : err.message;
|
|
414
|
+
return { success: false, message: `Git clone failed: ${msg.slice(0, 300)}` };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Resolve a marketplace plugin to a known source URL.
|
|
419
|
+
* Returns the URL/fetch string, or null if not found.
|
|
420
|
+
*/
|
|
421
|
+
function resolveMarketplacePlugin(marketplace, pluginName) {
|
|
422
|
+
const key = `${marketplace}/${pluginName}`;
|
|
423
|
+
const registry = {
|
|
424
|
+
'claude-plugins-official/chrome-devtools-mcp': 'https://github.com/anthropics/claude-code/tree/main/plugins',
|
|
425
|
+
'claude-plugins-official/code-review': 'https://github.com/anthropics/claude-code/tree/main/plugins',
|
|
426
|
+
'claude-plugins-official/mcp-server-context7': 'https://github.com/upstash/context7',
|
|
427
|
+
'context7/mcp-server-context7': 'https://github.com/upstash/context7',
|
|
428
|
+
'context7/context7': 'https://github.com/upstash/context7',
|
|
429
|
+
'claude-plugins-official/gitlab': 'https://gitlab.com',
|
|
430
|
+
'claude-plugins-official/playwright': 'https://github.com/microsoft/playwright',
|
|
431
|
+
'claude-plugins-official/linear': 'https://linear.app',
|
|
432
|
+
'claude-plugins-official/slack': 'https://slack.com',
|
|
433
|
+
'claude-plugins-official/notion': 'https://notion.so',
|
|
434
|
+
'claude-plugins-official/figma': 'https://figma.com',
|
|
435
|
+
'claude-plugins-official/sentry': 'https://sentry.io',
|
|
436
|
+
'claude-plugins-official/vercel': 'https://vercel.com',
|
|
437
|
+
};
|
|
438
|
+
return registry[key] ?? null;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Try to install a bare plugin name by searching known marketplaces.
|
|
442
|
+
* When the user types `/plugin install context7`, we search for it.
|
|
443
|
+
*/
|
|
444
|
+
function installFromMarketplace(bareName, destDir) {
|
|
445
|
+
// Try exact matches first across known marketplaces
|
|
446
|
+
const knownMarketplaces = [
|
|
447
|
+
'claude-plugins-official',
|
|
448
|
+
'context7',
|
|
449
|
+
'upstash',
|
|
450
|
+
];
|
|
451
|
+
// Exact match: name == known plugin
|
|
452
|
+
const exactMatches = [];
|
|
453
|
+
for (const mp of knownMarketplaces) {
|
|
454
|
+
const resolved = resolveMarketplacePlugin(mp, bareName);
|
|
455
|
+
if (resolved)
|
|
456
|
+
exactMatches.push(mp);
|
|
457
|
+
}
|
|
458
|
+
// Also check fuzzy matches — if the name contains or is contained by known plugins
|
|
459
|
+
const allKnownPlugins = [
|
|
460
|
+
'chrome-devtools-mcp', 'code-review', 'mcp-server-context7', 'context7',
|
|
461
|
+
'gitlab', 'playwright', 'linear', 'slack', 'notion', 'figma', 'sentry', 'vercel',
|
|
462
|
+
];
|
|
463
|
+
const fuzzyMatches = allKnownPlugins.filter(p => p.toLowerCase().includes(bareName.toLowerCase()) ||
|
|
464
|
+
bareName.toLowerCase().includes(p.toLowerCase()));
|
|
465
|
+
// Resolve each fuzzy match
|
|
466
|
+
const fuzzyResolved = [];
|
|
467
|
+
for (const pn of fuzzyMatches) {
|
|
468
|
+
for (const mp of knownMarketplaces) {
|
|
469
|
+
const resolved = resolveMarketplacePlugin(mp, pn);
|
|
470
|
+
if (resolved) {
|
|
471
|
+
fuzzyResolved.push({ name: pn, marketplace: mp });
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// If we found exact matches, install the first one
|
|
477
|
+
if (exactMatches.length > 0) {
|
|
478
|
+
const mp = exactMatches[0];
|
|
479
|
+
return finishMarketplaceInstall(mp, bareName, destDir);
|
|
480
|
+
}
|
|
481
|
+
// If we have one fuzzy match, use it
|
|
482
|
+
if (fuzzyResolved.length === 1) {
|
|
483
|
+
const match = fuzzyResolved[0];
|
|
484
|
+
return finishMarketplaceInstall(match.marketplace, match.name, destDir);
|
|
485
|
+
}
|
|
486
|
+
// If we have multiple matches, list them
|
|
487
|
+
if (fuzzyResolved.length > 1) {
|
|
488
|
+
const suggestions = fuzzyResolved
|
|
489
|
+
.map(f => ` /plugin install ${f.marketplace}/${f.name}`)
|
|
490
|
+
.join('\n');
|
|
491
|
+
return {
|
|
492
|
+
success: false,
|
|
493
|
+
message: [
|
|
494
|
+
`No exact match for "${bareName}". Did you mean one of these?`,
|
|
495
|
+
'',
|
|
496
|
+
suggestions,
|
|
497
|
+
'',
|
|
498
|
+
`Or install from a specific marketplace: /plugin install <marketplace>/<name>`,
|
|
499
|
+
].join('\n'),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// No match — scaffold a new plugin with the given name
|
|
503
|
+
const dest = join(destDir, bareName);
|
|
504
|
+
if (existsSync(dest)) {
|
|
505
|
+
return { success: false, message: `Plugin "${bareName}" is already installed.` };
|
|
506
|
+
}
|
|
507
|
+
mkdirSync(dest, { recursive: true });
|
|
508
|
+
writeFileSync(join(dest, 'manifest.json'), JSON.stringify({
|
|
509
|
+
name: bareName,
|
|
510
|
+
version: '1.0.0',
|
|
511
|
+
description: `Plugin: ${bareName}`,
|
|
512
|
+
}, null, 2), 'utf-8');
|
|
513
|
+
mkdirSync(join(dest, 'commands'), { recursive: true });
|
|
514
|
+
mkdirSync(join(dest, 'skills'), { recursive: true });
|
|
515
|
+
mkdirSync(join(dest, 'skills', bareName), { recursive: true });
|
|
516
|
+
writeFileSync(join(dest, 'skills', bareName, 'SKILL.md'), [
|
|
517
|
+
'---',
|
|
518
|
+
`description: "${bareName} skill"`,
|
|
519
|
+
'allowed-tools: [read, glob, grep]',
|
|
520
|
+
'context: inline',
|
|
521
|
+
'---',
|
|
522
|
+
'',
|
|
523
|
+
`# ${bareName}`,
|
|
524
|
+
'',
|
|
525
|
+
'## When to Use',
|
|
526
|
+
'',
|
|
527
|
+
'Describe when this skill should be invoked.',
|
|
528
|
+
'',
|
|
529
|
+
'## Instructions',
|
|
530
|
+
'',
|
|
531
|
+
'Describe your plugin instructions here.',
|
|
532
|
+
].join('\n'), 'utf-8');
|
|
533
|
+
return {
|
|
534
|
+
success: true,
|
|
535
|
+
message: [
|
|
536
|
+
`Plugin "${bareName}" scaffolded (not found in known marketplaces).`,
|
|
537
|
+
`Directory: ${dest}`,
|
|
538
|
+
'',
|
|
539
|
+
`Template files created:`,
|
|
540
|
+
` ${dest}/manifest.json`,
|
|
541
|
+
` ${dest}/skills/${bareName}/SKILL.md`,
|
|
542
|
+
'',
|
|
543
|
+
`Edit the SKILL.md and commands/*.md to add your plugin logic.`,
|
|
544
|
+
`Restart cray to activate.`,
|
|
545
|
+
].join('\n'),
|
|
546
|
+
pluginName: bareName,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Finish a marketplace install: scaffold the plugin directory with templates.
|
|
551
|
+
*/
|
|
552
|
+
function finishMarketplaceInstall(marketplace, pluginName, destDir) {
|
|
553
|
+
const dest = join(destDir, pluginName);
|
|
554
|
+
if (existsSync(dest)) {
|
|
555
|
+
return { success: false, message: `Plugin "${pluginName}" is already installed. Remove it first (/plugin remove ${pluginName}).` };
|
|
556
|
+
}
|
|
557
|
+
const displayName = `${marketplace} / ${pluginName}`;
|
|
558
|
+
mkdirSync(dest, { recursive: true });
|
|
559
|
+
writeFileSync(join(dest, 'manifest.json'), JSON.stringify({
|
|
560
|
+
name: pluginName,
|
|
561
|
+
version: '1.0.0',
|
|
562
|
+
description: `Marketplace plugin: ${displayName}`,
|
|
563
|
+
author: marketplace,
|
|
564
|
+
}, null, 2), 'utf-8');
|
|
565
|
+
mkdirSync(join(dest, 'commands'), { recursive: true });
|
|
566
|
+
mkdirSync(join(dest, 'skills'), { recursive: true });
|
|
567
|
+
mkdirSync(join(dest, 'skills', pluginName), { recursive: true });
|
|
568
|
+
writeFileSync(join(dest, 'skills', pluginName, 'SKILL.md'), [
|
|
569
|
+
'---',
|
|
570
|
+
`description: "${pluginName} — ${displayName}"`,
|
|
571
|
+
'allowed-tools: [read, glob, grep, bash, web_fetch]',
|
|
572
|
+
'context: inline',
|
|
573
|
+
'---',
|
|
574
|
+
'',
|
|
575
|
+
`# ${pluginName}`,
|
|
576
|
+
'',
|
|
577
|
+
`This skill was installed from the marketplace (${marketplace}).`,
|
|
578
|
+
'',
|
|
579
|
+
'To customize: edit this SKILL.md, then reload Cray Code.',
|
|
580
|
+
].join('\n'), 'utf-8');
|
|
581
|
+
return {
|
|
582
|
+
success: true,
|
|
583
|
+
message: [
|
|
584
|
+
`Plugin "${pluginName}" installed from ${displayName}!`,
|
|
585
|
+
`Directory: ${dest}`,
|
|
586
|
+
'',
|
|
587
|
+
`Template files created:`,
|
|
588
|
+
` ${dest}/manifest.json`,
|
|
589
|
+
` ${dest}/skills/${pluginName}/SKILL.md`,
|
|
590
|
+
'',
|
|
591
|
+
`Edit the SKILL.md to add your plugin's instructions.`,
|
|
592
|
+
`Restart cray to activate.`,
|
|
593
|
+
].join('\n'),
|
|
594
|
+
pluginName,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
export function removePlugin(name, projectRoot) {
|
|
598
|
+
const dirs = getAllPluginSearchDirs(projectRoot);
|
|
599
|
+
for (const d of dirs) {
|
|
600
|
+
const target = join(d, name);
|
|
601
|
+
if (existsSync(target)) {
|
|
602
|
+
rmDirSync(target);
|
|
603
|
+
plugins.delete(name);
|
|
604
|
+
loadedFromPaths.delete(target);
|
|
605
|
+
return { success: true, message: `Plugin "${name}" removed.` };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return { success: false, message: `Plugin "${name}" not found in any plugin directory.` };
|
|
609
|
+
}
|
|
610
|
+
export function enablePlugin(name) {
|
|
611
|
+
const p = plugins.get(name);
|
|
612
|
+
if (!p)
|
|
613
|
+
return { success: false, message: `Plugin "${name}" not registered.` };
|
|
614
|
+
if (p.enabled)
|
|
615
|
+
return { success: true, message: `Plugin "${name}" is already enabled.` };
|
|
616
|
+
p.enabled = true;
|
|
617
|
+
persistPluginState(name, true);
|
|
618
|
+
return { success: true, message: `Plugin "${name}" enabled. Reload to activate.` };
|
|
619
|
+
}
|
|
620
|
+
export function disablePlugin(name) {
|
|
621
|
+
const p = plugins.get(name);
|
|
622
|
+
if (!p)
|
|
623
|
+
return { success: false, message: `Plugin "${name}" not registered.` };
|
|
624
|
+
if (!p.enabled)
|
|
625
|
+
return { success: true, message: `Plugin "${name}" is already disabled.` };
|
|
626
|
+
p.enabled = false;
|
|
627
|
+
persistPluginState(name, false);
|
|
628
|
+
return { success: true, message: `Plugin "${name}" disabled.` };
|
|
629
|
+
}
|
|
630
|
+
// ─── State Persistence ────────────────────────────────────────────────
|
|
631
|
+
function persistPluginState(name, enabled) {
|
|
632
|
+
try {
|
|
633
|
+
const statePath = join(homedir(), '.cray', 'plugin_state.json');
|
|
634
|
+
let state = {};
|
|
635
|
+
if (existsSync(statePath)) {
|
|
636
|
+
try {
|
|
637
|
+
state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
638
|
+
}
|
|
639
|
+
catch { /* reset */ }
|
|
640
|
+
}
|
|
641
|
+
state[name] = enabled;
|
|
642
|
+
mkdirSync(join(statePath, '..'), { recursive: true });
|
|
643
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
644
|
+
}
|
|
645
|
+
catch { /* non-fatal */ }
|
|
646
|
+
}
|
|
647
|
+
function loadPluginState() {
|
|
648
|
+
try {
|
|
649
|
+
const statePath = join(homedir(), '.cray', 'plugin_state.json');
|
|
650
|
+
if (existsSync(statePath)) {
|
|
651
|
+
return JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
catch { /* use defaults */ }
|
|
655
|
+
return {};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Apply saved enable/disable state on startup.
|
|
659
|
+
*/
|
|
660
|
+
export function applyPluginState() {
|
|
661
|
+
const state = loadPluginState();
|
|
662
|
+
for (const [name, enabled] of Object.entries(state)) {
|
|
663
|
+
const p = plugins.get(name);
|
|
664
|
+
if (p)
|
|
665
|
+
p.enabled = enabled;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// ─── File System Helpers ──────────────────────────────────────────────
|
|
669
|
+
function copyDirSync(src, dest) {
|
|
670
|
+
mkdirSync(dest, { recursive: true });
|
|
671
|
+
for (const entry of readdirSync(src)) {
|
|
672
|
+
const sp = join(src, entry), dp = join(dest, entry);
|
|
673
|
+
if (statSync(sp).isDirectory()) {
|
|
674
|
+
copyDirSync(sp, dp);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
writeFileSync(dp, readFileSync(sp));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function rmDirSync(dir) {
|
|
682
|
+
if (!existsSync(dir))
|
|
683
|
+
return;
|
|
684
|
+
for (const entry of readdirSync(dir)) {
|
|
685
|
+
const p = join(dir, entry);
|
|
686
|
+
if (statSync(p).isDirectory()) {
|
|
687
|
+
rmDirSync(p);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
unlinkSync(p);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
rmdirSync(dir);
|
|
694
|
+
}
|
|
695
|
+
//# sourceMappingURL=registry.js.map
|