@dboio/cli 0.6.4 → 0.6.6
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/package.json
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbo",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "DBO.io CLI integration for Claude Code",
|
|
5
|
-
"author":
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"path": "skills/cli/SKILL.md",
|
|
9
|
-
"name": "cli",
|
|
10
|
-
"description": "Execute DBO.io CLI commands"
|
|
11
|
-
}
|
|
12
|
-
]
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "DBO.io"
|
|
7
|
+
}
|
|
13
8
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# DBO CLI Command
|
|
2
|
+
|
|
3
|
+
The dbo CLI interacts with DBO.io — a database-driven application framework.
|
|
4
|
+
|
|
5
|
+
**STEP 1: Check if `$ARGUMENTS` is empty or blank.**
|
|
6
|
+
|
|
7
|
+
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:
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Hi there! I can help you with running dbo commands. What would you like to do?
|
|
12
|
+
|
|
13
|
+
Here are the available commands:
|
|
14
|
+
|
|
15
|
+
| Command | Description |
|
|
16
|
+
|-----------|--------------------------------------------------|
|
|
17
|
+
| init | Initialize .dbo/ configuration |
|
|
18
|
+
| login | Authenticate with a DBO.io instance |
|
|
19
|
+
| logout | Clear session |
|
|
20
|
+
| status | Show config, domain, and session info |
|
|
21
|
+
| input | Submit CRUD operations (add/edit/delete records) |
|
|
22
|
+
| output | Query data from outputs or entities |
|
|
23
|
+
| content | Get or deploy content |
|
|
24
|
+
| media | Get media files |
|
|
25
|
+
| upload | Upload a file |
|
|
26
|
+
| message | Send messages (email, SMS, chatbot) |
|
|
27
|
+
| pull | Pull records to local files |
|
|
28
|
+
| push | Push local files back to DBO.io |
|
|
29
|
+
| add | Add a new file to DBO.io |
|
|
30
|
+
| clone | Clone an app to local project structure |
|
|
31
|
+
| diff | Compare local files with server versions |
|
|
32
|
+
| rm | Remove a file and stage server deletion |
|
|
33
|
+
| deploy | Deploy via manifest |
|
|
34
|
+
| cache | Manage cache |
|
|
35
|
+
| install | Install or upgrade CLI, plugins, Claude commands (shorthand: `i`) |
|
|
36
|
+
|
|
37
|
+
Just tell me what you'd like to do and I'll help you build the right command!
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
Then STOP. Wait for the user to respond. Guide them step by step — asking about entities, UIDs, file paths, flags, etc. to construct the correct `dbo` command. Run `dbo status` proactively to check if the CLI is initialized and authenticated before suggesting data commands.
|
|
42
|
+
|
|
43
|
+
**STEP 2: If `$ARGUMENTS` is NOT empty, run the command:**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
dbo $ARGUMENTS
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Command Reference
|
|
50
|
+
|
|
51
|
+
Available subcommands:
|
|
52
|
+
- `init` — Initialize .dbo/ configuration for the current directory
|
|
53
|
+
- `login` — Authenticate with a DBO.io instance
|
|
54
|
+
- `logout` — Clear session
|
|
55
|
+
- `status` — Show config, domain, and session info
|
|
56
|
+
- `input -d '<expr>'` — CRUD operations (add/edit/delete records)
|
|
57
|
+
- `output -e <entity>` — Query data from entities
|
|
58
|
+
- `output <uid>` — Query custom outputs
|
|
59
|
+
- `content <uid>` — Get content, `content deploy <uid> <file>` to deploy
|
|
60
|
+
- `pull [uid]` — Pull records to local files (default: content entity)
|
|
61
|
+
- `pull -e <entity> [uid]` — Pull from any entity
|
|
62
|
+
- `push <path>` — Push local files back to DBO using metadata
|
|
63
|
+
- `add <path>` — Add a new file to DBO (creates record on server)
|
|
64
|
+
- `media <uid>` — Get media files
|
|
65
|
+
- `upload <file>` — Upload binary files
|
|
66
|
+
- `message <uid>` — Send messages (email, SMS, chatbot)
|
|
67
|
+
- `cache list|refresh` — Manage cache
|
|
68
|
+
- `clone [source]` — Clone an app to local project (from file or server)
|
|
69
|
+
- `clone --app <name>` — Clone by app short name from server
|
|
70
|
+
- `diff [path]` — Compare local files against server and selectively merge changes
|
|
71
|
+
- `diff -y` — Accept all server changes without prompting
|
|
72
|
+
- `diff --no-interactive` — Show diffs without prompting to accept
|
|
73
|
+
- `rm <file>` — Remove a file locally and stage server deletion for next push
|
|
74
|
+
- `rm <directory>` — Remove a directory, all files, and sub-directories recursively
|
|
75
|
+
- `rm -f <path>` — Remove without confirmation prompts
|
|
76
|
+
- `rm --keep-local <path>` — Stage server deletions without deleting local files/directories
|
|
77
|
+
- `deploy [name]` — Deploy via dbo.deploy.json manifest
|
|
78
|
+
- `install` (alias: `i`) — Install or upgrade CLI, plugins, or Claude commands
|
|
79
|
+
- `i dbo` or `i dbo@latest` — Install/upgrade the CLI from npm
|
|
80
|
+
- `i dbo@0.4.1` — Install a specific CLI version
|
|
81
|
+
- `install /path/to/src` — Install CLI from local source
|
|
82
|
+
- `install plugins` — Install/upgrade Claude command plugins
|
|
83
|
+
- `install plugins --global` — Install plugins to `~/.claude/commands/` (shared across projects)
|
|
84
|
+
- `install plugins --local` — Install plugins to `.claude/commands/` (project only)
|
|
85
|
+
- `install claudecommands` — Install/upgrade Claude Code commands
|
|
86
|
+
- `install claudecode` — Install Claude Code CLI + commands
|
|
87
|
+
- `install --claudecommand dbo --global` — Install a specific command globally
|
|
88
|
+
|
|
89
|
+
## Change Detection (pull, clone, diff)
|
|
90
|
+
|
|
91
|
+
When pulling or cloning records that already exist locally, the CLI compares file modification times against the server's `_LastUpdated` timestamp. If the server has newer data, you'll be prompted with options:
|
|
92
|
+
|
|
93
|
+
1. **Overwrite** — Replace local files with server version
|
|
94
|
+
2. **Compare** — Show a line-by-line diff and selectively merge
|
|
95
|
+
3. **Skip** — Keep local files unchanged
|
|
96
|
+
4. **Overwrite all** — Accept all remaining server changes
|
|
97
|
+
5. **Skip all** — Skip all remaining files
|
|
98
|
+
|
|
99
|
+
Use `dbo diff [path]` to compare without pulling. Use `-y` to auto-accept all changes.
|
|
100
|
+
|
|
101
|
+
## Smart Command Building
|
|
102
|
+
|
|
103
|
+
When helping the user build a command interactively:
|
|
104
|
+
|
|
105
|
+
1. **Check readiness first**: Run `dbo status` to see if initialized and authenticated. If not, guide them through `dbo init` and `dbo login` first.
|
|
106
|
+
2. **Understand intent**: Ask what they want to do (query data, deploy a file, add a record, etc.)
|
|
107
|
+
3. **Gather parameters**: Ask for the specific values needed (entity name, UID, file path, filters, etc.)
|
|
108
|
+
4. **Build the command**: Construct the full `dbo` command with proper flags and syntax
|
|
109
|
+
5. **Execute**: Run it and explain the results
|
|
110
|
+
|
|
111
|
+
### Common workflows to suggest:
|
|
112
|
+
|
|
113
|
+
- **"I want to query data"** → Guide toward `dbo output -e <entity>` with filters
|
|
114
|
+
- **"I want to deploy/update a file"** → Check if `.metadata.json` exists → `dbo push` or `dbo content deploy`
|
|
115
|
+
- **"I want to add a new file"** → `dbo add <path>` (will create metadata interactively)
|
|
116
|
+
- **"I want to pull files from the server"** → `dbo pull` or `dbo pull -e <entity>`
|
|
117
|
+
- **"I want to delete/remove a file"** → `dbo rm <file>` (stages deletion for next `dbo push`)
|
|
118
|
+
- **"I want to delete a directory"** → `dbo rm <directory>` (removes all files + sub-dirs, stages bin deletions)
|
|
119
|
+
- **"I want to see what's on the server"** → `dbo output -e <entity> --format json`
|
|
120
|
+
- **"I need to set up this project"** → `dbo init` → `dbo login` → `dbo status`
|
|
121
|
+
- **"I want to clone an app"** → `dbo clone --app <name>` or `dbo clone <local.json>`
|
|
122
|
+
- **"I want to set up and clone"** → `dbo init --domain <host> --app <name> --clone`
|
|
123
|
+
|
|
124
|
+
## Add Command Details
|
|
125
|
+
|
|
126
|
+
`dbo add <path>` registers a new local file with the DBO server by creating an insert record.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Add a single file (interactive metadata wizard if no .metadata.json exists)
|
|
130
|
+
dbo add assets/css/colors.css
|
|
131
|
+
|
|
132
|
+
# Add with auto-accept and ticket
|
|
133
|
+
dbo add assets/css/colors.css -y --ticket abc123
|
|
134
|
+
|
|
135
|
+
# Scan current directory for all un-added files
|
|
136
|
+
dbo add .
|
|
137
|
+
|
|
138
|
+
# Scan a specific directory
|
|
139
|
+
dbo add assets/
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Flags: `-C/--confirm <true|false>`, `--ticket <id>`, `-y/--yes`, `--json`, `--jq <expr>`, `-v/--verbose`, `--domain <host>`
|
|
143
|
+
|
|
144
|
+
### How add works
|
|
145
|
+
|
|
146
|
+
1. Checks for a companion `<basename>.metadata.json` next to the file
|
|
147
|
+
2. If metadata exists with `_CreatedOn` → already on server, skips (use `push` instead)
|
|
148
|
+
3. If metadata exists without `_CreatedOn` → uses it to insert the record
|
|
149
|
+
4. If no metadata → interactive wizard prompts for: entity, content column, AppID, BinID, SiteID, Path
|
|
150
|
+
5. Creates `.metadata.json`, submits insert to `/api/input/submit`
|
|
151
|
+
6. Writes returned UID back to metadata file
|
|
152
|
+
7. Suggests running `dbo pull -e <entity> <uid>` to populate all server columns
|
|
153
|
+
|
|
154
|
+
### Non-interactive add (for scripting)
|
|
155
|
+
|
|
156
|
+
To add without prompts, create the `.metadata.json` first, then run `dbo add <file> -y`:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"Name": "colors",
|
|
161
|
+
"Path": "assets/css/colors.css",
|
|
162
|
+
"Content": "@colors.css",
|
|
163
|
+
"_entity": "content",
|
|
164
|
+
"_contentColumns": ["Content"]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Optional fields like `AppID`, `BinID`, `SiteID` are only included if the user provides values — they have no defaults and are omitted when left blank.
|
|
169
|
+
|
|
170
|
+
The `@colors.css` value means "read content from colors.css in the same directory".
|
|
171
|
+
|
|
172
|
+
### Directory scan (`dbo add .`)
|
|
173
|
+
|
|
174
|
+
Finds files that have no `.metadata.json` or whose metadata lacks `_CreatedOn`. Skips `.dbo/`, `.git/`, `node_modules/`, dotfiles, and `.metadata.json` files. When adding multiple files, defaults from the first file are reused.
|
|
175
|
+
|
|
176
|
+
## Push Command Details
|
|
177
|
+
|
|
178
|
+
`dbo push <path>` pushes local file changes back to existing DBO records using their `.metadata.json`.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
dbo push assets/css/colors.css # single file
|
|
182
|
+
dbo push assets/ # all records in directory
|
|
183
|
+
dbo push assets/ --content-only # only file content, skip metadata
|
|
184
|
+
dbo push assets/ --meta-only # only metadata columns, skip files
|
|
185
|
+
dbo push assets/ -y --ticket abc123 # auto-accept + ticket
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Flags: `-C/--confirm`, `--ticket <id>`, `--meta-only`, `--content-only`, `-y/--yes`, `--json`, `--jq`, `-v/--verbose`, `--domain`
|
|
189
|
+
|
|
190
|
+
## Column Filtering (add & push)
|
|
191
|
+
|
|
192
|
+
These columns are **never submitted** in add or push payloads:
|
|
193
|
+
- `_CreatedOn`, `_LastUpdated` — server-managed timestamps
|
|
194
|
+
- `_LastUpdatedUserID`, `_LastUpdatedTicketID` — session-provided values
|
|
195
|
+
- `UID` — server-assigned on insert; used as identifier on push (not as column value)
|
|
196
|
+
- `_id`, `_entity`, `_contentColumns`, `_mediaFile` — internal/metadata fields
|
|
197
|
+
|
|
198
|
+
## Pull → Edit → Push/Add Workflow
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Pull existing records
|
|
202
|
+
dbo pull -e content --filter 'AppID=10100'
|
|
203
|
+
|
|
204
|
+
# Edit files locally, then push changes back
|
|
205
|
+
dbo push assets/css/colors.css
|
|
206
|
+
|
|
207
|
+
# Or add a brand new file
|
|
208
|
+
dbo add assets/css/newstyle.css
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Clone Command Details
|
|
212
|
+
|
|
213
|
+
`dbo clone` scaffolds a local project from a DBO.io app export JSON, creating directories, files, metadata, and config.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Clone from a local JSON export file
|
|
217
|
+
dbo clone /path/to/app_export.json
|
|
218
|
+
|
|
219
|
+
# Clone from server by app short name (requires login)
|
|
220
|
+
dbo clone --app myapp
|
|
221
|
+
|
|
222
|
+
# Clone using AppShortName already in config
|
|
223
|
+
dbo clone
|
|
224
|
+
|
|
225
|
+
# Init + clone in one step
|
|
226
|
+
dbo init --domain my-domain.com --app myapp --clone
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Flags: `--app <name>`, `--domain <host>`, `-y/--yes`, `-v/--verbose`
|
|
230
|
+
|
|
231
|
+
### What clone does
|
|
232
|
+
|
|
233
|
+
1. Loads app JSON (local file, server API, or prompt)
|
|
234
|
+
2. Updates `.dbo/config.json` with `AppID`, `AppUID`, `AppName`, `AppShortName`
|
|
235
|
+
3. Updates `package.json` with `name`, `productName`, `description`, `homepage`, and `deploy` script
|
|
236
|
+
4. Creates directory structure from `children.bin` hierarchy → saves `.dbo/structure.json`
|
|
237
|
+
5. Writes content files (decodes base64) with `*.metadata.json` into bin directories
|
|
238
|
+
6. Downloads media files from server via `/api/media/{uid}` with `*.metadata.json`
|
|
239
|
+
7. Processes entity-dir records (`extension`, `app_version`, `data_source`, `site`, `group`, `integration`, `automation`) into project directories (`Extensions/`, `Data Sources/`, etc.) as `.metadata.json` files with optional companion content files
|
|
240
|
+
8. Processes remaining entities with BinID into corresponding bin directories
|
|
241
|
+
9. Saves `app.json` to project root with `@path/to/*.metadata.json` references
|
|
242
|
+
|
|
243
|
+
### Placement preferences
|
|
244
|
+
|
|
245
|
+
When a record has both `Path`/`FullPath` and `BinID`, clone prompts the user to choose placement. Preferences are saved to `.dbo/config.json` and reused on future clones:
|
|
246
|
+
|
|
247
|
+
- `ContentPlacement`: `bin` | `path` | `ask` — for content and other entities
|
|
248
|
+
- `MediaPlacement`: `bin` | `fullpath` | `ask` — for media files
|
|
249
|
+
|
|
250
|
+
Pre-set these in config.json to skip prompts. `.dbo/config.json` and `.dbo/structure.json` are shared via git; `.dbo/credentials.json` and `.dbo/cookies.txt` are gitignored (per-user).
|
|
251
|
+
|
|
252
|
+
### AppID awareness (add & input)
|
|
253
|
+
|
|
254
|
+
After cloning, the config has an `AppID`. When running `dbo add` or `dbo input` without an AppID in the data, the CLI prompts:
|
|
255
|
+
1. Yes, use AppID from config
|
|
256
|
+
2. No
|
|
257
|
+
3. Enter custom AppID
|
|
258
|
+
|
|
259
|
+
### Init flags for clone
|
|
260
|
+
|
|
261
|
+
`dbo init` now supports `--app <shortName>` and `--clone` flags to combine initialization with app cloning.
|
|
262
|
+
|
|
263
|
+
## Error Handling
|
|
264
|
+
|
|
265
|
+
- If the command fails with a session/authentication error, suggest: `dbo login`
|
|
266
|
+
- If it fails with "No domain configured", suggest: `dbo init`
|
|
267
|
+
- If a command is not found, suggest: `dbo --help`
|
|
268
|
+
|
|
269
|
+
## Output
|
|
270
|
+
|
|
271
|
+
When showing results:
|
|
272
|
+
- Format JSON output readably
|
|
273
|
+
- For pull/push/add operations, list the files created or modified
|
|
274
|
+
- For query operations, summarize the row count and key fields
|
package/src/commands/install.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readdir, readFile, writeFile, mkdir, access, copyFile, cp } 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';
|
|
@@ -9,6 +9,10 @@ import { homedir } from 'os';
|
|
|
9
9
|
import { log } from '../lib/logger.js';
|
|
10
10
|
import { getPluginScope, setPluginScope, isInitialized, removeFromGitignore } from '../lib/config.js';
|
|
11
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
|
+
|
|
12
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
17
|
const CLI_ROOT = join(__dirname, '..', '..');
|
|
14
18
|
const LEGACY_PLUGINS_DIR = join(__dirname, '..', 'plugins', 'claudecommands');
|
|
@@ -36,16 +40,117 @@ function getCommandsDir(scope) {
|
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
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.
|
|
39
44
|
* @param {'project' | 'global'} scope
|
|
40
45
|
* @returns {string} Absolute path to plugins directory
|
|
41
46
|
*/
|
|
42
47
|
function getPluginsDir(scope) {
|
|
43
48
|
if (scope === 'global') {
|
|
44
|
-
return join(
|
|
49
|
+
return join(CLAUDE_PLUGINS_DIR, 'cache', PLUGIN_MARKETPLACE);
|
|
45
50
|
}
|
|
46
51
|
return join(process.cwd(), '.claude', 'plugins');
|
|
47
52
|
}
|
|
48
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
|
|
67
|
+
*/
|
|
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
|
+
* Extract commands/*.md from a directory plugin to ~/.claude/commands/ or .claude/commands/.
|
|
121
|
+
* This provides the /commandName shorthand alongside the /plugin:skill format.
|
|
122
|
+
* @param {string} pluginSourcePath - Path to the plugin source directory
|
|
123
|
+
* @param {'project' | 'global'} scope - Installation scope
|
|
124
|
+
* @returns {Promise<number>} Number of commands extracted
|
|
125
|
+
*/
|
|
126
|
+
async function extractPluginCommands(pluginSourcePath, scope) {
|
|
127
|
+
const commandsSourceDir = join(pluginSourcePath, 'commands');
|
|
128
|
+
if (!existsSync(commandsSourceDir)) return 0;
|
|
129
|
+
|
|
130
|
+
const commandsTargetDir = getCommandsDir(scope);
|
|
131
|
+
await mkdir(commandsTargetDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
let extracted = 0;
|
|
134
|
+
const files = await readdir(commandsSourceDir);
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
if (!file.endsWith('.md')) continue;
|
|
137
|
+
const srcPath = join(commandsSourceDir, file);
|
|
138
|
+
const destPath = join(commandsTargetDir, file);
|
|
139
|
+
const srcContent = await readFile(srcPath, 'utf8');
|
|
140
|
+
|
|
141
|
+
if (await fileExists(destPath)) {
|
|
142
|
+
const destContent = await readFile(destPath, 'utf8');
|
|
143
|
+
if (fileHash(srcContent) === fileHash(destContent)) continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await copyFile(srcPath, destPath);
|
|
147
|
+
const label = scope === 'global' ? '~/.claude/commands/' : '.claude/commands/';
|
|
148
|
+
log.dim(` Extracted command: ${label}${file}`);
|
|
149
|
+
extracted++;
|
|
150
|
+
}
|
|
151
|
+
return extracted;
|
|
152
|
+
}
|
|
153
|
+
|
|
49
154
|
/**
|
|
50
155
|
* Check if a plugin source is a directory-based plugin (has .claude-plugin/).
|
|
51
156
|
* @param {string} pluginPath - Path to the plugin directory
|
|
@@ -147,16 +252,23 @@ async function resolvePluginScope(pluginName, options) {
|
|
|
147
252
|
|
|
148
253
|
/**
|
|
149
254
|
* Check if plugin exists in both project and global locations.
|
|
150
|
-
* Checks
|
|
255
|
+
* Checks cache path, old direct path, and legacy (.claude/commands/) paths.
|
|
151
256
|
* @param {string} pluginName - Plugin name (without extension)
|
|
152
257
|
* @param {string} [legacyFileName] - Legacy filename (with .md) for backward compat
|
|
153
258
|
* @returns {Promise<{project: boolean, global: boolean}>}
|
|
154
259
|
*/
|
|
155
260
|
async function checkPluginLocations(pluginName, legacyFileName) {
|
|
156
261
|
const projectPlugin = join(getPluginsDir('project'), pluginName);
|
|
157
|
-
const globalPlugin = join(getPluginsDir('global'), pluginName);
|
|
158
262
|
let project = await fileExists(projectPlugin);
|
|
159
|
-
|
|
263
|
+
|
|
264
|
+
// Check global: cache path first, then old direct path
|
|
265
|
+
const registry = await readPluginRegistry();
|
|
266
|
+
const key = `${pluginName}@${PLUGIN_MARKETPLACE}`;
|
|
267
|
+
let global = !!registry.plugins[key];
|
|
268
|
+
if (!global) {
|
|
269
|
+
// Check old direct path (~/.claude/plugins/<name>/)
|
|
270
|
+
global = await fileExists(join(CLAUDE_PLUGINS_DIR, pluginName, '.claude-plugin', 'plugin.json'));
|
|
271
|
+
}
|
|
160
272
|
|
|
161
273
|
// Also check legacy locations
|
|
162
274
|
if (legacyFileName) {
|
|
@@ -533,25 +645,29 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
533
645
|
}
|
|
534
646
|
}
|
|
535
647
|
|
|
536
|
-
// Persist the scope preference
|
|
537
|
-
if (options.global || options.local || !await getPluginScope(plugin.name)) {
|
|
538
|
-
if (hasProject) {
|
|
539
|
-
await setPluginScope(plugin.name, targetScope);
|
|
540
|
-
} else if (targetScope === 'global') {
|
|
541
|
-
log.warn(`Cannot persist scope preference (no .dbo/ directory). Run "dbo init" first.`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
648
|
if (plugin.type === 'directory') {
|
|
546
649
|
// New directory-based plugin installation
|
|
547
|
-
|
|
650
|
+
// Read version from plugin.json
|
|
651
|
+
const pluginMeta = JSON.parse(await readFile(join(plugin.path, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
652
|
+
const pluginVersion = pluginMeta.version || '0.0.0';
|
|
653
|
+
|
|
654
|
+
// For global scope, use the cache path and register in installed_plugins.json
|
|
655
|
+
const destDir = targetScope === 'global'
|
|
656
|
+
? getGlobalPluginCachePath(plugin.name, pluginVersion)
|
|
657
|
+
: join(getPluginsDir('project'), plugin.name);
|
|
548
658
|
const scopeLabel = targetScope === 'global' ? '~/.claude/plugins/' : '.claude/plugins/';
|
|
549
659
|
|
|
550
|
-
|
|
660
|
+
// Check for existing installation (cache path or old direct path)
|
|
661
|
+
const oldDirectPath = join(CLAUDE_PLUGINS_DIR, plugin.name);
|
|
662
|
+
const existingPath = await fileExists(destDir) ? destDir
|
|
663
|
+
: (targetScope === 'global' && await fileExists(oldDirectPath)) ? oldDirectPath
|
|
664
|
+
: null;
|
|
665
|
+
|
|
666
|
+
if (existingPath) {
|
|
551
667
|
// Check if upgrade needed via directory hash
|
|
552
668
|
const srcHash = await directoryHash(plugin.path);
|
|
553
|
-
const destHash = await directoryHash(
|
|
554
|
-
if (srcHash === destHash) {
|
|
669
|
+
const destHash = await directoryHash(existingPath);
|
|
670
|
+
if (srcHash === destHash && existingPath === destDir) {
|
|
555
671
|
upToDate++;
|
|
556
672
|
continue;
|
|
557
673
|
}
|
|
@@ -568,12 +684,29 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
568
684
|
continue;
|
|
569
685
|
}
|
|
570
686
|
|
|
687
|
+
await mkdir(destDir, { recursive: true });
|
|
571
688
|
await cp(plugin.path, destDir, { recursive: true, force: true });
|
|
689
|
+
|
|
690
|
+
// Clean up old direct path if migrating to cache path
|
|
691
|
+
if (targetScope === 'global' && existingPath === oldDirectPath && existingPath !== destDir) {
|
|
692
|
+
await rm(oldDirectPath, { recursive: true, force: true });
|
|
693
|
+
log.dim(` Migrated from ${oldDirectPath} to cache path`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (targetScope === 'global') {
|
|
697
|
+
await registerPlugin(plugin.name, pluginVersion, destDir);
|
|
698
|
+
}
|
|
699
|
+
|
|
572
700
|
log.success(`Upgraded ${scopeLabel}${plugin.name}/`);
|
|
573
701
|
updated++;
|
|
574
702
|
} else {
|
|
575
703
|
await mkdir(destDir, { recursive: true });
|
|
576
704
|
await cp(plugin.path, destDir, { recursive: true });
|
|
705
|
+
|
|
706
|
+
if (targetScope === 'global') {
|
|
707
|
+
await registerPlugin(plugin.name, pluginVersion, destDir);
|
|
708
|
+
}
|
|
709
|
+
|
|
577
710
|
log.success(`Installed ${scopeLabel}${plugin.name}/`);
|
|
578
711
|
installed++;
|
|
579
712
|
|
|
@@ -582,19 +715,36 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
582
715
|
}
|
|
583
716
|
}
|
|
584
717
|
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
await
|
|
591
|
-
|
|
592
|
-
|
|
718
|
+
// Extract commands/*.md to ~/.claude/commands/ or .claude/commands/
|
|
719
|
+
await extractPluginCommands(plugin.path, targetScope);
|
|
720
|
+
|
|
721
|
+
// Persist scope + metadata to config.local.json
|
|
722
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(plugin.name))) {
|
|
723
|
+
await setPluginScope(plugin.name, {
|
|
724
|
+
scope: targetScope === 'global' ? 'user' : 'project',
|
|
725
|
+
installPath: destDir,
|
|
726
|
+
version: pluginVersion,
|
|
727
|
+
});
|
|
728
|
+
} else if (targetScope === 'global' && !hasProject) {
|
|
729
|
+
log.warn(`Cannot persist scope preference (no .dbo/ directory). Run "dbo init" first.`);
|
|
593
730
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
731
|
+
|
|
732
|
+
// Clean up legacy command files (but not ones we just extracted from commands/)
|
|
733
|
+
const hasPluginCommand = existsSync(join(plugin.path, 'commands', legacyFileName));
|
|
734
|
+
if (!hasPluginCommand) {
|
|
735
|
+
const legacyProjectPath = join(getCommandsDir('project'), legacyFileName);
|
|
736
|
+
const legacyGlobalPath = join(getCommandsDir('global'), legacyFileName);
|
|
737
|
+
if (await fileExists(legacyProjectPath)) {
|
|
738
|
+
const { unlink } = await import('fs/promises');
|
|
739
|
+
await unlink(legacyProjectPath);
|
|
740
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
741
|
+
log.dim(` Removed legacy command: .claude/commands/${legacyFileName}`);
|
|
742
|
+
}
|
|
743
|
+
if (await fileExists(legacyGlobalPath)) {
|
|
744
|
+
const { unlink } = await import('fs/promises');
|
|
745
|
+
await unlink(legacyGlobalPath);
|
|
746
|
+
log.dim(` Removed legacy command: ~/.claude/commands/${legacyFileName}`);
|
|
747
|
+
}
|
|
598
748
|
}
|
|
599
749
|
|
|
600
750
|
if (targetScope === 'global' && locations.project) {
|
|
@@ -644,6 +794,11 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
644
794
|
if (targetScope === 'global' && locations.project) {
|
|
645
795
|
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
646
796
|
}
|
|
797
|
+
|
|
798
|
+
// Persist scope for legacy plugins
|
|
799
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(plugin.name))) {
|
|
800
|
+
await setPluginScope(plugin.name, targetScope);
|
|
801
|
+
}
|
|
647
802
|
}
|
|
648
803
|
}
|
|
649
804
|
|
|
@@ -652,7 +807,7 @@ export async function installOrUpdateClaudeCommands(options = {}) {
|
|
|
652
807
|
if (upToDate > 0) log.dim(`${upToDate} plugin(s) already up to date.`);
|
|
653
808
|
if (skipped > 0) log.dim(`${skipped} plugin(s) skipped.`);
|
|
654
809
|
if (installed > 0 || updated > 0) {
|
|
655
|
-
log.info('Use /dbo
|
|
810
|
+
log.info('Use /dbo in Claude Code.');
|
|
656
811
|
log.warn('Note: Plugins will be available in new Claude Code sessions (restart any active session).');
|
|
657
812
|
}
|
|
658
813
|
}
|
|
@@ -697,24 +852,26 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
697
852
|
targetScope = await resolvePluginScope(pluginName, options);
|
|
698
853
|
}
|
|
699
854
|
|
|
700
|
-
// Persist scope
|
|
701
|
-
if (options.global || options.local || !await getPluginScope(pluginName)) {
|
|
702
|
-
if (hasProject) {
|
|
703
|
-
await setPluginScope(pluginName, targetScope);
|
|
704
|
-
} else if (targetScope === 'global') {
|
|
705
|
-
log.warn(`Cannot persist scope preference (no .dbo/ directory). Run "dbo init" first.`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
855
|
if (plugin.type === 'directory') {
|
|
710
856
|
// Directory-based plugin installation
|
|
711
|
-
const
|
|
857
|
+
const pluginMeta = JSON.parse(await readFile(join(plugin.path, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
858
|
+
const pluginVersion = pluginMeta.version || '0.0.0';
|
|
859
|
+
|
|
860
|
+
const destDir = targetScope === 'global'
|
|
861
|
+
? getGlobalPluginCachePath(pluginName, pluginVersion)
|
|
862
|
+
: join(getPluginsDir('project'), pluginName);
|
|
712
863
|
const scopeLabel = targetScope === 'global' ? '~/.claude/plugins/' : '.claude/plugins/';
|
|
713
864
|
|
|
714
|
-
|
|
865
|
+
// Check for existing installation (cache path or old direct path)
|
|
866
|
+
const oldDirectPath = join(CLAUDE_PLUGINS_DIR, pluginName);
|
|
867
|
+
const existingPath = await fileExists(destDir) ? destDir
|
|
868
|
+
: (targetScope === 'global' && await fileExists(oldDirectPath)) ? oldDirectPath
|
|
869
|
+
: null;
|
|
870
|
+
|
|
871
|
+
if (existingPath) {
|
|
715
872
|
const srcHash = await directoryHash(plugin.path);
|
|
716
|
-
const destHash = await directoryHash(
|
|
717
|
-
if (srcHash === destHash) {
|
|
873
|
+
const destHash = await directoryHash(existingPath);
|
|
874
|
+
if (srcHash === destHash && existingPath === destDir) {
|
|
718
875
|
log.success(`${pluginName} is already up to date in ${scopeLabel}`);
|
|
719
876
|
return;
|
|
720
877
|
}
|
|
@@ -727,11 +884,28 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
727
884
|
}]);
|
|
728
885
|
if (!upgrade) return;
|
|
729
886
|
|
|
887
|
+
await mkdir(destDir, { recursive: true });
|
|
730
888
|
await cp(plugin.path, destDir, { recursive: true, force: true });
|
|
889
|
+
|
|
890
|
+
// Clean up old direct path if migrating to cache path
|
|
891
|
+
if (targetScope === 'global' && existingPath === oldDirectPath && existingPath !== destDir) {
|
|
892
|
+
await rm(oldDirectPath, { recursive: true, force: true });
|
|
893
|
+
log.dim(` Migrated from ${oldDirectPath} to cache path`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (targetScope === 'global') {
|
|
897
|
+
await registerPlugin(pluginName, pluginVersion, destDir);
|
|
898
|
+
}
|
|
899
|
+
|
|
731
900
|
log.success(`Upgraded ${scopeLabel}${pluginName}/`);
|
|
732
901
|
} else {
|
|
733
902
|
await mkdir(destDir, { recursive: true });
|
|
734
903
|
await cp(plugin.path, destDir, { recursive: true });
|
|
904
|
+
|
|
905
|
+
if (targetScope === 'global') {
|
|
906
|
+
await registerPlugin(pluginName, pluginVersion, destDir);
|
|
907
|
+
}
|
|
908
|
+
|
|
735
909
|
log.success(`Installed ${scopeLabel}${pluginName}/`);
|
|
736
910
|
|
|
737
911
|
if (targetScope === 'project') {
|
|
@@ -739,19 +913,36 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
739
913
|
}
|
|
740
914
|
}
|
|
741
915
|
|
|
742
|
-
//
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
await
|
|
748
|
-
|
|
749
|
-
|
|
916
|
+
// Extract commands/*.md to ~/.claude/commands/ or .claude/commands/
|
|
917
|
+
await extractPluginCommands(plugin.path, targetScope);
|
|
918
|
+
|
|
919
|
+
// Persist scope + metadata to config.local.json
|
|
920
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(pluginName))) {
|
|
921
|
+
await setPluginScope(pluginName, {
|
|
922
|
+
scope: targetScope === 'global' ? 'user' : 'project',
|
|
923
|
+
installPath: destDir,
|
|
924
|
+
version: pluginVersion,
|
|
925
|
+
});
|
|
926
|
+
} else if (targetScope === 'global' && !hasProject) {
|
|
927
|
+
log.warn(`Cannot persist scope preference (no .dbo/ directory). Run "dbo init" first.`);
|
|
750
928
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
929
|
+
|
|
930
|
+
// Clean up legacy command files (but not ones we just extracted from commands/)
|
|
931
|
+
const hasPluginCommand = existsSync(join(plugin.path, 'commands', legacyFileName));
|
|
932
|
+
if (!hasPluginCommand) {
|
|
933
|
+
const legacyProjectPath = join(getCommandsDir('project'), legacyFileName);
|
|
934
|
+
const legacyGlobalPath = join(getCommandsDir('global'), legacyFileName);
|
|
935
|
+
if (await fileExists(legacyProjectPath)) {
|
|
936
|
+
const { unlink } = await import('fs/promises');
|
|
937
|
+
await unlink(legacyProjectPath);
|
|
938
|
+
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
939
|
+
log.dim(` Removed legacy command: .claude/commands/${legacyFileName}`);
|
|
940
|
+
}
|
|
941
|
+
if (await fileExists(legacyGlobalPath)) {
|
|
942
|
+
const { unlink } = await import('fs/promises');
|
|
943
|
+
await unlink(legacyGlobalPath);
|
|
944
|
+
log.dim(` Removed legacy command: ~/.claude/commands/${legacyFileName}`);
|
|
945
|
+
}
|
|
755
946
|
}
|
|
756
947
|
|
|
757
948
|
if (targetScope === 'global' && locations.project) {
|
|
@@ -795,6 +986,11 @@ async function installOrUpdateSpecificCommand(name, options = {}) {
|
|
|
795
986
|
if (targetScope === 'global' && locations.project) {
|
|
796
987
|
await removeFromGitignore(`.claude/commands/${legacyFileName}`);
|
|
797
988
|
}
|
|
989
|
+
|
|
990
|
+
// Persist scope for legacy plugins
|
|
991
|
+
if (hasProject && (options.global || options.local || !await getPluginScope(pluginName))) {
|
|
992
|
+
await setPluginScope(pluginName, targetScope);
|
|
993
|
+
}
|
|
798
994
|
}
|
|
799
995
|
|
|
800
996
|
log.warn('Note: Plugins will be available in new Claude Code sessions (restart any active session).');
|
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 ────────────────────────────────────────────────────────────
|