@dboio/cli 0.6.3 → 0.6.5
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 +2 -2
- package/bin/dbo.js +1 -1
- package/bin/postinstall.js +1 -1
- package/package.json +2 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +8 -0
- package/{src/plugins/claudecommands/dbo.md → plugins/claude/dbo/skills/cli/SKILL.md} +3 -2
- package/src/commands/install.js +472 -137
- package/src/lib/config.js +60 -17
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ Once installed, use `/dbo` in Claude Code:
|
|
|
62
62
|
/dbo push assets/css/
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
The
|
|
65
|
+
The plugin source lives in `plugins/claude/dbo/` at the repository root. Installed copies in `.claude/plugins/` are gitignored and managed by `dbo install`. Each plugin's installation scope (project or global) is stored per-plugin in `.dbo/config.local.json`.
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
@@ -1169,7 +1169,7 @@ Smart behavior:
|
|
|
1169
1169
|
- If plugins are already installed and up to date, reports "already up to date"
|
|
1170
1170
|
- If plugins differ from source, prompts to upgrade to the latest version
|
|
1171
1171
|
- Compares file hashes — unchanged files are skipped
|
|
1172
|
-
- Adds project-scoped
|
|
1172
|
+
- Adds project-scoped plugins to `.gitignore` (source of truth is `plugins/claude/` at repo root)
|
|
1173
1173
|
- Per-plugin scope (project or global) is stored in `.dbo/config.local.json` and remembered for future upgrades
|
|
1174
1174
|
- On first install of a plugin without a stored preference, prompts for scope
|
|
1175
1175
|
- `--global` / `--local` flags override stored preferences and update them
|
package/bin/dbo.js
CHANGED
|
@@ -46,7 +46,7 @@ function checkFirstRun() {
|
|
|
46
46
|
console.log(`${BOLD}${CYAN} Welcome to DBO.io CLI!${RESET}`);
|
|
47
47
|
console.log('');
|
|
48
48
|
console.log(` ${DIM}Available plugins:${RESET}`);
|
|
49
|
-
console.log(` ${YELLOW}Claude Code — /dbo slash command${RESET}`);
|
|
49
|
+
console.log(` ${YELLOW}Claude Code — /dbo:cli slash command${RESET}`);
|
|
50
50
|
console.log(` ${DIM}dbo install --claudecommand dbo${RESET}`);
|
|
51
51
|
console.log('');
|
|
52
52
|
console.log(` To install all plugins at once, run:`);
|
package/bin/postinstall.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dboio/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "CLI for the DBO.io framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"node": ">=18.0.0"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
+
"prepublishOnly": "node scripts/copy-plugins.js",
|
|
19
20
|
"postinstall": "node bin/postinstall.js",
|
|
20
21
|
"test": "node --test src/**/*.test.js tests/**/*.test.js"
|
|
21
22
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: cli
|
|
2
3
|
description: Execute DBO.io CLI commands (pull, push, add, clone, output, input, content, deploy, etc.)
|
|
3
|
-
|
|
4
|
+
user-invokable: true
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# DBO CLI Command
|
|
@@ -9,7 +10,7 @@ The dbo CLI interacts with DBO.io — a database-driven application framework.
|
|
|
9
10
|
|
|
10
11
|
**STEP 1: Check if `$ARGUMENTS` is empty or blank.**
|
|
11
12
|
|
|
12
|
-
If `$ARGUMENTS` is empty — meaning the user typed just `/dbo` with nothing after it — then you MUST NOT run any bash command. Do NOT run `dbo`, `dbo --help`, or anything else. Instead, respond ONLY with this text message:
|
|
13
|
+
If `$ARGUMENTS` is empty — meaning the user typed just `/dbo:cli` with nothing after it — then you MUST NOT run any bash command. Do NOT run `dbo`, `dbo --help`, or anything else. Instead, respond ONLY with this text message:
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
package/src/commands/install.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readdir, readFile, writeFile, mkdir, access, copyFile } from 'fs/promises';
|
|
2
|
+
import { readdir, readFile, writeFile, mkdir, access, copyFile, cp, rm } from 'fs/promises';
|
|
3
3
|
import { join, dirname, resolve } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
7
8
|
import { homedir } from 'os';
|
|
8
9
|
import { log } from '../lib/logger.js';
|
|
9
10
|
import { getPluginScope, setPluginScope, isInitialized, removeFromGitignore } from '../lib/config.js';
|
|
10
11
|
|
|
12
|
+
const CLAUDE_PLUGINS_DIR = join(homedir(), '.claude', 'plugins');
|
|
13
|
+
const PLUGIN_REGISTRY_PATH = join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
14
|
+
const PLUGIN_MARKETPLACE = 'dboio';
|
|
15
|
+
|
|
11
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
17
|
const CLI_ROOT = join(__dirname, '..', '..');
|
|
13
|
-
const
|
|
18
|
+
const LEGACY_PLUGINS_DIR = join(__dirname, '..', 'plugins', 'claudecommands');
|
|
19
|
+
const PLUGINS_DIR = join(CLI_ROOT, 'plugins', 'claude');
|
|
14
20
|
|
|
15
21
|
async function fileExists(path) {
|
|
16
22
|
try { await access(path); return true; } catch { return false; }
|
|
@@ -21,7 +27,7 @@ function fileHash(content) {
|
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
|
-
* Get the target commands directory based on scope.
|
|
30
|
+
* Get the target commands directory based on scope (legacy .md format).
|
|
25
31
|
* @param {'project' | 'global'} scope
|
|
26
32
|
* @returns {string} Absolute path to commands directory
|
|
27
33
|
*/
|
|
@@ -33,10 +39,143 @@ function getCommandsDir(scope) {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
|
-
* Get
|
|
42
|
+
* Get the target plugins directory based on scope (new directory-based format).
|
|
43
|
+
* For global scope, returns the cache path that Claude Code expects.
|
|
44
|
+
* @param {'project' | 'global'} scope
|
|
45
|
+
* @returns {string} Absolute path to plugins directory
|
|
46
|
+
*/
|
|
47
|
+
function getPluginsDir(scope) {
|
|
48
|
+
if (scope === 'global') {
|
|
49
|
+
return join(CLAUDE_PLUGINS_DIR, 'cache', PLUGIN_MARKETPLACE);
|
|
50
|
+
}
|
|
51
|
+
return join(process.cwd(), '.claude', 'plugins');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the install path for a global plugin in the cache directory.
|
|
56
|
+
* @param {string} pluginName - Plugin name
|
|
57
|
+
* @param {string} version - Plugin version
|
|
58
|
+
* @returns {string} Absolute path like ~/.claude/plugins/cache/dboio/<name>/<version>/
|
|
59
|
+
*/
|
|
60
|
+
function getGlobalPluginCachePath(pluginName, version) {
|
|
61
|
+
return join(CLAUDE_PLUGINS_DIR, 'cache', PLUGIN_MARKETPLACE, pluginName, version);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the Claude Code plugin registry (installed_plugins.json).
|
|
66
|
+
* @returns {Promise<object>} The registry object
|
|
37
67
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
68
|
+
async function readPluginRegistry() {
|
|
69
|
+
try {
|
|
70
|
+
const content = await readFile(PLUGIN_REGISTRY_PATH, 'utf8');
|
|
71
|
+
return JSON.parse(content);
|
|
72
|
+
} catch {
|
|
73
|
+
return { version: 2, plugins: {} };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register a plugin in Claude Code's installed_plugins.json.
|
|
79
|
+
* @param {string} pluginName - Plugin name
|
|
80
|
+
* @param {string} version - Plugin version
|
|
81
|
+
* @param {string} installPath - Absolute path to installed plugin
|
|
82
|
+
*/
|
|
83
|
+
async function registerPlugin(pluginName, version, installPath) {
|
|
84
|
+
const registry = await readPluginRegistry();
|
|
85
|
+
const key = `${pluginName}@${PLUGIN_MARKETPLACE}`;
|
|
86
|
+
const now = new Date().toISOString();
|
|
87
|
+
|
|
88
|
+
const existing = registry.plugins[key]?.[0];
|
|
89
|
+
if (existing) {
|
|
90
|
+
existing.installPath = installPath;
|
|
91
|
+
existing.version = version;
|
|
92
|
+
existing.lastUpdated = now;
|
|
93
|
+
} else {
|
|
94
|
+
registry.plugins[key] = [{
|
|
95
|
+
scope: 'user',
|
|
96
|
+
installPath,
|
|
97
|
+
version,
|
|
98
|
+
installedAt: now,
|
|
99
|
+
lastUpdated: now,
|
|
100
|
+
}];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await writeFile(PLUGIN_REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Unregister a plugin from Claude Code's installed_plugins.json.
|
|
108
|
+
* @param {string} pluginName - Plugin name
|
|
109
|
+
*/
|
|
110
|
+
async function unregisterPlugin(pluginName) {
|
|
111
|
+
const registry = await readPluginRegistry();
|
|
112
|
+
const key = `${pluginName}@${PLUGIN_MARKETPLACE}`;
|
|
113
|
+
if (registry.plugins[key]) {
|
|
114
|
+
delete registry.plugins[key];
|
|
115
|
+
await writeFile(PLUGIN_REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if a plugin source is a directory-based plugin (has .claude-plugin/).
|
|
121
|
+
* @param {string} pluginPath - Path to the plugin directory
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function isDirectoryPlugin(pluginPath) {
|
|
125
|
+
return existsSync(join(pluginPath, '.claude-plugin', 'plugin.json'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Discover all available plugins (both legacy .md and new directory-based).
|
|
130
|
+
* @returns {Array<{name: string, type: 'legacy'|'directory', path: string}>}
|
|
131
|
+
*/
|
|
132
|
+
async function discoverPlugins() {
|
|
133
|
+
const plugins = [];
|
|
134
|
+
|
|
135
|
+
// Check new directory-based plugins first
|
|
136
|
+
if (existsSync(PLUGINS_DIR)) {
|
|
137
|
+
try {
|
|
138
|
+
const entries = await readdir(PLUGINS_DIR, { withFileTypes: true });
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (entry.isDirectory()) {
|
|
141
|
+
const pluginPath = join(PLUGINS_DIR, entry.name);
|
|
142
|
+
if (isDirectoryPlugin(pluginPath)) {
|
|
143
|
+
plugins.push({ name: entry.name, type: 'directory', path: pluginPath });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch { /* ignore */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fall back to legacy .md plugins if no directory plugins found
|
|
151
|
+
if (plugins.length === 0 && existsSync(LEGACY_PLUGINS_DIR)) {
|
|
152
|
+
try {
|
|
153
|
+
const files = (await readdir(LEGACY_PLUGINS_DIR)).filter(f => f.endsWith('.md'));
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
plugins.push({ name: file.replace(/\.md$/, ''), type: 'legacy', path: join(LEGACY_PLUGINS_DIR, file) });
|
|
156
|
+
}
|
|
157
|
+
} catch { /* ignore */ }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return plugins;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Compute a hash of a directory's contents for change detection.
|
|
165
|
+
* @param {string} dirPath - Path to the directory
|
|
166
|
+
* @returns {Promise<string>} MD5 hash of concatenated file contents
|
|
167
|
+
*/
|
|
168
|
+
async function directoryHash(dirPath) {
|
|
169
|
+
const hash = createHash('md5');
|
|
170
|
+
const entries = await readdir(dirPath, { withFileTypes: true, recursive: true });
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (!entry.isFile()) continue;
|
|
173
|
+
const parentDir = entry.parentPath || entry.path || dirPath;
|
|
174
|
+
const filePath = join(parentDir, entry.name);
|
|
175
|
+
const content = await readFile(filePath, 'utf8');
|
|
176
|
+
hash.update(entry.name + content);
|
|
177
|
+
}
|
|
178
|
+
return hash.digest('hex');
|
|
40
179
|
}
|
|
41
180
|
|
|
42
181
|
/**
|
|
@@ -51,8 +190,8 @@ async function promptForScope(pluginName) {
|
|
|
51
190
|
name: 'scope',
|
|
52
191
|
message: `Where should the "${pluginName}" command be installed?`,
|
|
53
192
|
choices: [
|
|
54
|
-
{ name: 'Project directory (.claude/
|
|
55
|
-
{ name: 'User home directory (~/.claude/
|
|
193
|
+
{ name: 'Project directory (.claude/plugins/)', value: 'project' },
|
|
194
|
+
{ name: 'User home directory (~/.claude/plugins/)', value: 'global' },
|
|
56
195
|
],
|
|
57
196
|
default: 'project',
|
|
58
197
|
}]);
|
|
@@ -78,33 +217,48 @@ async function resolvePluginScope(pluginName, options) {
|
|
|
78
217
|
|
|
79
218
|
/**
|
|
80
219
|
* Check if plugin exists in both project and global locations.
|
|
81
|
-
*
|
|
220
|
+
* Checks cache path, old direct path, and legacy (.claude/commands/) paths.
|
|
221
|
+
* @param {string} pluginName - Plugin name (without extension)
|
|
222
|
+
* @param {string} [legacyFileName] - Legacy filename (with .md) for backward compat
|
|
82
223
|
* @returns {Promise<{project: boolean, global: boolean}>}
|
|
83
224
|
*/
|
|
84
|
-
async function checkPluginLocations(
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
225
|
+
async function checkPluginLocations(pluginName, legacyFileName) {
|
|
226
|
+
const projectPlugin = join(getPluginsDir('project'), pluginName);
|
|
227
|
+
let project = await fileExists(projectPlugin);
|
|
228
|
+
|
|
229
|
+
// Check global: cache path first, then old direct path
|
|
230
|
+
const registry = await readPluginRegistry();
|
|
231
|
+
const key = `${pluginName}@${PLUGIN_MARKETPLACE}`;
|
|
232
|
+
let global = !!registry.plugins[key];
|
|
233
|
+
if (!global) {
|
|
234
|
+
// Check old direct path (~/.claude/plugins/<name>/)
|
|
235
|
+
global = await fileExists(join(CLAUDE_PLUGINS_DIR, pluginName, '.claude-plugin', 'plugin.json'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Also check legacy locations
|
|
239
|
+
if (legacyFileName) {
|
|
240
|
+
if (!project) project = await fileExists(join(getCommandsDir('project'), legacyFileName));
|
|
241
|
+
if (!global) global = await fileExists(join(getCommandsDir('global'), legacyFileName));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { project, global };
|
|
91
245
|
}
|
|
92
246
|
|
|
93
247
|
/**
|
|
94
248
|
* Handle conflict when plugin exists in both locations.
|
|
95
|
-
* @param {string}
|
|
249
|
+
* @param {string} pluginName - Plugin name
|
|
96
250
|
* @returns {Promise<'project' | 'global' | 'skip'>}
|
|
97
251
|
*/
|
|
98
|
-
async function handleDualLocation(
|
|
99
|
-
log.warn(
|
|
252
|
+
async function handleDualLocation(pluginName) {
|
|
253
|
+
log.warn(`"${pluginName}" exists in both project and global directories.`);
|
|
100
254
|
const inquirer = (await import('inquirer')).default;
|
|
101
255
|
const { target } = await inquirer.prompt([{
|
|
102
256
|
type: 'list',
|
|
103
257
|
name: 'target',
|
|
104
258
|
message: `Which location should be updated?`,
|
|
105
259
|
choices: [
|
|
106
|
-
{ name: 'Project (.claude/
|
|
107
|
-
{ name: 'Global (~/.claude/
|
|
260
|
+
{ name: 'Project (.claude/plugins/)', value: 'project' },
|
|
261
|
+
{ name: 'Global (~/.claude/plugins/)', value: 'global' },
|
|
108
262
|
{ name: 'Skip this plugin', value: 'skip' },
|
|
109
263
|
],
|
|
110
264
|
}]);
|
|
@@ -196,7 +350,7 @@ async function installInteractive(options = {}) {
|
|
|
196
350
|
message: 'What would you like to install?',
|
|
197
351
|
choices: [
|
|
198
352
|
{ name: 'DBO CLI (install or upgrade)', value: 'cli' },
|
|
199
|
-
{ name: 'Claude Code commands (adds /dbo to Claude Code)', value: 'claudecommands' },
|
|
353
|
+
{ name: 'Claude Code commands (adds /dbo:cli to Claude Code)', value: 'claudecommands' },
|
|
200
354
|
{ name: 'Claude Code CLI + commands', value: 'claudecode' },
|
|
201
355
|
{ name: 'Plugins (Claude commands)', value: 'plugins' },
|
|
202
356
|
],
|
|
@@ -404,17 +558,12 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
404
558
|
const cwd = process.cwd();
|
|
405
559
|
const hasProject = await isInitialized();
|
|
406
560
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
pluginFiles = (await readdir(PLUGINS_DIR)).filter(f => f.endsWith('.md'));
|
|
411
|
-
} catch {
|
|
412
|
-
log.error(`Plugin source directory not found: ${PLUGINS_DIR}`);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
561
|
+
// Discover all available plugins (directory-based or legacy)
|
|
562
|
+
const plugins = await discoverPlugins();
|
|
415
563
|
|
|
416
|
-
if (
|
|
417
|
-
log.
|
|
564
|
+
if (plugins.length === 0) {
|
|
565
|
+
log.error('No Claude plugins found in package.');
|
|
566
|
+
log.dim(` Searched: ${PLUGINS_DIR}`);
|
|
418
567
|
return;
|
|
419
568
|
}
|
|
420
569
|
|
|
@@ -423,23 +572,20 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
423
572
|
let upToDate = 0;
|
|
424
573
|
let skipped = 0;
|
|
425
574
|
|
|
426
|
-
for (const
|
|
427
|
-
const
|
|
428
|
-
const srcPath = join(PLUGINS_DIR, file);
|
|
429
|
-
const srcContent = await readFile(srcPath, 'utf8');
|
|
575
|
+
for (const plugin of plugins) {
|
|
576
|
+
const legacyFileName = `${plugin.name}.md`;
|
|
430
577
|
|
|
431
578
|
// Check if plugin exists in both locations
|
|
432
|
-
const locations = await checkPluginLocations(
|
|
579
|
+
const locations = await checkPluginLocations(plugin.name, legacyFileName);
|
|
433
580
|
let targetScope;
|
|
434
581
|
|
|
435
582
|
if (locations.project && locations.global) {
|
|
436
|
-
// Conflict — explicit flags resolve it, otherwise prompt
|
|
437
583
|
if (options.global) {
|
|
438
584
|
targetScope = 'global';
|
|
439
585
|
} else if (options.local) {
|
|
440
586
|
targetScope = 'project';
|
|
441
587
|
} else {
|
|
442
|
-
const choice = await handleDualLocation(
|
|
588
|
+
const choice = await handleDualLocation(plugin.name);
|
|
443
589
|
if (choice === 'skip') {
|
|
444
590
|
skipped++;
|
|
445
591
|
continue;
|
|
@@ -447,11 +593,10 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
447
593
|
targetScope = choice;
|
|
448
594
|
}
|
|
449
595
|
} else {
|
|
450
|
-
targetScope = await resolvePluginScope(
|
|
596
|
+
targetScope = await resolvePluginScope(plugin.name, options);
|
|
451
597
|
}
|
|
452
598
|
|
|
453
|
-
// Ensure target directory exists
|
|
454
|
-
const commandsDir = getCommandsDir(targetScope);
|
|
599
|
+
// Ensure target directory exists
|
|
455
600
|
if (targetScope === 'project' && !await fileExists(join(cwd, '.claude'))) {
|
|
456
601
|
const inquirer = (await import('inquirer')).default;
|
|
457
602
|
const { create } = await inquirer.prompt([{
|
|
@@ -464,90 +609,192 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
464
609
|
return;
|
|
465
610
|
}
|
|
466
611
|
}
|
|
467
|
-
await mkdir(commandsDir, { recursive: true });
|
|
468
612
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
613
|
+
if (plugin.type === 'directory') {
|
|
614
|
+
// New directory-based plugin installation
|
|
615
|
+
// Read version from plugin.json
|
|
616
|
+
const pluginMeta = JSON.parse(await readFile(join(plugin.path, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
617
|
+
const pluginVersion = pluginMeta.version || '0.0.0';
|
|
618
|
+
|
|
619
|
+
// For global scope, use the cache path and register in installed_plugins.json
|
|
620
|
+
const destDir = targetScope === 'global'
|
|
621
|
+
? getGlobalPluginCachePath(plugin.name, pluginVersion)
|
|
622
|
+
: join(getPluginsDir('project'), plugin.name);
|
|
623
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/plugins/' : '.claude/plugins/';
|
|
624
|
+
|
|
625
|
+
// Check for existing installation (cache path or old direct path)
|
|
626
|
+
const oldDirectPath = join(CLAUDE_PLUGINS_DIR, plugin.name);
|
|
627
|
+
const existingPath = await fileExists(destDir) ? destDir
|
|
628
|
+
: (targetScope === 'global' && await fileExists(oldDirectPath)) ? oldDirectPath
|
|
629
|
+
: null;
|
|
630
|
+
|
|
631
|
+
if (existingPath) {
|
|
632
|
+
// Check if upgrade needed via directory hash
|
|
633
|
+
const srcHash = await directoryHash(plugin.path);
|
|
634
|
+
const destHash = await directoryHash(existingPath);
|
|
635
|
+
if (srcHash === destHash && existingPath === destDir) {
|
|
636
|
+
upToDate++;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
477
639
|
|
|
478
|
-
|
|
479
|
-
|
|
640
|
+
const inquirer = (await import('inquirer')).default;
|
|
641
|
+
const { upgrade } = await inquirer.prompt([{
|
|
642
|
+
type: 'confirm', name: 'upgrade',
|
|
643
|
+
message: `${scopeLabel}${plugin.name}/ is already installed. Upgrade to latest version?`,
|
|
644
|
+
default: true,
|
|
645
|
+
}]);
|
|
646
|
+
if (!upgrade) {
|
|
647
|
+
log.dim(` Skipped ${plugin.name}`);
|
|
648
|
+
skipped++;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
480
651
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
652
|
+
await mkdir(destDir, { recursive: true });
|
|
653
|
+
await cp(plugin.path, destDir, { recursive: true, force: true });
|
|
654
|
+
|
|
655
|
+
// Clean up old direct path if migrating to cache path
|
|
656
|
+
if (targetScope === 'global' && existingPath === oldDirectPath && existingPath !== destDir) {
|
|
657
|
+
await rm(oldDirectPath, { recursive: true, force: true });
|
|
658
|
+
log.dim(` Migrated from ${oldDirectPath} to cache path`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (targetScope === 'global') {
|
|
662
|
+
await registerPlugin(plugin.name, pluginVersion, destDir);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
log.success(`Upgraded ${scopeLabel}${plugin.name}/`);
|
|
666
|
+
updated++;
|
|
667
|
+
} else {
|
|
668
|
+
await mkdir(destDir, { recursive: true });
|
|
669
|
+
await cp(plugin.path, destDir, { recursive: true });
|
|
670
|
+
|
|
671
|
+
if (targetScope === 'global') {
|
|
672
|
+
await registerPlugin(plugin.name, pluginVersion, destDir);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
log.success(`Installed ${scopeLabel}${plugin.name}/`);
|
|
676
|
+
installed++;
|
|
677
|
+
|
|
678
|
+
if (targetScope === 'project') {
|
|
679
|
+
await addToGitignore(cwd, `.claude/plugins/${plugin.name}/`);
|
|
680
|
+
}
|
|
487
681
|
}
|
|
488
682
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (!
|
|
497
|
-
log.
|
|
498
|
-
skipped++;
|
|
499
|
-
continue;
|
|
683
|
+
// Persist scope + metadata to config.local.json
|
|
684
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(plugin.name))) {
|
|
685
|
+
await setPluginScope(plugin.name, {
|
|
686
|
+
scope: targetScope === 'global' ? 'user' : 'project',
|
|
687
|
+
installPath: destDir,
|
|
688
|
+
version: pluginVersion,
|
|
689
|
+
});
|
|
690
|
+
} else if (targetScope === 'global' && !hasProject) {
|
|
691
|
+
log.warn(`Cannot persist scope preference (no .dbo/ directory). Run "dbo init" first.`);
|
|
500
692
|
}
|
|
501
693
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
694
|
+
// Clean up legacy file if it exists
|
|
695
|
+
const legacyProjectPath = join(getCommandsDir('project'), legacyFileName);
|
|
696
|
+
const legacyGlobalPath = join(getCommandsDir('global'), legacyFileName);
|
|
697
|
+
if (await fileExists(legacyProjectPath)) {
|
|
698
|
+
const { unlink } = await import('fs/promises');
|
|
699
|
+
await unlink(legacyProjectPath);
|
|
700
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
701
|
+
log.dim(` Removed legacy command: .claude/commands/${legacyFileName}`);
|
|
702
|
+
}
|
|
703
|
+
if (await fileExists(legacyGlobalPath)) {
|
|
704
|
+
const { unlink } = await import('fs/promises');
|
|
705
|
+
await unlink(legacyGlobalPath);
|
|
706
|
+
log.dim(` Removed legacy command: ~/.claude/commands/${legacyFileName}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (targetScope === 'global' && locations.project) {
|
|
710
|
+
await removeFromGitignore(`.claude/plugins/${plugin.name}/`);
|
|
711
|
+
}
|
|
505
712
|
} else {
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
713
|
+
// Legacy .md plugin installation
|
|
714
|
+
const commandsDir = getCommandsDir(targetScope);
|
|
715
|
+
await mkdir(commandsDir, { recursive: true });
|
|
716
|
+
|
|
717
|
+
const srcContent = await readFile(plugin.path, 'utf8');
|
|
718
|
+
const destPath = join(commandsDir, legacyFileName);
|
|
719
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/commands/' : '.claude/commands/';
|
|
720
|
+
|
|
721
|
+
if (await fileExists(destPath)) {
|
|
722
|
+
const destContent = await readFile(destPath, 'utf8');
|
|
723
|
+
if (fileHash(srcContent) === fileHash(destContent)) {
|
|
724
|
+
upToDate++;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
510
727
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
728
|
+
const inquirer = (await import('inquirer')).default;
|
|
729
|
+
const { upgrade } = await inquirer.prompt([{
|
|
730
|
+
type: 'confirm', name: 'upgrade',
|
|
731
|
+
message: `${scopeLabel}${legacyFileName} is already installed. Upgrade to latest version?`,
|
|
732
|
+
default: true,
|
|
733
|
+
}]);
|
|
734
|
+
if (!upgrade) {
|
|
735
|
+
log.dim(` Skipped ${legacyFileName}`);
|
|
736
|
+
skipped++;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
await copyFile(plugin.path, destPath);
|
|
741
|
+
log.success(`Upgraded ${scopeLabel}${legacyFileName}`);
|
|
742
|
+
updated++;
|
|
743
|
+
} else {
|
|
744
|
+
await copyFile(plugin.path, destPath);
|
|
745
|
+
log.success(`Installed ${scopeLabel}${legacyFileName}`);
|
|
746
|
+
installed++;
|
|
747
|
+
|
|
748
|
+
if (targetScope === 'project') {
|
|
749
|
+
await addToGitignore(cwd, `.claude/commands/${legacyFileName}`);
|
|
750
|
+
}
|
|
514
751
|
}
|
|
515
|
-
}
|
|
516
752
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
753
|
+
if (targetScope === 'global' && locations.project) {
|
|
754
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Persist scope for legacy plugins
|
|
758
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(plugin.name))) {
|
|
759
|
+
await setPluginScope(plugin.name, targetScope);
|
|
760
|
+
}
|
|
520
761
|
}
|
|
521
762
|
}
|
|
522
763
|
|
|
523
|
-
if (installed > 0) log.info(`${installed}
|
|
524
|
-
if (updated > 0) log.info(`${updated}
|
|
525
|
-
if (upToDate > 0) log.dim(`${upToDate}
|
|
526
|
-
if (skipped > 0) log.dim(`${skipped}
|
|
764
|
+
if (installed > 0) log.info(`${installed} plugin(s) installed.`);
|
|
765
|
+
if (updated > 0) log.info(`${updated} plugin(s) upgraded.`);
|
|
766
|
+
if (upToDate > 0) log.dim(`${upToDate} plugin(s) already up to date.`);
|
|
767
|
+
if (skipped > 0) log.dim(`${skipped} plugin(s) skipped.`);
|
|
527
768
|
if (installed > 0 || updated > 0) {
|
|
528
|
-
log.info('Use /dbo in Claude Code.');
|
|
529
|
-
log.warn('Note:
|
|
769
|
+
log.info('Use /dbo:cli in Claude Code.');
|
|
770
|
+
log.warn('Note: Plugins will be available in new Claude Code sessions (restart any active session).');
|
|
530
771
|
}
|
|
531
772
|
}
|
|
532
773
|
|
|
533
774
|
// ─── Specific Command ───────────────────────────────────────────────────────
|
|
534
775
|
|
|
535
776
|
async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
777
|
+
const pluginName = name.replace(/\.md$/, '');
|
|
778
|
+
|
|
779
|
+
// Find the plugin in available sources (directory-based first, then legacy)
|
|
780
|
+
const plugins = await discoverPlugins();
|
|
781
|
+
const plugin = plugins.find(p => p.name === pluginName);
|
|
782
|
+
|
|
783
|
+
if (!plugin) {
|
|
784
|
+
log.error(`Plugin "${pluginName}" not found.`);
|
|
785
|
+
if (plugins.length > 0) {
|
|
786
|
+
log.dim(` Available: ${plugins.map(p => p.name).join(', ')}`);
|
|
787
|
+
} else {
|
|
788
|
+
log.dim(` Searched: ${PLUGINS_DIR}`);
|
|
789
|
+
}
|
|
544
790
|
return;
|
|
545
791
|
}
|
|
546
792
|
|
|
547
793
|
const hasProject = await isInitialized();
|
|
794
|
+
const legacyFileName = `${pluginName}.md`;
|
|
548
795
|
|
|
549
796
|
// Check for dual location
|
|
550
|
-
const locations = await checkPluginLocations(
|
|
797
|
+
const locations = await checkPluginLocations(pluginName, legacyFileName);
|
|
551
798
|
let targetScope;
|
|
552
799
|
|
|
553
800
|
if (locations.project && locations.global) {
|
|
@@ -556,7 +803,7 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
556
803
|
} else if (options.local) {
|
|
557
804
|
targetScope = 'project';
|
|
558
805
|
} else {
|
|
559
|
-
const choice = await handleDualLocation(
|
|
806
|
+
const choice = await handleDualLocation(pluginName);
|
|
560
807
|
if (choice === 'skip') return;
|
|
561
808
|
targetScope = choice;
|
|
562
809
|
}
|
|
@@ -564,54 +811,142 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
564
811
|
targetScope = await resolvePluginScope(pluginName, options);
|
|
565
812
|
}
|
|
566
813
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
814
|
+
if (plugin.type === 'directory') {
|
|
815
|
+
// Directory-based plugin installation
|
|
816
|
+
const pluginMeta = JSON.parse(await readFile(join(plugin.path, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
817
|
+
const pluginVersion = pluginMeta.version || '0.0.0';
|
|
818
|
+
|
|
819
|
+
const destDir = targetScope === 'global'
|
|
820
|
+
? getGlobalPluginCachePath(pluginName, pluginVersion)
|
|
821
|
+
: join(getPluginsDir('project'), pluginName);
|
|
822
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/plugins/' : '.claude/plugins/';
|
|
823
|
+
|
|
824
|
+
// Check for existing installation (cache path or old direct path)
|
|
825
|
+
const oldDirectPath = join(CLAUDE_PLUGINS_DIR, pluginName);
|
|
826
|
+
const existingPath = await fileExists(destDir) ? destDir
|
|
827
|
+
: (targetScope === 'global' && await fileExists(oldDirectPath)) ? oldDirectPath
|
|
828
|
+
: null;
|
|
829
|
+
|
|
830
|
+
if (existingPath) {
|
|
831
|
+
const srcHash = await directoryHash(plugin.path);
|
|
832
|
+
const destHash = await directoryHash(existingPath);
|
|
833
|
+
if (srcHash === destHash && existingPath === destDir) {
|
|
834
|
+
log.success(`${pluginName} is already up to date in ${scopeLabel}`);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
575
837
|
|
|
576
|
-
|
|
577
|
-
|
|
838
|
+
const inquirer = (await import('inquirer')).default;
|
|
839
|
+
const { upgrade } = await inquirer.prompt([{
|
|
840
|
+
type: 'confirm', name: 'upgrade',
|
|
841
|
+
message: `${scopeLabel}${pluginName}/ is already installed. Upgrade to latest version?`,
|
|
842
|
+
default: true,
|
|
843
|
+
}]);
|
|
844
|
+
if (!upgrade) return;
|
|
578
845
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const scopeLabel = targetScope === 'global' ? '~/.claude/commands/' : '.claude/commands/';
|
|
846
|
+
await mkdir(destDir, { recursive: true });
|
|
847
|
+
await cp(plugin.path, destDir, { recursive: true, force: true });
|
|
582
848
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
849
|
+
// Clean up old direct path if migrating to cache path
|
|
850
|
+
if (targetScope === 'global' && existingPath === oldDirectPath && existingPath !== destDir) {
|
|
851
|
+
await rm(oldDirectPath, { recursive: true, force: true });
|
|
852
|
+
log.dim(` Migrated from ${oldDirectPath} to cache path`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (targetScope === 'global') {
|
|
856
|
+
await registerPlugin(pluginName, pluginVersion, destDir);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
log.success(`Upgraded ${scopeLabel}${pluginName}/`);
|
|
860
|
+
} else {
|
|
861
|
+
await mkdir(destDir, { recursive: true });
|
|
862
|
+
await cp(plugin.path, destDir, { recursive: true });
|
|
863
|
+
|
|
864
|
+
if (targetScope === 'global') {
|
|
865
|
+
await registerPlugin(pluginName, pluginVersion, destDir);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
log.success(`Installed ${scopeLabel}${pluginName}/`);
|
|
869
|
+
|
|
870
|
+
if (targetScope === 'project') {
|
|
871
|
+
await addToGitignore(process.cwd(), `.claude/plugins/${pluginName}/`);
|
|
872
|
+
}
|
|
588
873
|
}
|
|
589
874
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
875
|
+
// Persist scope + metadata to config.local.json
|
|
876
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(pluginName))) {
|
|
877
|
+
await setPluginScope(pluginName, {
|
|
878
|
+
scope: targetScope === 'global' ? 'user' : 'project',
|
|
879
|
+
installPath: destDir,
|
|
880
|
+
version: pluginVersion,
|
|
881
|
+
});
|
|
882
|
+
} else if (targetScope === 'global' && !hasProject) {
|
|
883
|
+
log.warn(`Cannot persist scope preference (no .dbo/ directory). Run "dbo init" first.`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Clean up legacy files
|
|
887
|
+
const legacyProjectPath = join(getCommandsDir('project'), legacyFileName);
|
|
888
|
+
const legacyGlobalPath = join(getCommandsDir('global'), legacyFileName);
|
|
889
|
+
if (await fileExists(legacyProjectPath)) {
|
|
890
|
+
const { unlink } = await import('fs/promises');
|
|
891
|
+
await unlink(legacyProjectPath);
|
|
892
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
893
|
+
log.dim(` Removed legacy command: .claude/commands/${legacyFileName}`);
|
|
894
|
+
}
|
|
895
|
+
if (await fileExists(legacyGlobalPath)) {
|
|
896
|
+
const { unlink } = await import('fs/promises');
|
|
897
|
+
await unlink(legacyGlobalPath);
|
|
898
|
+
log.dim(` Removed legacy command: ~/.claude/commands/${legacyFileName}`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (targetScope === 'global' && locations.project) {
|
|
902
|
+
await removeFromGitignore(`.claude/plugins/${pluginName}/`);
|
|
903
|
+
}
|
|
600
904
|
} else {
|
|
601
|
-
|
|
602
|
-
|
|
905
|
+
// Legacy .md plugin installation
|
|
906
|
+
const commandsDir = getCommandsDir(targetScope);
|
|
907
|
+
await mkdir(commandsDir, { recursive: true });
|
|
908
|
+
|
|
909
|
+
const destPath = join(commandsDir, legacyFileName);
|
|
910
|
+
const srcContent = await readFile(plugin.path, 'utf8');
|
|
911
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/commands/' : '.claude/commands/';
|
|
912
|
+
|
|
913
|
+
if (await fileExists(destPath)) {
|
|
914
|
+
const destContent = await readFile(destPath, 'utf8');
|
|
915
|
+
if (fileHash(srcContent) === fileHash(destContent)) {
|
|
916
|
+
log.success(`${legacyFileName} is already up to date in ${scopeLabel}`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const inquirer = (await import('inquirer')).default;
|
|
921
|
+
const { upgrade } = await inquirer.prompt([{
|
|
922
|
+
type: 'confirm', name: 'upgrade',
|
|
923
|
+
message: `${scopeLabel}${legacyFileName} is already installed. Upgrade to latest version?`,
|
|
924
|
+
default: true,
|
|
925
|
+
}]);
|
|
926
|
+
if (!upgrade) return;
|
|
603
927
|
|
|
604
|
-
|
|
605
|
-
|
|
928
|
+
await copyFile(plugin.path, destPath);
|
|
929
|
+
log.success(`Upgraded ${scopeLabel}${legacyFileName}`);
|
|
930
|
+
} else {
|
|
931
|
+
await copyFile(plugin.path, destPath);
|
|
932
|
+
log.success(`Installed ${scopeLabel}${legacyFileName}`);
|
|
933
|
+
|
|
934
|
+
if (targetScope === 'project') {
|
|
935
|
+
await addToGitignore(process.cwd(), `.claude/commands/${legacyFileName}`);
|
|
936
|
+
}
|
|
606
937
|
}
|
|
607
|
-
}
|
|
608
938
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
939
|
+
if (targetScope === 'global' && locations.project) {
|
|
940
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Persist scope for legacy plugins
|
|
944
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(pluginName))) {
|
|
945
|
+
await setPluginScope(pluginName, targetScope);
|
|
946
|
+
}
|
|
612
947
|
}
|
|
613
948
|
|
|
614
|
-
log.warn('Note:
|
|
949
|
+
log.warn('Note: Plugins will be available in new Claude Code sessions (restart any active session).');
|
|
615
950
|
}
|
|
616
951
|
|
|
617
952
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
package/src/lib/config.js
CHANGED
|
@@ -420,38 +420,81 @@ export async function saveLocalConfig(data) {
|
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
/**
|
|
423
|
-
* Get the stored scope for a plugin
|
|
424
|
-
*
|
|
425
|
-
*
|
|
423
|
+
* Get the stored scope for a plugin.
|
|
424
|
+
* Reads from the registry-style format: plugins["name@marketplace"][0].scope
|
|
425
|
+
* Falls back to legacy format: plugins.claudecommands.name
|
|
426
|
+
* @param {string} pluginName - Plugin name (without extension)
|
|
427
|
+
* @param {string} [marketplace='dboio'] - Marketplace identifier
|
|
426
428
|
* @returns {Promise<'project' | 'global' | null>}
|
|
427
429
|
*/
|
|
428
|
-
export async function getPluginScope(pluginName,
|
|
430
|
+
export async function getPluginScope(pluginName, marketplace = 'dboio') {
|
|
429
431
|
const config = await loadLocalConfig();
|
|
430
|
-
|
|
432
|
+
const key = `${pluginName}@${marketplace}`;
|
|
433
|
+
const entry = config.plugins?.[key]?.[0];
|
|
434
|
+
if (entry) {
|
|
435
|
+
return entry.scope === 'user' ? 'global' : entry.scope || null;
|
|
436
|
+
}
|
|
437
|
+
// Legacy fallback: plugins.claudecommands.dbo = "global"
|
|
438
|
+
const legacy = config.plugins?.claudecommands?.[pluginName];
|
|
439
|
+
return legacy || null;
|
|
431
440
|
}
|
|
432
441
|
|
|
433
442
|
/**
|
|
434
|
-
* Set
|
|
435
|
-
* @param {string} pluginName - Plugin name
|
|
436
|
-
* @param {
|
|
437
|
-
* @param {string} [
|
|
443
|
+
* Set plugin metadata in registry-style format.
|
|
444
|
+
* @param {string} pluginName - Plugin name
|
|
445
|
+
* @param {object} meta - Plugin metadata { scope, installPath, version }
|
|
446
|
+
* @param {string} [marketplace='dboio'] - Marketplace identifier
|
|
438
447
|
*/
|
|
439
|
-
export async function setPluginScope(pluginName,
|
|
448
|
+
export async function setPluginScope(pluginName, meta, marketplace = 'dboio') {
|
|
440
449
|
const config = await loadLocalConfig();
|
|
441
450
|
if (!config.plugins) config.plugins = {};
|
|
442
|
-
|
|
443
|
-
|
|
451
|
+
const key = `${pluginName}@${marketplace}`;
|
|
452
|
+
const now = new Date().toISOString();
|
|
453
|
+
|
|
454
|
+
// Support legacy callers passing just a scope string
|
|
455
|
+
if (typeof meta === 'string') {
|
|
456
|
+
meta = { scope: meta === 'global' ? 'user' : meta };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const existing = config.plugins[key]?.[0];
|
|
460
|
+
if (existing) {
|
|
461
|
+
Object.assign(existing, meta);
|
|
462
|
+
existing.lastUpdated = now;
|
|
463
|
+
} else {
|
|
464
|
+
config.plugins[key] = [{
|
|
465
|
+
scope: meta.scope || 'user',
|
|
466
|
+
installPath: meta.installPath || null,
|
|
467
|
+
version: meta.version || null,
|
|
468
|
+
installedAt: now,
|
|
469
|
+
lastUpdated: now,
|
|
470
|
+
}];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Remove legacy entry if present
|
|
474
|
+
if (config.plugins.claudecommands?.[pluginName]) {
|
|
475
|
+
delete config.plugins.claudecommands[pluginName];
|
|
476
|
+
if (Object.keys(config.plugins.claudecommands).length === 0) {
|
|
477
|
+
delete config.plugins.claudecommands;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
444
481
|
await saveLocalConfig(config);
|
|
445
482
|
}
|
|
446
483
|
|
|
447
484
|
/**
|
|
448
|
-
* Get all stored plugin
|
|
449
|
-
* Returns object mapping
|
|
450
|
-
* @param {string} [category='claudecommands'] - Plugin category
|
|
485
|
+
* Get all stored plugin entries.
|
|
486
|
+
* Returns object mapping registry keys to their entry arrays.
|
|
451
487
|
*/
|
|
452
|
-
export async function getAllPluginScopes(
|
|
488
|
+
export async function getAllPluginScopes() {
|
|
453
489
|
const config = await loadLocalConfig();
|
|
454
|
-
|
|
490
|
+
const result = {};
|
|
491
|
+
if (!config.plugins) return result;
|
|
492
|
+
for (const [key, value] of Object.entries(config.plugins)) {
|
|
493
|
+
// Skip legacy category keys
|
|
494
|
+
if (typeof value === 'object' && !Array.isArray(value) && !value.scope) continue;
|
|
495
|
+
result[key] = value;
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
455
498
|
}
|
|
456
499
|
|
|
457
500
|
// ─── Gitignore ────────────────────────────────────────────────────────────
|