@dboio/cli 0.4.2 → 0.5.1
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 +246 -70
- package/bin/dbo.js +7 -3
- package/package.json +9 -3
- package/src/commands/clone.js +469 -14
- package/src/commands/diff.js +246 -0
- package/src/commands/init.js +30 -22
- package/src/commands/install.js +526 -69
- package/src/commands/mv.js +869 -0
- package/src/commands/pull.js +6 -0
- package/src/commands/push.js +63 -21
- package/src/commands/rm.js +337 -0
- package/src/commands/status.js +28 -1
- package/src/lib/config.js +195 -0
- package/src/lib/diff.js +740 -0
- package/src/lib/save-to-disk.js +71 -4
- package/src/lib/structure.js +36 -0
- package/src/plugins/claudecommands/dbo.md +37 -6
- package/src/commands/update.js +0 -168
package/src/lib/save-to-disk.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFile, mkdir, access, readFile } from 'fs/promises';
|
|
2
2
|
import { join, dirname, basename, extname } from 'path';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
|
+
import { setFileTimestamps } from './timestamps.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Resolve a column value for file writing.
|
|
@@ -32,6 +33,8 @@ function resolveContentValue(value) {
|
|
|
32
33
|
* saveExtension: column name for extension, or literal extension
|
|
33
34
|
* nonInteractive: skip all prompts
|
|
34
35
|
* contentFileMap: object mapping column → { suffix, extension } per column
|
|
36
|
+
* changeDetection: enable smart change detection (compares timestamps)
|
|
37
|
+
* config: loaded config object (needed for ServerTimezone)
|
|
35
38
|
*/
|
|
36
39
|
export async function saveToDisk(rows, columns, options = {}) {
|
|
37
40
|
if (!rows || rows.length === 0) {
|
|
@@ -158,7 +161,14 @@ export async function saveToDisk(rows, columns, options = {}) {
|
|
|
158
161
|
extensionSource = useExtCol ? extCol : null;
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
// Lazy-load change detection functions only if needed
|
|
165
|
+
let diffModule = null;
|
|
166
|
+
if (options.changeDetection) {
|
|
167
|
+
diffModule = await import('./diff.js');
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
let savedCount = 0;
|
|
171
|
+
let bulkAction = null; // Track 'overwrite_all' or 'skip_all' across records
|
|
162
172
|
const usedNames = new Map(); // track dir+name to avoid collisions
|
|
163
173
|
|
|
164
174
|
for (const row of rows) {
|
|
@@ -210,10 +220,53 @@ export async function saveToDisk(rows, columns, options = {}) {
|
|
|
210
220
|
} catch { /* ignore parse errors */ }
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
// ── Change detection ────────────────────────────────────────────────
|
|
224
|
+
if (options.changeDetection && existingMeta && !options.nonInteractive && diffModule) {
|
|
225
|
+
// Handle bulk actions from previous iterations
|
|
226
|
+
if (bulkAction === 'skip_all') {
|
|
227
|
+
log.dim(` Skipped ${finalName}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (bulkAction !== 'overwrite_all') {
|
|
232
|
+
const localSyncTime = await diffModule.getLocalSyncTime(metaPath);
|
|
233
|
+
const serverIsNewer = diffModule.isServerNewer(localSyncTime, row._LastUpdated, options.config || {});
|
|
234
|
+
|
|
235
|
+
if (serverIsNewer) {
|
|
236
|
+
const action = await diffModule.promptChangeDetection(finalName, row, options.config || {});
|
|
237
|
+
|
|
238
|
+
if (action === 'skip') {
|
|
239
|
+
log.dim(` Skipped ${finalName}`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (action === 'skip_all') {
|
|
243
|
+
bulkAction = 'skip_all';
|
|
244
|
+
log.dim(` Skipped ${finalName}`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (action === 'overwrite_all') {
|
|
248
|
+
bulkAction = 'overwrite_all';
|
|
249
|
+
// Fall through to write
|
|
250
|
+
}
|
|
251
|
+
if (action === 'compare') {
|
|
252
|
+
// Run inline diff and merge
|
|
253
|
+
const mergeResult = await diffModule.inlineDiffAndMerge(row, metaPath, options.config || {});
|
|
254
|
+
if (!mergeResult.skipped) {
|
|
255
|
+
savedCount++;
|
|
256
|
+
}
|
|
257
|
+
continue; // inlineDiffAndMerge handles writing
|
|
258
|
+
}
|
|
259
|
+
// 'overwrite' falls through to normal write
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Write content files ─────────────────────────────────────────────
|
|
213
265
|
// Build metadata with @filename placeholders
|
|
214
266
|
const meta = { ...row };
|
|
215
267
|
if (options.entity) meta._entity = options.entity;
|
|
216
268
|
const contentColumnsList = [];
|
|
269
|
+
const writtenFilePaths = []; // Track files for timestamp setting
|
|
217
270
|
|
|
218
271
|
for (const col of contentCols) {
|
|
219
272
|
const content = row[col];
|
|
@@ -235,8 +288,8 @@ export async function saveToDisk(rows, columns, options = {}) {
|
|
|
235
288
|
|
|
236
289
|
const filePath = join(dir, fileName);
|
|
237
290
|
|
|
238
|
-
// Prompt before overwriting
|
|
239
|
-
if (await fileExists(filePath) && !options.nonInteractive) {
|
|
291
|
+
// Prompt before overwriting (only if NOT using change detection — change detection handles its own prompts)
|
|
292
|
+
if (!options.changeDetection && await fileExists(filePath) && !options.nonInteractive) {
|
|
240
293
|
const inquirer = (await import('inquirer')).default;
|
|
241
294
|
const { overwrite } = await inquirer.prompt([{
|
|
242
295
|
type: 'confirm', name: 'overwrite',
|
|
@@ -253,6 +306,7 @@ export async function saveToDisk(rows, columns, options = {}) {
|
|
|
253
306
|
|
|
254
307
|
await writeFile(filePath, resolveContentValue(content));
|
|
255
308
|
log.success(`Saved ${filePath}`);
|
|
309
|
+
writtenFilePaths.push(filePath);
|
|
256
310
|
savedCount++;
|
|
257
311
|
|
|
258
312
|
meta[col] = `@${fileName}`;
|
|
@@ -263,8 +317,9 @@ export async function saveToDisk(rows, columns, options = {}) {
|
|
|
263
317
|
meta._contentColumns = contentColumnsList;
|
|
264
318
|
}
|
|
265
319
|
|
|
266
|
-
// Save metadata
|
|
267
|
-
|
|
320
|
+
// ── Save metadata ───────────────────────────────────────────────────
|
|
321
|
+
// Prompt before overwriting (only if NOT using change detection)
|
|
322
|
+
if (!options.changeDetection && await fileExists(metaPath) && !options.nonInteractive) {
|
|
268
323
|
const inquirer = (await import('inquirer')).default;
|
|
269
324
|
const { overwrite } = await inquirer.prompt([{
|
|
270
325
|
type: 'confirm', name: 'overwrite',
|
|
@@ -279,7 +334,19 @@ export async function saveToDisk(rows, columns, options = {}) {
|
|
|
279
334
|
|
|
280
335
|
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
281
336
|
log.dim(` → ${metaPath}`);
|
|
337
|
+
writtenFilePaths.push(metaPath);
|
|
282
338
|
savedCount++;
|
|
339
|
+
|
|
340
|
+
// ── Set file timestamps ─────────────────────────────────────────────
|
|
341
|
+
// Sync file mtime to server's _LastUpdated (establishes sync point)
|
|
342
|
+
const serverTz = options.config?.ServerTimezone;
|
|
343
|
+
if (serverTz && (row._CreatedOn || row._LastUpdated)) {
|
|
344
|
+
for (const fp of writtenFilePaths) {
|
|
345
|
+
try {
|
|
346
|
+
await setFileTimestamps(fp, row._CreatedOn, row._LastUpdated, serverTz);
|
|
347
|
+
} catch { /* non-critical */ }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
283
350
|
}
|
|
284
351
|
|
|
285
352
|
log.info(`Saved ${savedCount} file(s) for ${rows.length} record(s)`);
|
package/src/lib/structure.js
CHANGED
|
@@ -9,13 +9,27 @@ export const BINS_DIR = 'Bins';
|
|
|
9
9
|
/** Default top-level directories created at project root during clone */
|
|
10
10
|
export const DEFAULT_PROJECT_DIRS = [
|
|
11
11
|
BINS_DIR,
|
|
12
|
+
'Automations',
|
|
12
13
|
'App Versions',
|
|
13
14
|
'Documentation',
|
|
14
15
|
'Sites',
|
|
15
16
|
'Extensions',
|
|
16
17
|
'Data Sources',
|
|
18
|
+
'Groups',
|
|
19
|
+
'Integrations',
|
|
17
20
|
];
|
|
18
21
|
|
|
22
|
+
/** Map from server entity key → local project directory name */
|
|
23
|
+
export const ENTITY_DIR_MAP = {
|
|
24
|
+
extension: 'Extensions',
|
|
25
|
+
app_version: 'App Versions',
|
|
26
|
+
data_source: 'Data Sources',
|
|
27
|
+
site: 'Sites',
|
|
28
|
+
group: 'Groups',
|
|
29
|
+
integration: 'Integrations',
|
|
30
|
+
automation: 'Automations',
|
|
31
|
+
};
|
|
32
|
+
|
|
19
33
|
/**
|
|
20
34
|
* Build a bin hierarchy from an array of bin objects.
|
|
21
35
|
* Filters by targetAppId and resolves full directory paths via ParentBinID traversal.
|
|
@@ -127,3 +141,25 @@ export function getBinName(binId, structure) {
|
|
|
127
141
|
const entry = structure[binId];
|
|
128
142
|
return entry ? entry.name : null;
|
|
129
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Reverse-lookup: find a bin entry by its directory path.
|
|
147
|
+
* Accepts paths with or without the "Bins/" prefix.
|
|
148
|
+
* Returns { binId, name, path, segment, parentBinID, uid, fullPath } or null.
|
|
149
|
+
*/
|
|
150
|
+
export function findBinByPath(dirPath, structure) {
|
|
151
|
+
const normalized = dirPath.replace(/^Bins\//, '').replace(/^\/+|\/+$/g, '');
|
|
152
|
+
for (const [binId, entry] of Object.entries(structure)) {
|
|
153
|
+
if (entry.fullPath === normalized) {
|
|
154
|
+
return { binId: Number(binId), ...entry };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Find all direct child bins of a given BinID.
|
|
162
|
+
*/
|
|
163
|
+
export function findChildBins(binId, structure) {
|
|
164
|
+
return Object.values(structure).filter(e => e.parentBinID === binId);
|
|
165
|
+
}
|
|
@@ -33,10 +33,11 @@ Here are the available commands:
|
|
|
33
33
|
| push | Push local files back to DBO.io |
|
|
34
34
|
| add | Add a new file to DBO.io |
|
|
35
35
|
| clone | Clone an app to local project structure |
|
|
36
|
+
| diff | Compare local files with server versions |
|
|
37
|
+
| rm | Remove a file and stage server deletion |
|
|
36
38
|
| deploy | Deploy via manifest |
|
|
37
39
|
| cache | Manage cache |
|
|
38
|
-
| install | Install
|
|
39
|
-
| update | Update CLI or plugins |
|
|
40
|
+
| install | Install or upgrade CLI, plugins, Claude commands |
|
|
40
41
|
|
|
41
42
|
Just tell me what you'd like to do and I'll help you build the right command!
|
|
42
43
|
|
|
@@ -71,9 +72,36 @@ Available subcommands:
|
|
|
71
72
|
- `cache list|refresh` — Manage cache
|
|
72
73
|
- `clone [source]` — Clone an app to local project (from file or server)
|
|
73
74
|
- `clone --app <name>` — Clone by app short name from server
|
|
75
|
+
- `diff [path]` — Compare local files against server and selectively merge changes
|
|
76
|
+
- `diff -y` — Accept all server changes without prompting
|
|
77
|
+
- `diff --no-interactive` — Show diffs without prompting to accept
|
|
78
|
+
- `rm <file>` — Remove a file locally and stage server deletion for next push
|
|
79
|
+
- `rm <directory>` — Remove a directory, all files, and sub-directories recursively
|
|
80
|
+
- `rm -f <path>` — Remove without confirmation prompts
|
|
81
|
+
- `rm --keep-local <path>` — Stage server deletions without deleting local files/directories
|
|
74
82
|
- `deploy [name]` — Deploy via dbo.deploy.json manifest
|
|
75
|
-
- `install` — Install
|
|
76
|
-
- `
|
|
83
|
+
- `install` — Install or upgrade CLI, plugins, or Claude commands
|
|
84
|
+
- `install dbo` or `install dbo@latest` — Install/upgrade the CLI from npm
|
|
85
|
+
- `install dbo@0.4.1` — Install a specific CLI version
|
|
86
|
+
- `install /path/to/src` — Install CLI from local source
|
|
87
|
+
- `install plugins` — Install/upgrade Claude command plugins
|
|
88
|
+
- `install plugins --global` — Install plugins to `~/.claude/commands/` (shared across projects)
|
|
89
|
+
- `install plugins --local` — Install plugins to `.claude/commands/` (project only)
|
|
90
|
+
- `install claudecommands` — Install/upgrade Claude Code commands
|
|
91
|
+
- `install claudecode` — Install Claude Code CLI + commands
|
|
92
|
+
- `install --claudecommand dbo --global` — Install a specific command globally
|
|
93
|
+
|
|
94
|
+
## Change Detection (pull, clone, diff)
|
|
95
|
+
|
|
96
|
+
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:
|
|
97
|
+
|
|
98
|
+
1. **Overwrite** — Replace local files with server version
|
|
99
|
+
2. **Compare** — Show a line-by-line diff and selectively merge
|
|
100
|
+
3. **Skip** — Keep local files unchanged
|
|
101
|
+
4. **Overwrite all** — Accept all remaining server changes
|
|
102
|
+
5. **Skip all** — Skip all remaining files
|
|
103
|
+
|
|
104
|
+
Use `dbo diff [path]` to compare without pulling. Use `-y` to auto-accept all changes.
|
|
77
105
|
|
|
78
106
|
## Smart Command Building
|
|
79
107
|
|
|
@@ -91,6 +119,8 @@ When helping the user build a command interactively:
|
|
|
91
119
|
- **"I want to deploy/update a file"** → Check if `.metadata.json` exists → `dbo push` or `dbo content deploy`
|
|
92
120
|
- **"I want to add a new file"** → `dbo add <path>` (will create metadata interactively)
|
|
93
121
|
- **"I want to pull files from the server"** → `dbo pull` or `dbo pull -e <entity>`
|
|
122
|
+
- **"I want to delete/remove a file"** → `dbo rm <file>` (stages deletion for next `dbo push`)
|
|
123
|
+
- **"I want to delete a directory"** → `dbo rm <directory>` (removes all files + sub-dirs, stages bin deletions)
|
|
94
124
|
- **"I want to see what's on the server"** → `dbo output -e <entity> --format json`
|
|
95
125
|
- **"I need to set up this project"** → `dbo init` → `dbo login` → `dbo status`
|
|
96
126
|
- **"I want to clone an app"** → `dbo clone --app <name>` or `dbo clone <local.json>`
|
|
@@ -211,8 +241,9 @@ Flags: `--app <name>`, `--domain <host>`, `-y/--yes`, `-v/--verbose`
|
|
|
211
241
|
4. Creates directory structure from `children.bin` hierarchy → saves `.dbo/structure.json`
|
|
212
242
|
5. Writes content files (decodes base64) with `*.metadata.json` into bin directories
|
|
213
243
|
6. Downloads media files from server via `/api/media/{uid}` with `*.metadata.json`
|
|
214
|
-
7. Processes
|
|
215
|
-
8.
|
|
244
|
+
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
|
|
245
|
+
8. Processes remaining entities with BinID into corresponding bin directories
|
|
246
|
+
9. Saves `app.json` to project root with `@path/to/*.metadata.json` references
|
|
216
247
|
|
|
217
248
|
### Placement preferences
|
|
218
249
|
|
package/src/commands/update.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { readdir, readFile, copyFile, access, mkdir } from 'fs/promises';
|
|
3
|
-
import { join, dirname } from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
|
-
import { createHash } from 'crypto';
|
|
7
|
-
import { installClaudeCommands } from './install.js';
|
|
8
|
-
import { log } from '../lib/logger.js';
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const CLI_ROOT = join(__dirname, '..', '..');
|
|
12
|
-
const PLUGINS_DIR = join(__dirname, '..', 'plugins', 'claudecommands');
|
|
13
|
-
|
|
14
|
-
async function fileExists(path) {
|
|
15
|
-
try { await access(path); return true; } catch { return false; }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function fileHash(content) {
|
|
19
|
-
return createHash('md5').update(content).digest('hex');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const updateCommand = new Command('update')
|
|
23
|
-
.description('Update dbo-cli, plugins, or Claude Code commands')
|
|
24
|
-
.argument('[target]', 'What to update: cli, plugins, claudecommands')
|
|
25
|
-
.option('--claudecommand <name>', 'Update a specific Claude command by name')
|
|
26
|
-
.action(async (target, options) => {
|
|
27
|
-
try {
|
|
28
|
-
if (options.claudecommand) {
|
|
29
|
-
await updateSpecificCommand(options.claudecommand);
|
|
30
|
-
} else if (target === 'cli') {
|
|
31
|
-
await updateCli();
|
|
32
|
-
} else if (target === 'plugins') {
|
|
33
|
-
await updatePlugins();
|
|
34
|
-
} else if (target === 'claudecommands') {
|
|
35
|
-
await updateClaudeCommands();
|
|
36
|
-
} else {
|
|
37
|
-
// No target: update CLI + ask about claude commands
|
|
38
|
-
await updateCli();
|
|
39
|
-
const inquirer = (await import('inquirer')).default;
|
|
40
|
-
const { updateCmds } = await inquirer.prompt([{
|
|
41
|
-
type: 'confirm', name: 'updateCmds',
|
|
42
|
-
message: 'Also update Claude Code commands?',
|
|
43
|
-
default: true,
|
|
44
|
-
}]);
|
|
45
|
-
if (updateCmds) await updateClaudeCommands();
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
log.error(err.message);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
async function updateCli() {
|
|
54
|
-
// Detect install method: git repo or npm global
|
|
55
|
-
const isGitRepo = await fileExists(join(CLI_ROOT, '.git'));
|
|
56
|
-
const isInNodeModules = CLI_ROOT.includes('node_modules');
|
|
57
|
-
|
|
58
|
-
if (isGitRepo && !isInNodeModules) {
|
|
59
|
-
log.info('Updating dbo-cli from git...');
|
|
60
|
-
try {
|
|
61
|
-
execSync('git pull', { cwd: CLI_ROOT, stdio: 'inherit' });
|
|
62
|
-
execSync('npm install', { cwd: CLI_ROOT, stdio: 'inherit' });
|
|
63
|
-
log.success('dbo-cli updated from git');
|
|
64
|
-
} catch (err) {
|
|
65
|
-
log.error(`Git update failed: ${err.message}`);
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
log.info('Updating dbo-cli via npm...');
|
|
69
|
-
try {
|
|
70
|
-
execSync('npm update -g @dboio/cli', { stdio: 'inherit' });
|
|
71
|
-
log.success('dbo-cli updated via npm');
|
|
72
|
-
} catch (err) {
|
|
73
|
-
log.error(`npm update failed: ${err.message}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Show current version
|
|
78
|
-
try {
|
|
79
|
-
const pkg = JSON.parse(await readFile(join(CLI_ROOT, 'package.json'), 'utf8'));
|
|
80
|
-
log.label('Version', pkg.version);
|
|
81
|
-
} catch { /* ignore */ }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function updatePlugins() {
|
|
85
|
-
log.info('Updating all plugins...');
|
|
86
|
-
await updateClaudeCommands();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function updateClaudeCommands() {
|
|
90
|
-
const commandsDir = join(process.cwd(), '.claude', 'commands');
|
|
91
|
-
|
|
92
|
-
if (!await fileExists(commandsDir)) {
|
|
93
|
-
log.info('No .claude/commands/ directory found. Installing...');
|
|
94
|
-
await installClaudeCommands();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let pluginFiles;
|
|
99
|
-
try {
|
|
100
|
-
pluginFiles = (await readdir(PLUGINS_DIR)).filter(f => f.endsWith('.md'));
|
|
101
|
-
} catch {
|
|
102
|
-
log.error(`Plugin source not found: ${PLUGINS_DIR}`);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let updated = 0;
|
|
107
|
-
let upToDate = 0;
|
|
108
|
-
|
|
109
|
-
for (const file of pluginFiles) {
|
|
110
|
-
const srcPath = join(PLUGINS_DIR, file);
|
|
111
|
-
const destPath = join(commandsDir, file);
|
|
112
|
-
|
|
113
|
-
const srcContent = await readFile(srcPath, 'utf8');
|
|
114
|
-
|
|
115
|
-
if (await fileExists(destPath)) {
|
|
116
|
-
const destContent = await readFile(destPath, 'utf8');
|
|
117
|
-
if (fileHash(srcContent) === fileHash(destContent)) {
|
|
118
|
-
upToDate++;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
await copyFile(srcPath, destPath);
|
|
122
|
-
log.success(`Updated .claude/commands/${file}`);
|
|
123
|
-
updated++;
|
|
124
|
-
} else {
|
|
125
|
-
await copyFile(srcPath, destPath);
|
|
126
|
-
log.success(`Installed .claude/commands/${file} (new)`);
|
|
127
|
-
updated++;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (updated > 0) {
|
|
132
|
-
log.info(`${updated} command(s) updated`);
|
|
133
|
-
log.warn('Note: Updated commands will be available in new Claude Code sessions (restart any active session).');
|
|
134
|
-
}
|
|
135
|
-
if (upToDate > 0) {
|
|
136
|
-
log.dim(`${upToDate} command(s) already up to date`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function updateSpecificCommand(name) {
|
|
141
|
-
const fileName = name.endsWith('.md') ? name : `${name}.md`;
|
|
142
|
-
const srcPath = join(PLUGINS_DIR, fileName);
|
|
143
|
-
const destPath = join(process.cwd(), '.claude', 'commands', fileName);
|
|
144
|
-
|
|
145
|
-
if (!await fileExists(srcPath)) {
|
|
146
|
-
log.error(`Command plugin "${name}" not found in source`);
|
|
147
|
-
const available = (await readdir(PLUGINS_DIR)).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''));
|
|
148
|
-
if (available.length > 0) log.dim(` Available: ${available.join(', ')}`);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!await fileExists(destPath)) {
|
|
153
|
-
log.warn(`"${fileName}" not installed. Run "dbo install --claudecommand ${name}" first.`);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const srcContent = await readFile(srcPath, 'utf8');
|
|
158
|
-
const destContent = await readFile(destPath, 'utf8');
|
|
159
|
-
|
|
160
|
-
if (fileHash(srcContent) === fileHash(destContent)) {
|
|
161
|
-
log.success(`${fileName} is already up to date`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
await copyFile(srcPath, destPath);
|
|
166
|
-
log.success(`Updated .claude/commands/${fileName}`);
|
|
167
|
-
log.warn('Note: Updated commands will be available in new Claude Code sessions (restart any active session).');
|
|
168
|
-
}
|