@dboio/cli 0.6.2 → 0.6.4
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 +36 -0
- package/bin/postinstall.js +3 -13
- package/package.json +2 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +13 -0
- package/{src/plugins/claudecommands/dbo.md → plugins/claude/dbo/skills/cli/SKILL.md} +3 -2
- package/src/commands/install.js +309 -123
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
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
5
8
|
|
|
6
9
|
const require = createRequire(import.meta.url);
|
|
7
10
|
const packageJson = require('../package.json');
|
|
@@ -28,6 +31,36 @@ import { diffCommand } from '../src/commands/diff.js';
|
|
|
28
31
|
import { rmCommand } from '../src/commands/rm.js';
|
|
29
32
|
import { mvCommand } from '../src/commands/mv.js';
|
|
30
33
|
|
|
34
|
+
// First-run welcome message
|
|
35
|
+
function checkFirstRun() {
|
|
36
|
+
const markerFile = join(homedir(), '.dbo-cli-welcomed');
|
|
37
|
+
|
|
38
|
+
if (!existsSync(markerFile)) {
|
|
39
|
+
const RESET = '\x1b[0m';
|
|
40
|
+
const BOLD = '\x1b[1m';
|
|
41
|
+
const DIM = '\x1b[2m';
|
|
42
|
+
const CYAN = '\x1b[36m';
|
|
43
|
+
const YELLOW = '\x1b[33m';
|
|
44
|
+
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(`${BOLD}${CYAN} Welcome to DBO.io CLI!${RESET}`);
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(` ${DIM}Available plugins:${RESET}`);
|
|
49
|
+
console.log(` ${YELLOW}Claude Code — /dbo:cli slash command${RESET}`);
|
|
50
|
+
console.log(` ${DIM}dbo install --claudecommand dbo${RESET}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(` To install all plugins at once, run:`);
|
|
53
|
+
console.log(` ${BOLD}dbo install plugins${RESET}`);
|
|
54
|
+
console.log('');
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(markerFile, new Date().toISOString());
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore write errors
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
const program = new Command();
|
|
32
65
|
|
|
33
66
|
program
|
|
@@ -57,4 +90,7 @@ program.addCommand(diffCommand);
|
|
|
57
90
|
program.addCommand(rmCommand);
|
|
58
91
|
program.addCommand(mvCommand);
|
|
59
92
|
|
|
93
|
+
// Show welcome message on first run
|
|
94
|
+
checkFirstRun();
|
|
95
|
+
|
|
60
96
|
program.parse();
|
package/bin/postinstall.js
CHANGED
|
@@ -3,32 +3,22 @@
|
|
|
3
3
|
// Post-install hook for `npm i @dboio/cli`
|
|
4
4
|
// Interactive plugin picker when TTY is available, static message otherwise.
|
|
5
5
|
|
|
6
|
-
import { openSync, writeSync, closeSync } from 'fs';
|
|
7
|
-
|
|
8
6
|
const RESET = '\x1b[0m';
|
|
9
7
|
const BOLD = '\x1b[1m';
|
|
10
8
|
const DIM = '\x1b[2m';
|
|
11
9
|
const CYAN = '\x1b[36m';
|
|
12
10
|
const YELLOW = '\x1b[33m';
|
|
13
11
|
|
|
14
|
-
//
|
|
12
|
+
// Simple log function - console.log works fine in postinstall
|
|
15
13
|
function write(message) {
|
|
16
|
-
|
|
17
|
-
// Try to write directly to /dev/tty (works on Unix-like systems)
|
|
18
|
-
const fd = openSync('/dev/tty', 'w');
|
|
19
|
-
writeSync(fd, message);
|
|
20
|
-
closeSync(fd);
|
|
21
|
-
} catch {
|
|
22
|
-
// Fallback to console.error if /dev/tty is not available
|
|
23
|
-
console.error(message);
|
|
24
|
-
}
|
|
14
|
+
console.log(message);
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
// Available plugins — add new entries here as plugins are created
|
|
28
18
|
const PLUGINS = [
|
|
29
19
|
{
|
|
30
20
|
name: 'dbo',
|
|
31
|
-
label: 'Claude Code — /dbo slash command',
|
|
21
|
+
label: 'Claude Code — /dbo:cli slash command',
|
|
32
22
|
command: 'dbo install --claudecommand dbo'
|
|
33
23
|
}
|
|
34
24
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dboio/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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,18 @@
|
|
|
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 } 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
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const CLI_ROOT = join(__dirname, '..', '..');
|
|
13
|
-
const
|
|
14
|
+
const LEGACY_PLUGINS_DIR = join(__dirname, '..', 'plugins', 'claudecommands');
|
|
15
|
+
const PLUGINS_DIR = join(CLI_ROOT, 'plugins', 'claude');
|
|
14
16
|
|
|
15
17
|
async function fileExists(path) {
|
|
16
18
|
try { await access(path); return true; } catch { return false; }
|
|
@@ -21,7 +23,7 @@ function fileHash(content) {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
* Get the target commands directory based on scope.
|
|
26
|
+
* Get the target commands directory based on scope (legacy .md format).
|
|
25
27
|
* @param {'project' | 'global'} scope
|
|
26
28
|
* @returns {string} Absolute path to commands directory
|
|
27
29
|
*/
|
|
@@ -33,10 +35,77 @@ function getCommandsDir(scope) {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
|
-
* Get
|
|
38
|
+
* Get the target plugins directory based on scope (new directory-based format).
|
|
39
|
+
* @param {'project' | 'global'} scope
|
|
40
|
+
* @returns {string} Absolute path to plugins directory
|
|
41
|
+
*/
|
|
42
|
+
function getPluginsDir(scope) {
|
|
43
|
+
if (scope === 'global') {
|
|
44
|
+
return join(homedir(), '.claude', 'plugins');
|
|
45
|
+
}
|
|
46
|
+
return join(process.cwd(), '.claude', 'plugins');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a plugin source is a directory-based plugin (has .claude-plugin/).
|
|
51
|
+
* @param {string} pluginPath - Path to the plugin directory
|
|
52
|
+
* @returns {boolean}
|
|
37
53
|
*/
|
|
38
|
-
function
|
|
39
|
-
return
|
|
54
|
+
function isDirectoryPlugin(pluginPath) {
|
|
55
|
+
return existsSync(join(pluginPath, '.claude-plugin', 'plugin.json'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Discover all available plugins (both legacy .md and new directory-based).
|
|
60
|
+
* @returns {Array<{name: string, type: 'legacy'|'directory', path: string}>}
|
|
61
|
+
*/
|
|
62
|
+
async function discoverPlugins() {
|
|
63
|
+
const plugins = [];
|
|
64
|
+
|
|
65
|
+
// Check new directory-based plugins first
|
|
66
|
+
if (existsSync(PLUGINS_DIR)) {
|
|
67
|
+
try {
|
|
68
|
+
const entries = await readdir(PLUGINS_DIR, { withFileTypes: true });
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
const pluginPath = join(PLUGINS_DIR, entry.name);
|
|
72
|
+
if (isDirectoryPlugin(pluginPath)) {
|
|
73
|
+
plugins.push({ name: entry.name, type: 'directory', path: pluginPath });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fall back to legacy .md plugins if no directory plugins found
|
|
81
|
+
if (plugins.length === 0 && existsSync(LEGACY_PLUGINS_DIR)) {
|
|
82
|
+
try {
|
|
83
|
+
const files = (await readdir(LEGACY_PLUGINS_DIR)).filter(f => f.endsWith('.md'));
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
plugins.push({ name: file.replace(/\.md$/, ''), type: 'legacy', path: join(LEGACY_PLUGINS_DIR, file) });
|
|
86
|
+
}
|
|
87
|
+
} catch { /* ignore */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return plugins;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compute a hash of a directory's contents for change detection.
|
|
95
|
+
* @param {string} dirPath - Path to the directory
|
|
96
|
+
* @returns {Promise<string>} MD5 hash of concatenated file contents
|
|
97
|
+
*/
|
|
98
|
+
async function directoryHash(dirPath) {
|
|
99
|
+
const hash = createHash('md5');
|
|
100
|
+
const entries = await readdir(dirPath, { withFileTypes: true, recursive: true });
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.isFile()) continue;
|
|
103
|
+
const parentDir = entry.parentPath || entry.path || dirPath;
|
|
104
|
+
const filePath = join(parentDir, entry.name);
|
|
105
|
+
const content = await readFile(filePath, 'utf8');
|
|
106
|
+
hash.update(entry.name + content);
|
|
107
|
+
}
|
|
108
|
+
return hash.digest('hex');
|
|
40
109
|
}
|
|
41
110
|
|
|
42
111
|
/**
|
|
@@ -51,8 +120,8 @@ async function promptForScope(pluginName) {
|
|
|
51
120
|
name: 'scope',
|
|
52
121
|
message: `Where should the "${pluginName}" command be installed?`,
|
|
53
122
|
choices: [
|
|
54
|
-
{ name: 'Project directory (.claude/
|
|
55
|
-
{ name: 'User home directory (~/.claude/
|
|
123
|
+
{ name: 'Project directory (.claude/plugins/)', value: 'project' },
|
|
124
|
+
{ name: 'User home directory (~/.claude/plugins/)', value: 'global' },
|
|
56
125
|
],
|
|
57
126
|
default: 'project',
|
|
58
127
|
}]);
|
|
@@ -78,33 +147,41 @@ async function resolvePluginScope(pluginName, options) {
|
|
|
78
147
|
|
|
79
148
|
/**
|
|
80
149
|
* Check if plugin exists in both project and global locations.
|
|
81
|
-
*
|
|
150
|
+
* Checks both legacy (.claude/commands/) and new (.claude/plugins/) paths.
|
|
151
|
+
* @param {string} pluginName - Plugin name (without extension)
|
|
152
|
+
* @param {string} [legacyFileName] - Legacy filename (with .md) for backward compat
|
|
82
153
|
* @returns {Promise<{project: boolean, global: boolean}>}
|
|
83
154
|
*/
|
|
84
|
-
async function checkPluginLocations(
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
155
|
+
async function checkPluginLocations(pluginName, legacyFileName) {
|
|
156
|
+
const projectPlugin = join(getPluginsDir('project'), pluginName);
|
|
157
|
+
const globalPlugin = join(getPluginsDir('global'), pluginName);
|
|
158
|
+
let project = await fileExists(projectPlugin);
|
|
159
|
+
let global = await fileExists(globalPlugin);
|
|
160
|
+
|
|
161
|
+
// Also check legacy locations
|
|
162
|
+
if (legacyFileName) {
|
|
163
|
+
if (!project) project = await fileExists(join(getCommandsDir('project'), legacyFileName));
|
|
164
|
+
if (!global) global = await fileExists(join(getCommandsDir('global'), legacyFileName));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { project, global };
|
|
91
168
|
}
|
|
92
169
|
|
|
93
170
|
/**
|
|
94
171
|
* Handle conflict when plugin exists in both locations.
|
|
95
|
-
* @param {string}
|
|
172
|
+
* @param {string} pluginName - Plugin name
|
|
96
173
|
* @returns {Promise<'project' | 'global' | 'skip'>}
|
|
97
174
|
*/
|
|
98
|
-
async function handleDualLocation(
|
|
99
|
-
log.warn(
|
|
175
|
+
async function handleDualLocation(pluginName) {
|
|
176
|
+
log.warn(`"${pluginName}" exists in both project and global directories.`);
|
|
100
177
|
const inquirer = (await import('inquirer')).default;
|
|
101
178
|
const { target } = await inquirer.prompt([{
|
|
102
179
|
type: 'list',
|
|
103
180
|
name: 'target',
|
|
104
181
|
message: `Which location should be updated?`,
|
|
105
182
|
choices: [
|
|
106
|
-
{ name: 'Project (.claude/
|
|
107
|
-
{ name: 'Global (~/.claude/
|
|
183
|
+
{ name: 'Project (.claude/plugins/)', value: 'project' },
|
|
184
|
+
{ name: 'Global (~/.claude/plugins/)', value: 'global' },
|
|
108
185
|
{ name: 'Skip this plugin', value: 'skip' },
|
|
109
186
|
],
|
|
110
187
|
}]);
|
|
@@ -196,7 +273,7 @@ async function installInteractive(options = {}) {
|
|
|
196
273
|
message: 'What would you like to install?',
|
|
197
274
|
choices: [
|
|
198
275
|
{ name: 'DBO CLI (install or upgrade)', value: 'cli' },
|
|
199
|
-
{ name: 'Claude Code commands (adds /dbo to Claude Code)', value: 'claudecommands' },
|
|
276
|
+
{ name: 'Claude Code commands (adds /dbo:cli to Claude Code)', value: 'claudecommands' },
|
|
200
277
|
{ name: 'Claude Code CLI + commands', value: 'claudecode' },
|
|
201
278
|
{ name: 'Plugins (Claude commands)', value: 'plugins' },
|
|
202
279
|
],
|
|
@@ -404,17 +481,12 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
404
481
|
const cwd = process.cwd();
|
|
405
482
|
const hasProject = await isInitialized();
|
|
406
483
|
|
|
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
|
-
}
|
|
484
|
+
// Discover all available plugins (directory-based or legacy)
|
|
485
|
+
const plugins = await discoverPlugins();
|
|
415
486
|
|
|
416
|
-
if (
|
|
417
|
-
log.
|
|
487
|
+
if (plugins.length === 0) {
|
|
488
|
+
log.error('No Claude plugins found in package.');
|
|
489
|
+
log.dim(` Searched: ${PLUGINS_DIR}`);
|
|
418
490
|
return;
|
|
419
491
|
}
|
|
420
492
|
|
|
@@ -423,23 +495,20 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
423
495
|
let upToDate = 0;
|
|
424
496
|
let skipped = 0;
|
|
425
497
|
|
|
426
|
-
for (const
|
|
427
|
-
const
|
|
428
|
-
const srcPath = join(PLUGINS_DIR, file);
|
|
429
|
-
const srcContent = await readFile(srcPath, 'utf8');
|
|
498
|
+
for (const plugin of plugins) {
|
|
499
|
+
const legacyFileName = `${plugin.name}.md`;
|
|
430
500
|
|
|
431
501
|
// Check if plugin exists in both locations
|
|
432
|
-
const locations = await checkPluginLocations(
|
|
502
|
+
const locations = await checkPluginLocations(plugin.name, legacyFileName);
|
|
433
503
|
let targetScope;
|
|
434
504
|
|
|
435
505
|
if (locations.project && locations.global) {
|
|
436
|
-
// Conflict — explicit flags resolve it, otherwise prompt
|
|
437
506
|
if (options.global) {
|
|
438
507
|
targetScope = 'global';
|
|
439
508
|
} else if (options.local) {
|
|
440
509
|
targetScope = 'project';
|
|
441
510
|
} else {
|
|
442
|
-
const choice = await handleDualLocation(
|
|
511
|
+
const choice = await handleDualLocation(plugin.name);
|
|
443
512
|
if (choice === 'skip') {
|
|
444
513
|
skipped++;
|
|
445
514
|
continue;
|
|
@@ -447,11 +516,10 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
447
516
|
targetScope = choice;
|
|
448
517
|
}
|
|
449
518
|
} else {
|
|
450
|
-
targetScope = await resolvePluginScope(
|
|
519
|
+
targetScope = await resolvePluginScope(plugin.name, options);
|
|
451
520
|
}
|
|
452
521
|
|
|
453
|
-
// Ensure target directory exists
|
|
454
|
-
const commandsDir = getCommandsDir(targetScope);
|
|
522
|
+
// Ensure target directory exists
|
|
455
523
|
if (targetScope === 'project' && !await fileExists(join(cwd, '.claude'))) {
|
|
456
524
|
const inquirer = (await import('inquirer')).default;
|
|
457
525
|
const { create } = await inquirer.prompt([{
|
|
@@ -464,90 +532,155 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
464
532
|
return;
|
|
465
533
|
}
|
|
466
534
|
}
|
|
467
|
-
await mkdir(commandsDir, { recursive: true });
|
|
468
535
|
|
|
469
536
|
// Persist the scope preference
|
|
470
|
-
if (options.global || options.local || !await getPluginScope(
|
|
537
|
+
if (options.global || options.local || !await getPluginScope(plugin.name)) {
|
|
471
538
|
if (hasProject) {
|
|
472
|
-
await setPluginScope(
|
|
539
|
+
await setPluginScope(plugin.name, targetScope);
|
|
473
540
|
} else if (targetScope === 'global') {
|
|
474
541
|
log.warn(`Cannot persist scope preference (no .dbo/ directory). Run "dbo init" first.`);
|
|
475
542
|
}
|
|
476
543
|
}
|
|
477
544
|
|
|
478
|
-
|
|
479
|
-
|
|
545
|
+
if (plugin.type === 'directory') {
|
|
546
|
+
// New directory-based plugin installation
|
|
547
|
+
const destDir = join(getPluginsDir(targetScope), plugin.name);
|
|
548
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/plugins/' : '.claude/plugins/';
|
|
549
|
+
|
|
550
|
+
if (await fileExists(destDir)) {
|
|
551
|
+
// Check if upgrade needed via directory hash
|
|
552
|
+
const srcHash = await directoryHash(plugin.path);
|
|
553
|
+
const destHash = await directoryHash(destDir);
|
|
554
|
+
if (srcHash === destHash) {
|
|
555
|
+
upToDate++;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
480
558
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
559
|
+
const inquirer = (await import('inquirer')).default;
|
|
560
|
+
const { upgrade } = await inquirer.prompt([{
|
|
561
|
+
type: 'confirm', name: 'upgrade',
|
|
562
|
+
message: `${scopeLabel}${plugin.name}/ is already installed. Upgrade to latest version?`,
|
|
563
|
+
default: true,
|
|
564
|
+
}]);
|
|
565
|
+
if (!upgrade) {
|
|
566
|
+
log.dim(` Skipped ${plugin.name}`);
|
|
567
|
+
skipped++;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
await cp(plugin.path, destDir, { recursive: true, force: true });
|
|
572
|
+
log.success(`Upgraded ${scopeLabel}${plugin.name}/`);
|
|
573
|
+
updated++;
|
|
574
|
+
} else {
|
|
575
|
+
await mkdir(destDir, { recursive: true });
|
|
576
|
+
await cp(plugin.path, destDir, { recursive: true });
|
|
577
|
+
log.success(`Installed ${scopeLabel}${plugin.name}/`);
|
|
578
|
+
installed++;
|
|
579
|
+
|
|
580
|
+
if (targetScope === 'project') {
|
|
581
|
+
await addToGitignore(cwd, `.claude/plugins/${plugin.name}/`);
|
|
582
|
+
}
|
|
487
583
|
}
|
|
488
584
|
|
|
489
|
-
//
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
585
|
+
// Clean up legacy file if it exists
|
|
586
|
+
const legacyProjectPath = join(getCommandsDir('project'), legacyFileName);
|
|
587
|
+
const legacyGlobalPath = join(getCommandsDir('global'), legacyFileName);
|
|
588
|
+
if (await fileExists(legacyProjectPath)) {
|
|
589
|
+
const { unlink } = await import('fs/promises');
|
|
590
|
+
await unlink(legacyProjectPath);
|
|
591
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
592
|
+
log.dim(` Removed legacy command: .claude/commands/${legacyFileName}`);
|
|
593
|
+
}
|
|
594
|
+
if (await fileExists(legacyGlobalPath)) {
|
|
595
|
+
const { unlink } = await import('fs/promises');
|
|
596
|
+
await unlink(legacyGlobalPath);
|
|
597
|
+
log.dim(` Removed legacy command: ~/.claude/commands/${legacyFileName}`);
|
|
500
598
|
}
|
|
501
599
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
600
|
+
if (targetScope === 'global' && locations.project) {
|
|
601
|
+
await removeFromGitignore(`.claude/plugins/${plugin.name}/`);
|
|
602
|
+
}
|
|
505
603
|
} else {
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
604
|
+
// Legacy .md plugin installation
|
|
605
|
+
const commandsDir = getCommandsDir(targetScope);
|
|
606
|
+
await mkdir(commandsDir, { recursive: true });
|
|
607
|
+
|
|
608
|
+
const srcContent = await readFile(plugin.path, 'utf8');
|
|
609
|
+
const destPath = join(commandsDir, legacyFileName);
|
|
610
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/commands/' : '.claude/commands/';
|
|
611
|
+
|
|
612
|
+
if (await fileExists(destPath)) {
|
|
613
|
+
const destContent = await readFile(destPath, 'utf8');
|
|
614
|
+
if (fileHash(srcContent) === fileHash(destContent)) {
|
|
615
|
+
upToDate++;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
510
618
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
619
|
+
const inquirer = (await import('inquirer')).default;
|
|
620
|
+
const { upgrade } = await inquirer.prompt([{
|
|
621
|
+
type: 'confirm', name: 'upgrade',
|
|
622
|
+
message: `${scopeLabel}${legacyFileName} is already installed. Upgrade to latest version?`,
|
|
623
|
+
default: true,
|
|
624
|
+
}]);
|
|
625
|
+
if (!upgrade) {
|
|
626
|
+
log.dim(` Skipped ${legacyFileName}`);
|
|
627
|
+
skipped++;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
await copyFile(plugin.path, destPath);
|
|
632
|
+
log.success(`Upgraded ${scopeLabel}${legacyFileName}`);
|
|
633
|
+
updated++;
|
|
634
|
+
} else {
|
|
635
|
+
await copyFile(plugin.path, destPath);
|
|
636
|
+
log.success(`Installed ${scopeLabel}${legacyFileName}`);
|
|
637
|
+
installed++;
|
|
638
|
+
|
|
639
|
+
if (targetScope === 'project') {
|
|
640
|
+
await addToGitignore(cwd, `.claude/commands/${legacyFileName}`);
|
|
641
|
+
}
|
|
514
642
|
}
|
|
515
|
-
}
|
|
516
643
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
644
|
+
if (targetScope === 'global' && locations.project) {
|
|
645
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
646
|
+
}
|
|
520
647
|
}
|
|
521
648
|
}
|
|
522
649
|
|
|
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}
|
|
650
|
+
if (installed > 0) log.info(`${installed} plugin(s) installed.`);
|
|
651
|
+
if (updated > 0) log.info(`${updated} plugin(s) upgraded.`);
|
|
652
|
+
if (upToDate > 0) log.dim(`${upToDate} plugin(s) already up to date.`);
|
|
653
|
+
if (skipped > 0) log.dim(`${skipped} plugin(s) skipped.`);
|
|
527
654
|
if (installed > 0 || updated > 0) {
|
|
528
|
-
log.info('Use /dbo in Claude Code.');
|
|
529
|
-
log.warn('Note:
|
|
655
|
+
log.info('Use /dbo:cli in Claude Code.');
|
|
656
|
+
log.warn('Note: Plugins will be available in new Claude Code sessions (restart any active session).');
|
|
530
657
|
}
|
|
531
658
|
}
|
|
532
659
|
|
|
533
660
|
// ─── Specific Command ───────────────────────────────────────────────────────
|
|
534
661
|
|
|
535
662
|
async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
663
|
+
const pluginName = name.replace(/\.md$/, '');
|
|
664
|
+
|
|
665
|
+
// Find the plugin in available sources (directory-based first, then legacy)
|
|
666
|
+
const plugins = await discoverPlugins();
|
|
667
|
+
const plugin = plugins.find(p => p.name === pluginName);
|
|
668
|
+
|
|
669
|
+
if (!plugin) {
|
|
670
|
+
log.error(`Plugin "${pluginName}" not found.`);
|
|
671
|
+
if (plugins.length > 0) {
|
|
672
|
+
log.dim(` Available: ${plugins.map(p => p.name).join(', ')}`);
|
|
673
|
+
} else {
|
|
674
|
+
log.dim(` Searched: ${PLUGINS_DIR}`);
|
|
675
|
+
}
|
|
544
676
|
return;
|
|
545
677
|
}
|
|
546
678
|
|
|
547
679
|
const hasProject = await isInitialized();
|
|
680
|
+
const legacyFileName = `${pluginName}.md`;
|
|
548
681
|
|
|
549
682
|
// Check for dual location
|
|
550
|
-
const locations = await checkPluginLocations(
|
|
683
|
+
const locations = await checkPluginLocations(pluginName, legacyFileName);
|
|
551
684
|
let targetScope;
|
|
552
685
|
|
|
553
686
|
if (locations.project && locations.global) {
|
|
@@ -556,7 +689,7 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
556
689
|
} else if (options.local) {
|
|
557
690
|
targetScope = 'project';
|
|
558
691
|
} else {
|
|
559
|
-
const choice = await handleDualLocation(
|
|
692
|
+
const choice = await handleDualLocation(pluginName);
|
|
560
693
|
if (choice === 'skip') return;
|
|
561
694
|
targetScope = choice;
|
|
562
695
|
}
|
|
@@ -573,45 +706,98 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
573
706
|
}
|
|
574
707
|
}
|
|
575
708
|
|
|
576
|
-
|
|
577
|
-
|
|
709
|
+
if (plugin.type === 'directory') {
|
|
710
|
+
// Directory-based plugin installation
|
|
711
|
+
const destDir = join(getPluginsDir(targetScope), pluginName);
|
|
712
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/plugins/' : '.claude/plugins/';
|
|
578
713
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
714
|
+
if (await fileExists(destDir)) {
|
|
715
|
+
const srcHash = await directoryHash(plugin.path);
|
|
716
|
+
const destHash = await directoryHash(destDir);
|
|
717
|
+
if (srcHash === destHash) {
|
|
718
|
+
log.success(`${pluginName} is already up to date in ${scopeLabel}`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
582
721
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
722
|
+
const inquirer = (await import('inquirer')).default;
|
|
723
|
+
const { upgrade } = await inquirer.prompt([{
|
|
724
|
+
type: 'confirm', name: 'upgrade',
|
|
725
|
+
message: `${scopeLabel}${pluginName}/ is already installed. Upgrade to latest version?`,
|
|
726
|
+
default: true,
|
|
727
|
+
}]);
|
|
728
|
+
if (!upgrade) return;
|
|
729
|
+
|
|
730
|
+
await cp(plugin.path, destDir, { recursive: true, force: true });
|
|
731
|
+
log.success(`Upgraded ${scopeLabel}${pluginName}/`);
|
|
732
|
+
} else {
|
|
733
|
+
await mkdir(destDir, { recursive: true });
|
|
734
|
+
await cp(plugin.path, destDir, { recursive: true });
|
|
735
|
+
log.success(`Installed ${scopeLabel}${pluginName}/`);
|
|
736
|
+
|
|
737
|
+
if (targetScope === 'project') {
|
|
738
|
+
await addToGitignore(process.cwd(), `.claude/plugins/${pluginName}/`);
|
|
739
|
+
}
|
|
588
740
|
}
|
|
589
741
|
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
742
|
+
// Clean up legacy files
|
|
743
|
+
const legacyProjectPath = join(getCommandsDir('project'), legacyFileName);
|
|
744
|
+
const legacyGlobalPath = join(getCommandsDir('global'), legacyFileName);
|
|
745
|
+
if (await fileExists(legacyProjectPath)) {
|
|
746
|
+
const { unlink } = await import('fs/promises');
|
|
747
|
+
await unlink(legacyProjectPath);
|
|
748
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
749
|
+
log.dim(` Removed legacy command: .claude/commands/${legacyFileName}`);
|
|
750
|
+
}
|
|
751
|
+
if (await fileExists(legacyGlobalPath)) {
|
|
752
|
+
const { unlink } = await import('fs/promises');
|
|
753
|
+
await unlink(legacyGlobalPath);
|
|
754
|
+
log.dim(` Removed legacy command: ~/.claude/commands/${legacyFileName}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (targetScope === 'global' && locations.project) {
|
|
758
|
+
await removeFromGitignore(`.claude/plugins/${pluginName}/`);
|
|
759
|
+
}
|
|
600
760
|
} else {
|
|
601
|
-
|
|
602
|
-
|
|
761
|
+
// Legacy .md plugin installation
|
|
762
|
+
const commandsDir = getCommandsDir(targetScope);
|
|
763
|
+
await mkdir(commandsDir, { recursive: true });
|
|
764
|
+
|
|
765
|
+
const destPath = join(commandsDir, legacyFileName);
|
|
766
|
+
const srcContent = await readFile(plugin.path, 'utf8');
|
|
767
|
+
const scopeLabel = targetScope === 'global' ? '~/.claude/commands/' : '.claude/commands/';
|
|
768
|
+
|
|
769
|
+
if (await fileExists(destPath)) {
|
|
770
|
+
const destContent = await readFile(destPath, 'utf8');
|
|
771
|
+
if (fileHash(srcContent) === fileHash(destContent)) {
|
|
772
|
+
log.success(`${legacyFileName} is already up to date in ${scopeLabel}`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const inquirer = (await import('inquirer')).default;
|
|
777
|
+
const { upgrade } = await inquirer.prompt([{
|
|
778
|
+
type: 'confirm', name: 'upgrade',
|
|
779
|
+
message: `${scopeLabel}${legacyFileName} is already installed. Upgrade to latest version?`,
|
|
780
|
+
default: true,
|
|
781
|
+
}]);
|
|
782
|
+
if (!upgrade) return;
|
|
603
783
|
|
|
604
|
-
|
|
605
|
-
|
|
784
|
+
await copyFile(plugin.path, destPath);
|
|
785
|
+
log.success(`Upgraded ${scopeLabel}${legacyFileName}`);
|
|
786
|
+
} else {
|
|
787
|
+
await copyFile(plugin.path, destPath);
|
|
788
|
+
log.success(`Installed ${scopeLabel}${legacyFileName}`);
|
|
789
|
+
|
|
790
|
+
if (targetScope === 'project') {
|
|
791
|
+
await addToGitignore(process.cwd(), `.claude/commands/${legacyFileName}`);
|
|
792
|
+
}
|
|
606
793
|
}
|
|
607
|
-
}
|
|
608
794
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
795
|
+
if (targetScope === 'global' && locations.project) {
|
|
796
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
797
|
+
}
|
|
612
798
|
}
|
|
613
799
|
|
|
614
|
-
log.warn('Note:
|
|
800
|
+
log.warn('Note: Plugins will be available in new Claude Code sessions (restart any active session).');
|
|
615
801
|
}
|
|
616
802
|
|
|
617
803
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|