@dboio/cli 0.4.1 → 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 +268 -87
- package/bin/dbo.js +7 -3
- package/package.json +27 -4
- package/src/commands/clone.js +469 -14
- package/src/commands/diff.js +246 -0
- package/src/commands/init.js +31 -23
- 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 +38 -7
- package/src/commands/update.js +0 -168
package/src/commands/pull.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { DboClient } from '../lib/client.js';
|
|
4
|
+
import { loadConfig } from '../lib/config.js';
|
|
4
5
|
import { formatError } from '../lib/formatter.js';
|
|
5
6
|
import { saveToDisk } from '../lib/save-to-disk.js';
|
|
6
7
|
import { log } from '../lib/logger.js';
|
|
@@ -82,6 +83,7 @@ export const pullCommand = new Command('pull')
|
|
|
82
83
|
|
|
83
84
|
// Content entity has well-known defaults — skip prompts
|
|
84
85
|
const isContent = entity === 'content';
|
|
86
|
+
const config = await loadConfig();
|
|
85
87
|
|
|
86
88
|
if (isContent) {
|
|
87
89
|
await saveToDisk(rows, columns, {
|
|
@@ -94,12 +96,16 @@ export const pullCommand = new Command('pull')
|
|
|
94
96
|
contentFileMap: columns.includes('Content') ? {
|
|
95
97
|
Content: { suffix: '', extensionSource: columns.includes('Extension') ? 'Extension' : null, extension: 'txt' },
|
|
96
98
|
} : null,
|
|
99
|
+
changeDetection: true,
|
|
100
|
+
config,
|
|
97
101
|
});
|
|
98
102
|
} else {
|
|
99
103
|
// Other entities: interactive prompts for column selection
|
|
100
104
|
await saveToDisk(rows, columns, {
|
|
101
105
|
entity,
|
|
102
106
|
nonInteractive: false,
|
|
107
|
+
changeDetection: true,
|
|
108
|
+
config,
|
|
103
109
|
});
|
|
104
110
|
}
|
|
105
111
|
} catch (err) {
|
package/src/commands/push.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readFile,
|
|
2
|
+
import { readFile, stat, writeFile } from 'fs/promises';
|
|
3
3
|
import { join, dirname, basename, extname, relative } from 'path';
|
|
4
4
|
import { DboClient } from '../lib/client.js';
|
|
5
5
|
import { buildInputBody, checkSubmitErrors } from '../lib/input-parser.js';
|
|
6
6
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
7
7
|
import { log } from '../lib/logger.js';
|
|
8
8
|
import { shouldSkipColumn } from '../lib/columns.js';
|
|
9
|
-
import { loadConfig } from '../lib/config.js';
|
|
9
|
+
import { loadConfig, loadSynchronize, saveSynchronize } from '../lib/config.js';
|
|
10
10
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
11
|
+
import { findMetadataFiles } from '../lib/diff.js';
|
|
11
12
|
|
|
12
13
|
export const pushCommand = new Command('push')
|
|
13
14
|
.description('Push local files back to DBO.io using metadata from pull')
|
|
@@ -24,6 +25,10 @@ export const pushCommand = new Command('push')
|
|
|
24
25
|
.action(async (targetPath, options) => {
|
|
25
26
|
try {
|
|
26
27
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
28
|
+
|
|
29
|
+
// Process pending deletions from synchronize.json
|
|
30
|
+
await processPendingDeletes(client, options);
|
|
31
|
+
|
|
27
32
|
const pathStat = await stat(targetPath);
|
|
28
33
|
|
|
29
34
|
if (pathStat.isDirectory()) {
|
|
@@ -37,6 +42,62 @@ export const pushCommand = new Command('push')
|
|
|
37
42
|
}
|
|
38
43
|
});
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Process pending delete entries from .dbo/synchronize.json
|
|
47
|
+
*/
|
|
48
|
+
async function processPendingDeletes(client, options) {
|
|
49
|
+
const sync = await loadSynchronize();
|
|
50
|
+
if (!sync.delete || sync.delete.length === 0) return;
|
|
51
|
+
|
|
52
|
+
log.info(`Processing ${sync.delete.length} pending deletion(s)...`);
|
|
53
|
+
|
|
54
|
+
const remaining = [];
|
|
55
|
+
for (const entry of sync.delete) {
|
|
56
|
+
log.info(`Deleting "${entry.name}" (${entry.entity}:${entry.RowID})`);
|
|
57
|
+
|
|
58
|
+
const extraParams = { '_confirm': options.confirm || 'true' };
|
|
59
|
+
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
60
|
+
|
|
61
|
+
const body = await buildInputBody([entry.expression], extraParams);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await client.postUrlEncoded('/api/input/submit', body);
|
|
65
|
+
|
|
66
|
+
// Retry with prompted params if needed
|
|
67
|
+
const retryParams = await checkSubmitErrors(result);
|
|
68
|
+
if (retryParams) {
|
|
69
|
+
Object.assign(extraParams, retryParams);
|
|
70
|
+
const retryBody = await buildInputBody([entry.expression], extraParams);
|
|
71
|
+
const retryResult = await client.postUrlEncoded('/api/input/submit', retryBody);
|
|
72
|
+
if (retryResult.successful) {
|
|
73
|
+
log.success(` Deleted "${entry.name}" from server`);
|
|
74
|
+
} else {
|
|
75
|
+
log.error(` Failed to delete "${entry.name}"`);
|
|
76
|
+
formatResponse(retryResult, { json: options.json, jq: options.jq });
|
|
77
|
+
remaining.push(entry);
|
|
78
|
+
}
|
|
79
|
+
} else if (result.successful) {
|
|
80
|
+
log.success(` Deleted "${entry.name}" from server`);
|
|
81
|
+
} else {
|
|
82
|
+
log.error(` Failed to delete "${entry.name}"`);
|
|
83
|
+
formatResponse(result, { json: options.json, jq: options.jq });
|
|
84
|
+
remaining.push(entry);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.error(` Failed to delete "${entry.name}": ${err.message}`);
|
|
88
|
+
remaining.push(entry);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Update synchronize.json with any remaining entries
|
|
93
|
+
sync.delete = remaining;
|
|
94
|
+
await saveSynchronize(sync);
|
|
95
|
+
|
|
96
|
+
if (remaining.length > 0) {
|
|
97
|
+
log.warn(`${remaining.length} deletion(s) failed and remain staged.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
40
101
|
/**
|
|
41
102
|
* Push a single file using its companion .metadata.json
|
|
42
103
|
*/
|
|
@@ -288,22 +349,3 @@ async function checkPathMismatch(meta, metaPath, options) {
|
|
|
288
349
|
log.success(` Path updated to "${currentPath}"`);
|
|
289
350
|
}
|
|
290
351
|
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Recursively find all .metadata.json files in a directory
|
|
294
|
-
*/
|
|
295
|
-
async function findMetadataFiles(dir) {
|
|
296
|
-
const results = [];
|
|
297
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
298
|
-
|
|
299
|
-
for (const entry of entries) {
|
|
300
|
-
const fullPath = join(dir, entry.name);
|
|
301
|
-
if (entry.isDirectory()) {
|
|
302
|
-
results.push(...await findMetadataFiles(fullPath));
|
|
303
|
-
} else if (entry.name.endsWith('.metadata.json')) {
|
|
304
|
-
results.push(fullPath);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return results;
|
|
309
|
-
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFile, unlink, stat, rm as fsRm } from 'fs/promises';
|
|
3
|
+
import { join, dirname, basename, extname } from 'path';
|
|
4
|
+
import { log } from '../lib/logger.js';
|
|
5
|
+
import { formatError } from '../lib/formatter.js';
|
|
6
|
+
import { addDeleteEntry, removeAppJsonReference } from '../lib/config.js';
|
|
7
|
+
import { findMetadataFiles } from '../lib/diff.js';
|
|
8
|
+
import { loadStructureFile, findBinByPath, findChildBins, BINS_DIR } from '../lib/structure.js';
|
|
9
|
+
|
|
10
|
+
export const rmCommand = new Command('rm')
|
|
11
|
+
.description('Remove a file or directory locally and stage server deletions for the next dbo push')
|
|
12
|
+
.argument('<path>', 'File, metadata.json, or directory to remove')
|
|
13
|
+
.option('-f, --force', 'Skip confirmation prompts')
|
|
14
|
+
.option('--keep-local', 'Only stage server deletion, do not delete local files')
|
|
15
|
+
.action(async (targetPath, options) => {
|
|
16
|
+
try {
|
|
17
|
+
const pathStat = await stat(targetPath).catch(() => null);
|
|
18
|
+
if (!pathStat) {
|
|
19
|
+
log.error(`Path not found: "${targetPath}"`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (pathStat.isDirectory()) {
|
|
24
|
+
await rmDirectory(targetPath, options);
|
|
25
|
+
} else {
|
|
26
|
+
await rmFile(targetPath, options);
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
formatError(err);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ── File removal (existing logic) ──────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a file path to its metadata.json path.
|
|
38
|
+
*/
|
|
39
|
+
function resolveMetaPath(filePath) {
|
|
40
|
+
if (filePath.endsWith('.metadata.json')) {
|
|
41
|
+
return filePath;
|
|
42
|
+
}
|
|
43
|
+
const dir = dirname(filePath);
|
|
44
|
+
const ext = extname(filePath);
|
|
45
|
+
const base = basename(filePath, ext);
|
|
46
|
+
return join(dir, `${base}.metadata.json`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the entity-specific row ID from metadata.
|
|
51
|
+
* Checks _id first (shorthand), then derives from entity name.
|
|
52
|
+
*/
|
|
53
|
+
function getRowId(meta) {
|
|
54
|
+
if (meta._id) return meta._id;
|
|
55
|
+
if (meta._entity) {
|
|
56
|
+
const entityIdKey = meta._entity.charAt(0).toUpperCase() + meta._entity.slice(1) + 'ID';
|
|
57
|
+
if (meta[entityIdKey]) return meta[entityIdKey];
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove a single file record: stage deletion, remove from app.json, delete local files.
|
|
64
|
+
* Returns true if removed, false if skipped.
|
|
65
|
+
*/
|
|
66
|
+
async function rmFileRecord(metaPath, options, { skipPrompt = false } = {}) {
|
|
67
|
+
let meta;
|
|
68
|
+
try {
|
|
69
|
+
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
log.warn(` Could not read metadata: ${metaPath}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entity = meta._entity;
|
|
76
|
+
const uid = meta.UID;
|
|
77
|
+
const rowId = getRowId(meta);
|
|
78
|
+
|
|
79
|
+
if (!entity || !rowId) {
|
|
80
|
+
log.warn(` Skipping "${metaPath}": missing _entity or row ID`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Collect local files
|
|
85
|
+
const metaDir = dirname(metaPath);
|
|
86
|
+
const localFiles = [metaPath];
|
|
87
|
+
|
|
88
|
+
for (const col of (meta._contentColumns || [])) {
|
|
89
|
+
const ref = meta[col];
|
|
90
|
+
if (ref && String(ref).startsWith('@')) {
|
|
91
|
+
localFiles.push(join(metaDir, String(ref).substring(1)));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
96
|
+
localFiles.push(join(metaDir, String(meta._mediaFile).substring(1)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const displayName = basename(metaPath, '.metadata.json');
|
|
100
|
+
|
|
101
|
+
// Prompt if needed
|
|
102
|
+
if (!skipPrompt && !options.force) {
|
|
103
|
+
const inquirer = (await import('inquirer')).default;
|
|
104
|
+
const { confirm } = await inquirer.prompt([{
|
|
105
|
+
type: 'confirm',
|
|
106
|
+
name: 'confirm',
|
|
107
|
+
message: `Remove "${displayName}" (${entity}:${uid || rowId})?`,
|
|
108
|
+
default: true,
|
|
109
|
+
}]);
|
|
110
|
+
if (!confirm) {
|
|
111
|
+
log.dim(` Skipped ${displayName}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Stage deletion
|
|
117
|
+
const expression = `RowID:del${rowId};entity:${entity}=true`;
|
|
118
|
+
await addDeleteEntry({ UID: uid, RowID: rowId, entity, name: displayName, expression });
|
|
119
|
+
log.success(` Staged: ${displayName} → ${expression}`);
|
|
120
|
+
|
|
121
|
+
// Remove from app.json
|
|
122
|
+
await removeAppJsonReference(metaPath);
|
|
123
|
+
|
|
124
|
+
// Delete local files
|
|
125
|
+
if (!options.keepLocal) {
|
|
126
|
+
for (const f of localFiles) {
|
|
127
|
+
try {
|
|
128
|
+
await unlink(f);
|
|
129
|
+
log.dim(` Deleted ${f}`);
|
|
130
|
+
} catch { /* file may not exist */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Remove a single file (entry point for non-directory rm).
|
|
139
|
+
*/
|
|
140
|
+
async function rmFile(filePath, options) {
|
|
141
|
+
const metaPath = resolveMetaPath(filePath);
|
|
142
|
+
|
|
143
|
+
let meta;
|
|
144
|
+
try {
|
|
145
|
+
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
146
|
+
} catch {
|
|
147
|
+
log.error(`No metadata found at "${metaPath}". Cannot determine record to delete.`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const entity = meta._entity;
|
|
152
|
+
const uid = meta.UID;
|
|
153
|
+
const rowId = getRowId(meta);
|
|
154
|
+
|
|
155
|
+
if (!entity) {
|
|
156
|
+
log.error(`No _entity found in "${metaPath}".`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
if (!rowId) {
|
|
160
|
+
log.error(`No row ID found in "${metaPath}". Cannot build delete expression.`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Collect local files for display
|
|
165
|
+
const metaDir = dirname(metaPath);
|
|
166
|
+
const localFiles = [metaPath];
|
|
167
|
+
for (const col of (meta._contentColumns || [])) {
|
|
168
|
+
const ref = meta[col];
|
|
169
|
+
if (ref && String(ref).startsWith('@')) {
|
|
170
|
+
localFiles.push(join(metaDir, String(ref).substring(1)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
174
|
+
localFiles.push(join(metaDir, String(meta._mediaFile).substring(1)));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const displayName = basename(metaPath, '.metadata.json');
|
|
178
|
+
log.info(`Removing "${displayName}" (${entity}:${uid || rowId})`);
|
|
179
|
+
for (const f of localFiles) {
|
|
180
|
+
log.dim(` ${f}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Confirmation
|
|
184
|
+
if (!options.force) {
|
|
185
|
+
const inquirer = (await import('inquirer')).default;
|
|
186
|
+
const { confirm } = await inquirer.prompt([{
|
|
187
|
+
type: 'confirm',
|
|
188
|
+
name: 'confirm',
|
|
189
|
+
message: 'Do you really want to remove this file and all of its nodes?\n The next `dbo push` will also remove the record from the server.',
|
|
190
|
+
default: false,
|
|
191
|
+
}]);
|
|
192
|
+
if (!confirm) {
|
|
193
|
+
log.dim('Cancelled.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const expression = `RowID:del${rowId};entity:${entity}=true`;
|
|
199
|
+
await addDeleteEntry({ UID: uid, RowID: rowId, entity, name: displayName, expression });
|
|
200
|
+
log.success(`Staged deletion: ${expression}`);
|
|
201
|
+
|
|
202
|
+
await removeAppJsonReference(metaPath);
|
|
203
|
+
|
|
204
|
+
if (!options.keepLocal) {
|
|
205
|
+
for (const f of localFiles) {
|
|
206
|
+
try {
|
|
207
|
+
await unlink(f);
|
|
208
|
+
log.dim(` Deleted ${f}`);
|
|
209
|
+
} catch {
|
|
210
|
+
log.warn(` Could not delete ${f} (may not exist)`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
log.info('Run `dbo push` to apply the deletion on the server.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Directory removal ──────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Collect bins in depth-first order (leaves first) for safe deletion.
|
|
222
|
+
*/
|
|
223
|
+
function collectBinsDepthFirst(binId, structure) {
|
|
224
|
+
const result = [];
|
|
225
|
+
const children = findChildBins(binId, structure);
|
|
226
|
+
for (const child of children) {
|
|
227
|
+
result.push(...collectBinsDepthFirst(child.binId, structure));
|
|
228
|
+
}
|
|
229
|
+
result.push(structure[binId]);
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Remove a directory: all files inside, sub-bins recursively, then the bin itself.
|
|
235
|
+
*/
|
|
236
|
+
async function rmDirectory(dirPath, options) {
|
|
237
|
+
const structure = await loadStructureFile();
|
|
238
|
+
const bin = findBinByPath(dirPath, structure);
|
|
239
|
+
|
|
240
|
+
if (!bin) {
|
|
241
|
+
log.error(`Directory "${dirPath}" is not a known bin in structure.json.`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Collect all bins depth-first (leaves first)
|
|
246
|
+
const binsToRemove = collectBinsDepthFirst(bin.binId, structure);
|
|
247
|
+
|
|
248
|
+
// Count total files across all directories
|
|
249
|
+
let totalFiles = 0;
|
|
250
|
+
const binFilesMap = new Map(); // binId → [metaFiles]
|
|
251
|
+
for (const b of binsToRemove) {
|
|
252
|
+
const binDir = `${BINS_DIR}/${b.fullPath}`;
|
|
253
|
+
try {
|
|
254
|
+
const metaFiles = await findMetadataFiles(binDir);
|
|
255
|
+
binFilesMap.set(b.binId, metaFiles);
|
|
256
|
+
totalFiles += metaFiles.length;
|
|
257
|
+
} catch {
|
|
258
|
+
binFilesMap.set(b.binId, []);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Summary
|
|
263
|
+
log.info(`Directory "${dirPath}" contains ${totalFiles} file(s) across ${binsToRemove.length} directory(ies)`);
|
|
264
|
+
for (const b of binsToRemove) {
|
|
265
|
+
const files = binFilesMap.get(b.binId) || [];
|
|
266
|
+
log.dim(` ${BINS_DIR}/${b.fullPath}/ (${files.length} files, bin:${b.binId})`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Prompt for approach
|
|
270
|
+
let bulkRemove = options.force;
|
|
271
|
+
if (!options.force && totalFiles > 0) {
|
|
272
|
+
const inquirer = (await import('inquirer')).default;
|
|
273
|
+
const { action } = await inquirer.prompt([{
|
|
274
|
+
type: 'list',
|
|
275
|
+
name: 'action',
|
|
276
|
+
message: `How do you want to proceed?`,
|
|
277
|
+
choices: [
|
|
278
|
+
{ name: 'Remove all files and directories', value: 'all' },
|
|
279
|
+
{ name: 'Prompt for each file', value: 'each' },
|
|
280
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
281
|
+
],
|
|
282
|
+
}]);
|
|
283
|
+
|
|
284
|
+
if (action === 'cancel') {
|
|
285
|
+
log.dim('Cancelled.');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
bulkRemove = action === 'all';
|
|
289
|
+
} else if (!options.force && totalFiles === 0) {
|
|
290
|
+
// Empty directory — just confirm bin deletion
|
|
291
|
+
const inquirer = (await import('inquirer')).default;
|
|
292
|
+
const { confirm } = await inquirer.prompt([{
|
|
293
|
+
type: 'confirm',
|
|
294
|
+
name: 'confirm',
|
|
295
|
+
message: `Remove empty directory "${dirPath}" (bin:${bin.binId})?\n The next \`dbo push\` will also remove it from the server.`,
|
|
296
|
+
default: true,
|
|
297
|
+
}]);
|
|
298
|
+
if (!confirm) {
|
|
299
|
+
log.dim('Cancelled.');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
bulkRemove = true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Process each bin (depth-first: leaves first)
|
|
306
|
+
for (const b of binsToRemove) {
|
|
307
|
+
const metaFiles = binFilesMap.get(b.binId) || [];
|
|
308
|
+
|
|
309
|
+
// Remove files in this bin
|
|
310
|
+
for (const metaPath of metaFiles) {
|
|
311
|
+
await rmFileRecord(metaPath, options, { skipPrompt: bulkRemove });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Stage the bin itself for deletion
|
|
315
|
+
const binExpression = `RowID:del${b.binId};entity:bin=true`;
|
|
316
|
+
await addDeleteEntry({
|
|
317
|
+
UID: b.uid,
|
|
318
|
+
RowID: b.binId,
|
|
319
|
+
entity: 'bin',
|
|
320
|
+
name: b.name,
|
|
321
|
+
expression: binExpression,
|
|
322
|
+
});
|
|
323
|
+
log.success(` Staged bin: ${b.name} (${b.binId}) → ${binExpression}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Remove local directories
|
|
327
|
+
if (!options.keepLocal) {
|
|
328
|
+
try {
|
|
329
|
+
await fsRm(dirPath, { recursive: true });
|
|
330
|
+
log.dim(` Removed directory: ${dirPath}`);
|
|
331
|
+
} catch {
|
|
332
|
+
log.warn(` Could not remove directory: ${dirPath}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
log.info('Run `dbo push` to apply the deletions on the server.');
|
|
337
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { loadConfig, isInitialized, getActiveCookiesPath, loadUserInfo } from '../lib/config.js';
|
|
2
|
+
import { loadConfig, isInitialized, getActiveCookiesPath, loadUserInfo, getAllPluginScopes } from '../lib/config.js';
|
|
3
3
|
import { loadCookies } from '../lib/cookie-jar.js';
|
|
4
|
+
import { access } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
4
7
|
import { log } from '../lib/logger.js';
|
|
5
8
|
|
|
6
9
|
export const statusCommand = new Command('status')
|
|
@@ -34,6 +37,30 @@ export const statusCommand = new Command('status')
|
|
|
34
37
|
} else {
|
|
35
38
|
log.label('Session', 'No active session. Run "dbo login".');
|
|
36
39
|
}
|
|
40
|
+
|
|
41
|
+
// Display plugin status
|
|
42
|
+
const scopes = await getAllPluginScopes();
|
|
43
|
+
const pluginNames = Object.keys(scopes);
|
|
44
|
+
|
|
45
|
+
if (pluginNames.length > 0) {
|
|
46
|
+
log.plain('');
|
|
47
|
+
log.info('Claude Code Plugins:');
|
|
48
|
+
for (const [name, scope] of Object.entries(scopes)) {
|
|
49
|
+
const resolvedScope = scope || 'project';
|
|
50
|
+
const fileName = `${name}.md`;
|
|
51
|
+
let location;
|
|
52
|
+
if (resolvedScope === 'global') {
|
|
53
|
+
location = join(homedir(), '.claude', 'commands', fileName);
|
|
54
|
+
} else {
|
|
55
|
+
location = join(process.cwd(), '.claude', 'commands', fileName);
|
|
56
|
+
}
|
|
57
|
+
let installed = false;
|
|
58
|
+
try { await access(location); installed = true; } catch {}
|
|
59
|
+
const icon = installed ? '\u2713' : '\u2717';
|
|
60
|
+
const scopeLabel = resolvedScope === 'global' ? 'global' : 'project';
|
|
61
|
+
log.label(` ${name}`, `${icon} ${scopeLabel} (${installed ? location : 'not found'})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
37
64
|
} catch (err) {
|
|
38
65
|
log.error(err.message);
|
|
39
66
|
process.exit(1);
|