@dboio/cli 0.4.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 +1161 -0
- package/bin/dbo.js +51 -0
- package/package.json +22 -0
- package/src/commands/add.js +374 -0
- package/src/commands/cache.js +49 -0
- package/src/commands/clone.js +742 -0
- package/src/commands/content.js +143 -0
- package/src/commands/deploy.js +89 -0
- package/src/commands/init.js +105 -0
- package/src/commands/input.js +111 -0
- package/src/commands/install.js +186 -0
- package/src/commands/instance.js +44 -0
- package/src/commands/login.js +97 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/media.js +46 -0
- package/src/commands/message.js +28 -0
- package/src/commands/output.js +129 -0
- package/src/commands/pull.js +109 -0
- package/src/commands/push.js +309 -0
- package/src/commands/status.js +41 -0
- package/src/commands/update.js +168 -0
- package/src/commands/upload.js +37 -0
- package/src/lib/client.js +161 -0
- package/src/lib/columns.js +30 -0
- package/src/lib/config.js +269 -0
- package/src/lib/cookie-jar.js +104 -0
- package/src/lib/formatter.js +310 -0
- package/src/lib/input-parser.js +212 -0
- package/src/lib/logger.js +12 -0
- package/src/lib/save-to-disk.js +383 -0
- package/src/lib/structure.js +129 -0
- package/src/lib/timestamps.js +67 -0
- package/src/plugins/claudecommands/dbo.md +248 -0
package/bin/dbo.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { initCommand } from '../src/commands/init.js';
|
|
5
|
+
import { loginCommand } from '../src/commands/login.js';
|
|
6
|
+
import { logoutCommand } from '../src/commands/logout.js';
|
|
7
|
+
import { statusCommand } from '../src/commands/status.js';
|
|
8
|
+
import { inputCommand } from '../src/commands/input.js';
|
|
9
|
+
import { outputCommand } from '../src/commands/output.js';
|
|
10
|
+
import { contentCommand } from '../src/commands/content.js';
|
|
11
|
+
import { mediaCommand } from '../src/commands/media.js';
|
|
12
|
+
import { uploadCommand } from '../src/commands/upload.js';
|
|
13
|
+
import { messageCommand } from '../src/commands/message.js';
|
|
14
|
+
import { cacheCommand } from '../src/commands/cache.js';
|
|
15
|
+
import { instanceCommand } from '../src/commands/instance.js';
|
|
16
|
+
import { deployCommand } from '../src/commands/deploy.js';
|
|
17
|
+
import { pushCommand } from '../src/commands/push.js';
|
|
18
|
+
import { pullCommand } from '../src/commands/pull.js';
|
|
19
|
+
import { addCommand } from '../src/commands/add.js';
|
|
20
|
+
import { installCommand } from '../src/commands/install.js';
|
|
21
|
+
import { updateCommand } from '../src/commands/update.js';
|
|
22
|
+
import { cloneCommand } from '../src/commands/clone.js';
|
|
23
|
+
|
|
24
|
+
const program = new Command();
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('dbo')
|
|
28
|
+
.description('CLI for the DBO.io framework')
|
|
29
|
+
.version('0.4.1');
|
|
30
|
+
|
|
31
|
+
program.addCommand(initCommand);
|
|
32
|
+
program.addCommand(loginCommand);
|
|
33
|
+
program.addCommand(logoutCommand);
|
|
34
|
+
program.addCommand(statusCommand);
|
|
35
|
+
program.addCommand(inputCommand);
|
|
36
|
+
program.addCommand(outputCommand);
|
|
37
|
+
program.addCommand(contentCommand);
|
|
38
|
+
program.addCommand(mediaCommand);
|
|
39
|
+
program.addCommand(uploadCommand);
|
|
40
|
+
program.addCommand(messageCommand);
|
|
41
|
+
program.addCommand(cacheCommand);
|
|
42
|
+
program.addCommand(instanceCommand);
|
|
43
|
+
program.addCommand(deployCommand);
|
|
44
|
+
program.addCommand(pushCommand);
|
|
45
|
+
program.addCommand(pullCommand);
|
|
46
|
+
program.addCommand(addCommand);
|
|
47
|
+
program.addCommand(installCommand);
|
|
48
|
+
program.addCommand(updateCommand);
|
|
49
|
+
program.addCommand(cloneCommand);
|
|
50
|
+
|
|
51
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dboio/cli",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "CLI for the DBO.io framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dbo": "./bin/dbo.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test src/**/*.test.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"commander": "^12.1.0",
|
|
17
|
+
"chalk": "^5.3.0",
|
|
18
|
+
"inquirer": "^9.3.0"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["dbo", "dboio", "database", "cli", "api"],
|
|
21
|
+
"license": "UNLICENSED"
|
|
22
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFile, readdir, stat, writeFile } from 'fs/promises';
|
|
3
|
+
import { join, dirname, basename, extname, relative } from 'path';
|
|
4
|
+
import { DboClient } from '../lib/client.js';
|
|
5
|
+
import { buildInputBody, checkSubmitErrors } from '../lib/input-parser.js';
|
|
6
|
+
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
7
|
+
import { log } from '../lib/logger.js';
|
|
8
|
+
import { shouldSkipColumn } from '../lib/columns.js';
|
|
9
|
+
import { loadAppConfig } from '../lib/config.js';
|
|
10
|
+
|
|
11
|
+
// Directories and patterns to skip when scanning with `dbo add .`
|
|
12
|
+
const IGNORE_DIRS = new Set(['.dbo', '.git', 'node_modules', '.svn', '.hg']);
|
|
13
|
+
|
|
14
|
+
export const addCommand = new Command('add')
|
|
15
|
+
.description('Add a new file to DBO.io (creates record on server)')
|
|
16
|
+
.argument('<path>', 'File or "." to scan current directory')
|
|
17
|
+
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
18
|
+
.option('--ticket <id>', 'Override ticket ID')
|
|
19
|
+
.option('-y, --yes', 'Auto-accept all prompts')
|
|
20
|
+
.option('--json', 'Output raw JSON')
|
|
21
|
+
.option('--jq <expr>', 'Filter JSON response')
|
|
22
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
23
|
+
.option('--domain <host>', 'Override domain')
|
|
24
|
+
.action(async (targetPath, options) => {
|
|
25
|
+
try {
|
|
26
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
27
|
+
|
|
28
|
+
if (targetPath === '.') {
|
|
29
|
+
await addDirectory(process.cwd(), client, options);
|
|
30
|
+
} else {
|
|
31
|
+
const pathStat = await stat(targetPath);
|
|
32
|
+
if (pathStat.isDirectory()) {
|
|
33
|
+
await addDirectory(targetPath, client, options);
|
|
34
|
+
} else {
|
|
35
|
+
await addSingleFile(targetPath, client, options);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
formatError(err);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Add a single file to DBO.io.
|
|
46
|
+
* Returns the defaults used (entity, contentColumn) for batch reuse.
|
|
47
|
+
*/
|
|
48
|
+
async function addSingleFile(filePath, client, options, batchDefaults) {
|
|
49
|
+
const dir = dirname(filePath);
|
|
50
|
+
const ext = extname(filePath);
|
|
51
|
+
const base = basename(filePath, ext);
|
|
52
|
+
const fileName = basename(filePath);
|
|
53
|
+
const metaPath = join(dir, `${base}.metadata.json`);
|
|
54
|
+
|
|
55
|
+
// Step 1: Check for existing metadata
|
|
56
|
+
let meta = null;
|
|
57
|
+
try {
|
|
58
|
+
const raw = await readFile(metaPath, 'utf8');
|
|
59
|
+
if (raw.trim()) {
|
|
60
|
+
meta = JSON.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// No metadata file — that's fine, we'll create one
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Step 2: If metadata exists with _CreatedOn, already on server
|
|
67
|
+
if (meta && meta._CreatedOn) {
|
|
68
|
+
log.warn(`"${fileName}" is already on the server (has _CreatedOn). Use "dbo push" to update.`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 3: If metadata exists without _CreatedOn, use it for insert
|
|
73
|
+
if (meta && meta._entity) {
|
|
74
|
+
log.info(`Found metadata for "${fileName}" — proceeding with insert`);
|
|
75
|
+
return await submitAdd(meta, metaPath, filePath, client, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 4: No usable metadata — interactive wizard
|
|
79
|
+
const inquirer = (await import('inquirer')).default;
|
|
80
|
+
|
|
81
|
+
log.plain('');
|
|
82
|
+
log.warn(`I cannot add "${fileName}" to the server, because I don't have any metadata.`);
|
|
83
|
+
log.plain(`I have a few things that I need, and I can set that up for you,`);
|
|
84
|
+
log.plain(`or you manually add "${base}.metadata.json" next to the file.`);
|
|
85
|
+
log.plain('');
|
|
86
|
+
|
|
87
|
+
if (!options.yes) {
|
|
88
|
+
const { proceed } = await inquirer.prompt([{
|
|
89
|
+
type: 'confirm',
|
|
90
|
+
name: 'proceed',
|
|
91
|
+
message: 'Want me to set that up for you?',
|
|
92
|
+
default: true,
|
|
93
|
+
}]);
|
|
94
|
+
|
|
95
|
+
if (!proceed) {
|
|
96
|
+
log.dim(`Skipping "${fileName}". Create "${base}.metadata.json" manually and try again.`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Prompt for required fields (reuse batch defaults if available)
|
|
102
|
+
// Check if config has AppID for smart default
|
|
103
|
+
const appConfig = await loadAppConfig();
|
|
104
|
+
|
|
105
|
+
const answers = await inquirer.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: 'input',
|
|
108
|
+
name: 'entity',
|
|
109
|
+
message: 'Entity name (e.g. content, template, style):',
|
|
110
|
+
default: batchDefaults?.entity || 'content',
|
|
111
|
+
validate: v => v.trim() ? true : 'Entity is required',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'input',
|
|
115
|
+
name: 'contentColumn',
|
|
116
|
+
message: 'Column name for the file content (e.g. Content, Body, Template):',
|
|
117
|
+
default: batchDefaults?.contentColumn || 'Content',
|
|
118
|
+
validate: v => v.trim() ? true : 'Column name is required',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: 'input',
|
|
122
|
+
name: 'BinID',
|
|
123
|
+
message: 'BinID (optional, press Enter to skip):',
|
|
124
|
+
default: batchDefaults?.BinID || '',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'input',
|
|
128
|
+
name: 'SiteID',
|
|
129
|
+
message: 'SiteID (optional, press Enter to skip):',
|
|
130
|
+
default: batchDefaults?.SiteID || '',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'input',
|
|
134
|
+
name: 'Path',
|
|
135
|
+
message: 'Path (optional, press Enter for auto-generated):',
|
|
136
|
+
default: batchDefaults?.Path || relative(process.cwd(), filePath).replace(/\\/g, '/'),
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
// AppID: smart prompt when config has app info
|
|
141
|
+
if (batchDefaults?.AppID) {
|
|
142
|
+
answers.AppID = batchDefaults.AppID;
|
|
143
|
+
} else if (appConfig.AppID) {
|
|
144
|
+
const { appIdChoice } = await inquirer.prompt([{
|
|
145
|
+
type: 'list',
|
|
146
|
+
name: 'appIdChoice',
|
|
147
|
+
message: `You're submitting data without an AppID, but your config has information about the current App. Do you want me to add that Column information along with your submission?`,
|
|
148
|
+
choices: [
|
|
149
|
+
{ name: `Yes, use AppID ${appConfig.AppID}`, value: 'use_config' },
|
|
150
|
+
{ name: 'No', value: 'none' },
|
|
151
|
+
{ name: 'Enter custom AppID', value: 'custom' },
|
|
152
|
+
],
|
|
153
|
+
}]);
|
|
154
|
+
if (appIdChoice === 'use_config') {
|
|
155
|
+
answers.AppID = String(appConfig.AppID);
|
|
156
|
+
} else if (appIdChoice === 'custom') {
|
|
157
|
+
const { customAppId } = await inquirer.prompt([{
|
|
158
|
+
type: 'input', name: 'customAppId',
|
|
159
|
+
message: 'Custom AppID:',
|
|
160
|
+
}]);
|
|
161
|
+
answers.AppID = customAppId;
|
|
162
|
+
} else {
|
|
163
|
+
answers.AppID = '';
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
const { appId } = await inquirer.prompt([{
|
|
167
|
+
type: 'input', name: 'appId',
|
|
168
|
+
message: 'AppID (optional, press Enter to skip):',
|
|
169
|
+
default: '',
|
|
170
|
+
}]);
|
|
171
|
+
answers.AppID = appId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build metadata object
|
|
175
|
+
meta = {};
|
|
176
|
+
if (answers.AppID.trim()) meta.AppID = isFinite(answers.AppID) ? Number(answers.AppID) : answers.AppID;
|
|
177
|
+
if (answers.BinID.trim()) meta.BinID = isFinite(answers.BinID) ? Number(answers.BinID) : answers.BinID;
|
|
178
|
+
if (answers.SiteID.trim()) meta.SiteID = isFinite(answers.SiteID) ? Number(answers.SiteID) : answers.SiteID;
|
|
179
|
+
meta.Name = base;
|
|
180
|
+
if (answers.Path.trim()) meta.Path = answers.Path.trim();
|
|
181
|
+
meta[answers.contentColumn.trim()] = `@${fileName}`;
|
|
182
|
+
meta._entity = answers.entity.trim();
|
|
183
|
+
meta._contentColumns = [answers.contentColumn.trim()];
|
|
184
|
+
|
|
185
|
+
// Write metadata file
|
|
186
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
187
|
+
log.success(`Created ${basename(metaPath)}`);
|
|
188
|
+
|
|
189
|
+
// Submit the insert
|
|
190
|
+
const defaults = {
|
|
191
|
+
entity: answers.entity.trim(),
|
|
192
|
+
contentColumn: answers.contentColumn.trim(),
|
|
193
|
+
AppID: answers.AppID,
|
|
194
|
+
BinID: answers.BinID,
|
|
195
|
+
SiteID: answers.SiteID,
|
|
196
|
+
Path: answers.Path,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await submitAdd(meta, metaPath, filePath, client, options);
|
|
200
|
+
return defaults;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Submit an insert to the server from a metadata object.
|
|
205
|
+
*/
|
|
206
|
+
async function submitAdd(meta, metaPath, filePath, client, options) {
|
|
207
|
+
const entity = meta._entity;
|
|
208
|
+
const contentCols = new Set(meta._contentColumns || []);
|
|
209
|
+
const metaDir = dirname(metaPath);
|
|
210
|
+
|
|
211
|
+
if (!entity) {
|
|
212
|
+
throw new Error(`No _entity found in ${metaPath}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const dataExprs = [];
|
|
216
|
+
let addIndex = 1;
|
|
217
|
+
|
|
218
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
219
|
+
if (shouldSkipColumn(key)) continue;
|
|
220
|
+
if (key === 'UID') continue; // Never submit UID on add — server assigns it
|
|
221
|
+
if (value === null || value === undefined) continue;
|
|
222
|
+
|
|
223
|
+
const strValue = String(value);
|
|
224
|
+
|
|
225
|
+
if (strValue.startsWith('@')) {
|
|
226
|
+
// @filename reference — resolve to actual file path
|
|
227
|
+
const refFile = strValue.substring(1);
|
|
228
|
+
const refPath = join(metaDir, refFile);
|
|
229
|
+
dataExprs.push(`RowID:add${addIndex};column:${entity}.${key}@${refPath}`);
|
|
230
|
+
} else {
|
|
231
|
+
dataExprs.push(`RowID:add${addIndex};column:${entity}.${key}=${strValue}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (dataExprs.length === 0) {
|
|
236
|
+
log.warn(`Nothing to submit for ${basename(metaPath)}`);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
log.info(`Adding ${basename(filePath)} (${entity}) — ${dataExprs.length} field(s)`);
|
|
241
|
+
|
|
242
|
+
const extraParams = { '_confirm': options.confirm };
|
|
243
|
+
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
244
|
+
|
|
245
|
+
let body = await buildInputBody(dataExprs, extraParams);
|
|
246
|
+
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
247
|
+
|
|
248
|
+
// Retry with prompted params if needed (ticket, user)
|
|
249
|
+
const retryParams = await checkSubmitErrors(result);
|
|
250
|
+
if (retryParams) {
|
|
251
|
+
Object.assign(extraParams, retryParams);
|
|
252
|
+
body = await buildInputBody(dataExprs, extraParams);
|
|
253
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
formatResponse(result, { json: options.json, jq: options.jq });
|
|
257
|
+
|
|
258
|
+
if (!result.successful) {
|
|
259
|
+
throw new Error('Add failed');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Extract UID from response and update metadata
|
|
263
|
+
const addResults = result.payload?.Results?.Add || result.data?.Payload?.Results?.Add || [];
|
|
264
|
+
if (addResults.length > 0) {
|
|
265
|
+
const returnedUID = addResults[0].UID;
|
|
266
|
+
if (returnedUID) {
|
|
267
|
+
meta.UID = returnedUID;
|
|
268
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
269
|
+
log.success(`UID assigned: ${returnedUID}`);
|
|
270
|
+
log.dim(` Run "dbo pull -e ${entity} ${returnedUID}" to populate all columns`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { entity: meta._entity };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Scan a directory for un-added files and add them.
|
|
279
|
+
*/
|
|
280
|
+
async function addDirectory(dirPath, client, options) {
|
|
281
|
+
const unadded = await findUnaddedFiles(dirPath);
|
|
282
|
+
|
|
283
|
+
if (unadded.length === 0) {
|
|
284
|
+
log.info('No un-added files found.');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
log.info(`Found ${unadded.length} file(s) to add:`);
|
|
289
|
+
for (const f of unadded) {
|
|
290
|
+
log.plain(` ${relative(process.cwd(), f)}`);
|
|
291
|
+
}
|
|
292
|
+
log.plain('');
|
|
293
|
+
|
|
294
|
+
if (!options.yes) {
|
|
295
|
+
const inquirer = (await import('inquirer')).default;
|
|
296
|
+
const { proceed } = await inquirer.prompt([{
|
|
297
|
+
type: 'confirm',
|
|
298
|
+
name: 'proceed',
|
|
299
|
+
message: `Add ${unadded.length} file(s)?`,
|
|
300
|
+
default: true,
|
|
301
|
+
}]);
|
|
302
|
+
if (!proceed) return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let succeeded = 0;
|
|
306
|
+
let failed = 0;
|
|
307
|
+
let batchDefaults = null;
|
|
308
|
+
|
|
309
|
+
for (const filePath of unadded) {
|
|
310
|
+
try {
|
|
311
|
+
const result = await addSingleFile(filePath, client, options, batchDefaults);
|
|
312
|
+
if (result) {
|
|
313
|
+
batchDefaults = result;
|
|
314
|
+
succeeded++;
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
log.error(`Failed: ${relative(process.cwd(), filePath)} — ${err.message}`);
|
|
318
|
+
failed++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
log.info(`Add complete: ${succeeded} succeeded, ${failed} failed`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Recursively find files that are not yet added to the server.
|
|
327
|
+
* A file is "un-added" if:
|
|
328
|
+
* - It has no companion .metadata.json, OR
|
|
329
|
+
* - Its .metadata.json exists but has no _CreatedOn (never been on server)
|
|
330
|
+
*/
|
|
331
|
+
async function findUnaddedFiles(dir) {
|
|
332
|
+
const results = [];
|
|
333
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
334
|
+
|
|
335
|
+
for (const entry of entries) {
|
|
336
|
+
const fullPath = join(dir, entry.name);
|
|
337
|
+
|
|
338
|
+
if (entry.isDirectory()) {
|
|
339
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
340
|
+
results.push(...await findUnaddedFiles(fullPath));
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Skip metadata files themselves
|
|
345
|
+
if (entry.name.endsWith('.metadata.json')) continue;
|
|
346
|
+
// Skip dotfiles
|
|
347
|
+
if (entry.name.startsWith('.')) continue;
|
|
348
|
+
|
|
349
|
+
// Check for companion metadata
|
|
350
|
+
const ext = extname(entry.name);
|
|
351
|
+
const base = basename(entry.name, ext);
|
|
352
|
+
const metaPath = join(dir, `${base}.metadata.json`);
|
|
353
|
+
|
|
354
|
+
let hasMetadata = false;
|
|
355
|
+
let isOnServer = false;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const raw = await readFile(metaPath, 'utf8');
|
|
359
|
+
if (raw.trim()) {
|
|
360
|
+
const meta = JSON.parse(raw);
|
|
361
|
+
hasMetadata = true;
|
|
362
|
+
isOnServer = !!meta._CreatedOn;
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
// No metadata file
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!isOnServer) {
|
|
369
|
+
results.push(fullPath);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return results;
|
|
374
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { DboClient } from '../lib/client.js';
|
|
3
|
+
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
4
|
+
|
|
5
|
+
export const cacheCommand = new Command('cache')
|
|
6
|
+
.description('Manage DBO.io cache');
|
|
7
|
+
|
|
8
|
+
cacheCommand
|
|
9
|
+
.command('list')
|
|
10
|
+
.description('List cached items')
|
|
11
|
+
.option('--metadata', 'Include cache metadata')
|
|
12
|
+
.option('--json', 'Output raw JSON')
|
|
13
|
+
.option('--jq <expr>', 'Filter JSON response with jq-like expression')
|
|
14
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
15
|
+
.option('--domain <host>', 'Override domain')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
19
|
+
const params = {};
|
|
20
|
+
if (options.metadata) params['_metadata'] = 'true';
|
|
21
|
+
const result = await client.get('/api/cache/list', params);
|
|
22
|
+
formatResponse(result, { json: options.json, jq: options.jq });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
formatError(err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cacheCommand
|
|
30
|
+
.command('refresh')
|
|
31
|
+
.description('Refresh a cache entry by key')
|
|
32
|
+
.requiredOption('--key <value>', 'Cache key to refresh')
|
|
33
|
+
.option('--part <value>', 'Cache partition')
|
|
34
|
+
.option('--json', 'Output raw JSON')
|
|
35
|
+
.option('--jq <expr>', 'Filter JSON response with jq-like expression')
|
|
36
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
37
|
+
.option('--domain <host>', 'Override domain')
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
try {
|
|
40
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
41
|
+
const params = { '_key': options.key };
|
|
42
|
+
if (options.part) params['_part'] = options.part;
|
|
43
|
+
const result = await client.get('/api/cache/refresh', params);
|
|
44
|
+
formatResponse(result, { json: options.json, jq: options.jq });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
formatError(err);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
});
|