@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { DboClient } from '../lib/client.js';
|
|
5
|
+
import { buildInputBody } from '../lib/input-parser.js';
|
|
6
|
+
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
7
|
+
import { saveToDisk } from '../lib/save-to-disk.js';
|
|
8
|
+
import { log } from '../lib/logger.js';
|
|
9
|
+
|
|
10
|
+
function collect(value, previous) {
|
|
11
|
+
return previous.concat([value]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const contentCommand = new Command('content')
|
|
15
|
+
.description('Get, deploy, or pull content from DBO.io')
|
|
16
|
+
.argument('[uid]', 'Content UID')
|
|
17
|
+
.option('-o, --output <path>', 'Save content to local file')
|
|
18
|
+
.option('--format <type>', 'Output format')
|
|
19
|
+
.option('--no-minify', 'Disable minification')
|
|
20
|
+
.option('--json', 'Output raw JSON')
|
|
21
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
22
|
+
.option('--domain <host>', 'Override domain');
|
|
23
|
+
|
|
24
|
+
// Subcommand: dbo content deploy <uid> <filepath>
|
|
25
|
+
const deployCmd = new Command('deploy')
|
|
26
|
+
.description('Deploy a local file to a DBO content record')
|
|
27
|
+
.argument('<uid>', 'Content UID (RowUID)')
|
|
28
|
+
.argument('<filepath>', 'Local file path')
|
|
29
|
+
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
30
|
+
.option('--ticket <id>', 'Override ticket ID')
|
|
31
|
+
.option('--json', 'Output raw JSON')
|
|
32
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
33
|
+
.option('--domain <host>', 'Override domain')
|
|
34
|
+
.action(async (uid, filepath, options) => {
|
|
35
|
+
try {
|
|
36
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
37
|
+
const extraParams = { '_confirm': options.confirm };
|
|
38
|
+
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
39
|
+
const body = await buildInputBody([`RowUID:${uid};column:content.Content@${filepath}`], extraParams);
|
|
40
|
+
const result = await client.postUrlEncoded('/api/input/submit', body);
|
|
41
|
+
formatResponse(result, { json: options.json });
|
|
42
|
+
if (!result.successful) process.exit(1);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
formatError(err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Subcommand: dbo content pull [uid] [--filter]
|
|
50
|
+
const pullCmd = new Command('pull')
|
|
51
|
+
.description('Pull content records to local files (uses content entity defaults)')
|
|
52
|
+
.argument('[uid]', 'Content UID (optional, pull single record)')
|
|
53
|
+
.option('--filter <expr>', 'Filter expression (repeatable)', collect, [])
|
|
54
|
+
.option('--maxrows <n>', 'Maximum rows')
|
|
55
|
+
.option('--json', 'Output raw JSON')
|
|
56
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
57
|
+
.option('--domain <host>', 'Override domain')
|
|
58
|
+
.action(async (uid, options) => {
|
|
59
|
+
try {
|
|
60
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
61
|
+
const params = { '_format': 'json_raw' };
|
|
62
|
+
if (options.maxrows) params['_maxrows'] = options.maxrows;
|
|
63
|
+
|
|
64
|
+
for (const f of options.filter) {
|
|
65
|
+
const atIdx = f.indexOf('=');
|
|
66
|
+
if (atIdx !== -1) {
|
|
67
|
+
const key = f.substring(0, atIdx);
|
|
68
|
+
const value = f.substring(atIdx + 1);
|
|
69
|
+
params[`_filter@${key}`] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let path;
|
|
74
|
+
if (uid) {
|
|
75
|
+
path = `/api/output/entity/content/${uid}`;
|
|
76
|
+
} else {
|
|
77
|
+
path = '/api/output/entity/content';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = await client.get(path, params);
|
|
81
|
+
|
|
82
|
+
log.success(`Pulled from ${chalk.underline(result.url || '')}`);
|
|
83
|
+
|
|
84
|
+
const data = result.payload || result.data;
|
|
85
|
+
|
|
86
|
+
if (data && data.raw && typeof data.raw === 'string') {
|
|
87
|
+
log.error('Received non-JSON response. Check the content UID.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rows = Array.isArray(data) ? data : (data?.Rows || data?.rows || [data]);
|
|
92
|
+
|
|
93
|
+
if (rows.length === 0) {
|
|
94
|
+
log.warn('No content records found.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
log.info(`${rows.length} record(s) to save`);
|
|
99
|
+
|
|
100
|
+
const columns = Object.keys(rows[0]);
|
|
101
|
+
await saveToDisk(rows, columns, {
|
|
102
|
+
entity: 'content',
|
|
103
|
+
saveFilename: columns.includes('Name') ? 'Name' : 'UID',
|
|
104
|
+
savePath: columns.includes('Path') ? 'Path' : null,
|
|
105
|
+
saveContent: columns.includes('Content') ? 'Content' : null,
|
|
106
|
+
saveExtension: columns.includes('Extension') ? 'Extension' : null,
|
|
107
|
+
nonInteractive: false,
|
|
108
|
+
});
|
|
109
|
+
} catch (err) {
|
|
110
|
+
formatError(err);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
contentCommand.addCommand(deployCmd);
|
|
116
|
+
contentCommand.addCommand(pullCmd);
|
|
117
|
+
|
|
118
|
+
// Default action: get content
|
|
119
|
+
contentCommand.action(async (uid, options) => {
|
|
120
|
+
try {
|
|
121
|
+
if (!uid) {
|
|
122
|
+
console.error('Error: provide a content UID or use a subcommand (deploy, pull)');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
126
|
+
const params = {};
|
|
127
|
+
if (options.format) params['_format'] = options.format;
|
|
128
|
+
if (options.noMinify) params['_no_minify'] = 'true';
|
|
129
|
+
|
|
130
|
+
const result = await client.get(`/api/content/${uid}`, params);
|
|
131
|
+
|
|
132
|
+
if (options.output) {
|
|
133
|
+
const text = typeof result.data === 'string' ? result.data : (result.data?.raw || JSON.stringify(result.data, null, 2));
|
|
134
|
+
await writeFile(options.output, text);
|
|
135
|
+
log.success(`Saved to ${options.output}`);
|
|
136
|
+
} else {
|
|
137
|
+
formatResponse(result, { json: options.json });
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
formatError(err);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { DboClient } from '../lib/client.js';
|
|
4
|
+
import { buildInputBody } from '../lib/input-parser.js';
|
|
5
|
+
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
6
|
+
import { log } from '../lib/logger.js';
|
|
7
|
+
|
|
8
|
+
const MANIFEST_FILE = 'dbo.deploy.json';
|
|
9
|
+
|
|
10
|
+
export const deployCommand = new Command('deploy')
|
|
11
|
+
.description('Deploy files to DBO.io using a manifest or direct arguments')
|
|
12
|
+
.argument('[name]', 'Deployment name from dbo.deploy.json (e.g., css:colors)')
|
|
13
|
+
.option('--all', 'Deploy all entries in the manifest')
|
|
14
|
+
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
15
|
+
.option('--ticket <id>', 'Override ticket ID')
|
|
16
|
+
.option('--json', 'Output raw JSON')
|
|
17
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
18
|
+
.option('--domain <host>', 'Override domain')
|
|
19
|
+
.action(async (name, options) => {
|
|
20
|
+
try {
|
|
21
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
22
|
+
|
|
23
|
+
// Load manifest
|
|
24
|
+
let manifest;
|
|
25
|
+
try {
|
|
26
|
+
const raw = await readFile(MANIFEST_FILE, 'utf8');
|
|
27
|
+
manifest = JSON.parse(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
if (!name) {
|
|
30
|
+
log.error(`No ${MANIFEST_FILE} found and no deployment name specified.`);
|
|
31
|
+
log.dim(' Create a dbo.deploy.json or use: dbo content deploy <uid> <filepath>');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!manifest?.deployments) {
|
|
37
|
+
log.error(`Invalid ${MANIFEST_FILE}: missing "deployments" key.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const entries = options.all
|
|
42
|
+
? Object.entries(manifest.deployments)
|
|
43
|
+
: name
|
|
44
|
+
? [[name, manifest.deployments[name]]]
|
|
45
|
+
: [];
|
|
46
|
+
|
|
47
|
+
if (entries.length === 0 || (entries.length === 1 && !entries[0][1])) {
|
|
48
|
+
const available = Object.keys(manifest.deployments).join(', ');
|
|
49
|
+
log.error(name ? `Deployment "${name}" not found in manifest.` : 'No deployment specified.');
|
|
50
|
+
log.dim(` Available: ${available}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [entryName, entry] of entries) {
|
|
55
|
+
if (!entry) {
|
|
56
|
+
log.warn(`Skipping unknown deployment: ${entryName}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entity = entry.entity || 'content';
|
|
61
|
+
const column = entry.column || 'Content';
|
|
62
|
+
const uid = entry.uid;
|
|
63
|
+
const file = entry.file;
|
|
64
|
+
|
|
65
|
+
if (!uid || !file) {
|
|
66
|
+
log.warn(`Skipping "${entryName}": missing uid or file.`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
log.info(`Deploying ${entryName}: ${file} → ${entity}.${column} (${uid})`);
|
|
71
|
+
|
|
72
|
+
const extraParams = { '_confirm': options.confirm };
|
|
73
|
+
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
74
|
+
const body = await buildInputBody([`RowUID:${uid};column:${entity}.${column}@${file}`], extraParams);
|
|
75
|
+
const result = await client.postUrlEncoded('/api/input/submit', body);
|
|
76
|
+
|
|
77
|
+
if (result.successful) {
|
|
78
|
+
log.success(`${entryName} deployed`);
|
|
79
|
+
} else {
|
|
80
|
+
log.error(`${entryName} failed`);
|
|
81
|
+
for (const msg of result.messages) log.label('Message', msg);
|
|
82
|
+
if (!options.all) process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
formatError(err);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { access } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore } from '../lib/config.js';
|
|
5
|
+
import { installClaudeCommands } from './install.js';
|
|
6
|
+
import { log } from '../lib/logger.js';
|
|
7
|
+
|
|
8
|
+
export const initCommand = new Command('init')
|
|
9
|
+
.description('Initialize DBO CLI configuration for the current directory')
|
|
10
|
+
.option('--domain <host>', 'DBO instance domain (e.g., beta-dev_model.dbo.io)')
|
|
11
|
+
.option('--username <user>', 'DBO username')
|
|
12
|
+
.option('--force', 'Overwrite existing configuration')
|
|
13
|
+
.option('--app <shortName>', 'App short name (for clone)')
|
|
14
|
+
.option('--clone', 'Clone the app after initialization')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
if (await isInitialized() && !options.force) {
|
|
18
|
+
log.warn('Already initialized. Use --force to overwrite.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let domain = options.domain;
|
|
23
|
+
let username = options.username;
|
|
24
|
+
|
|
25
|
+
// Check for legacy config to offer migration
|
|
26
|
+
if (!domain && await hasLegacyConfig()) {
|
|
27
|
+
const legacy = await readLegacyConfig();
|
|
28
|
+
log.info('Legacy configuration detected (.domain, .username, .password)');
|
|
29
|
+
const inquirer = (await import('inquirer')).default;
|
|
30
|
+
const { migrate } = await inquirer.prompt([{
|
|
31
|
+
type: 'confirm',
|
|
32
|
+
name: 'migrate',
|
|
33
|
+
message: `Migrate from legacy config? Domain: ${legacy.domain || '(none)'}`,
|
|
34
|
+
default: true,
|
|
35
|
+
}]);
|
|
36
|
+
if (migrate) {
|
|
37
|
+
domain = legacy.domain;
|
|
38
|
+
username = legacy.username;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Interactive prompts for missing values
|
|
43
|
+
if (!domain || !username) {
|
|
44
|
+
const inquirer = (await import('inquirer')).default;
|
|
45
|
+
const answers = await inquirer.prompt([
|
|
46
|
+
{ type: 'input', name: 'domain', message: 'Domain (e.g., myapp.dbo.io):', default: domain || undefined, when: !domain, validate: v => v ? true : 'Domain is required' },
|
|
47
|
+
{ type: 'input', name: 'username', message: 'Username (email):', when: !username },
|
|
48
|
+
]);
|
|
49
|
+
domain = domain || answers.domain;
|
|
50
|
+
username = username || answers.username;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await initConfig(domain);
|
|
54
|
+
if (username) {
|
|
55
|
+
await saveCredentials(username);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Ensure sensitive files are gitignored
|
|
59
|
+
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt']);
|
|
60
|
+
|
|
61
|
+
log.success(`Initialized .dbo/ for ${domain}`);
|
|
62
|
+
log.dim(' Run "dbo login" to authenticate.');
|
|
63
|
+
|
|
64
|
+
// Clone if requested
|
|
65
|
+
if (options.clone || options.app) {
|
|
66
|
+
let appShortName = options.app;
|
|
67
|
+
if (!appShortName) {
|
|
68
|
+
const inq = (await import('inquirer')).default;
|
|
69
|
+
const { appName } = await inq.prompt([{
|
|
70
|
+
type: 'input', name: 'appName',
|
|
71
|
+
message: 'App short name to clone:',
|
|
72
|
+
validate: v => v.trim() ? true : 'App short name is required',
|
|
73
|
+
}]);
|
|
74
|
+
appShortName = appName;
|
|
75
|
+
}
|
|
76
|
+
const { performClone } = await import('./clone.js');
|
|
77
|
+
await performClone(null, { app: appShortName, domain });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Offer Claude Code integration
|
|
81
|
+
const claudeDir = join(process.cwd(), '.claude');
|
|
82
|
+
const inquirer = (await import('inquirer')).default;
|
|
83
|
+
let hasClaudeDir = false;
|
|
84
|
+
try { await access(claudeDir); hasClaudeDir = true; } catch {}
|
|
85
|
+
|
|
86
|
+
if (hasClaudeDir) {
|
|
87
|
+
const { installCmds } = await inquirer.prompt([{
|
|
88
|
+
type: 'confirm', name: 'installCmds',
|
|
89
|
+
message: 'Install DBO commands for Claude Code? (adds /dbo slash command)',
|
|
90
|
+
default: true,
|
|
91
|
+
}]);
|
|
92
|
+
if (installCmds) await installClaudeCommands();
|
|
93
|
+
} else {
|
|
94
|
+
const { setupClaude } = await inquirer.prompt([{
|
|
95
|
+
type: 'confirm', name: 'setupClaude',
|
|
96
|
+
message: 'Set up Claude Code integration? (creates .claude/commands/)',
|
|
97
|
+
default: false,
|
|
98
|
+
}]);
|
|
99
|
+
if (setupClaude) await installClaudeCommands();
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
log.error(err.message);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { DboClient } from '../lib/client.js';
|
|
3
|
+
import { buildInputBody, parseFileArg, checkSubmitErrors } from '../lib/input-parser.js';
|
|
4
|
+
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
5
|
+
import { loadAppConfig } from '../lib/config.js';
|
|
6
|
+
import { log } from '../lib/logger.js';
|
|
7
|
+
|
|
8
|
+
function collect(value, previous) {
|
|
9
|
+
return previous.concat([value]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const inputCommand = new Command('input')
|
|
13
|
+
.description('Submit CRUD operations to DBO.io (add, edit, delete records)')
|
|
14
|
+
.requiredOption('-d, --data <expr>', 'DBO input expression (repeatable)', collect, [])
|
|
15
|
+
.option('-f, --file <field=@path>', 'File attachment for multipart upload (repeatable)', collect, [])
|
|
16
|
+
.option('-C, --confirm <value>', 'Commit changes: true (default) or false for validation only', 'true')
|
|
17
|
+
.option('--ticket <id>', 'Override ticket ID (_OverrideTicketID)')
|
|
18
|
+
.option('--login', 'Auto-login user created by this submission')
|
|
19
|
+
.option('--transactional', 'Use transactional processing')
|
|
20
|
+
.option('--json', 'Output raw JSON response')
|
|
21
|
+
.option('--jq <expr>', 'Filter JSON response with jq-like expression (implies --json)')
|
|
22
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
23
|
+
.option('--domain <host>', 'Override domain')
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
try {
|
|
26
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
27
|
+
|
|
28
|
+
const extraParams = {};
|
|
29
|
+
extraParams['_confirm'] = options.confirm;
|
|
30
|
+
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
31
|
+
if (options.login) extraParams['_login'] = 'true';
|
|
32
|
+
if (options.transactional) extraParams['_transactional'] = 'true';
|
|
33
|
+
|
|
34
|
+
// Check if data expressions include AppID; if not and config has one, prompt
|
|
35
|
+
const allDataText = options.data.join(' ');
|
|
36
|
+
const hasAppId = /\.AppID[=@]/.test(allDataText) || /AppID=/.test(allDataText);
|
|
37
|
+
if (!hasAppId) {
|
|
38
|
+
const appConfig = await loadAppConfig();
|
|
39
|
+
if (appConfig.AppID) {
|
|
40
|
+
const inquirer = (await import('inquirer')).default;
|
|
41
|
+
const { appIdChoice } = await inquirer.prompt([{
|
|
42
|
+
type: 'list',
|
|
43
|
+
name: 'appIdChoice',
|
|
44
|
+
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?`,
|
|
45
|
+
choices: [
|
|
46
|
+
{ name: `Yes, use AppID ${appConfig.AppID}`, value: 'use_config' },
|
|
47
|
+
{ name: 'No', value: 'none' },
|
|
48
|
+
{ name: 'Enter custom AppID', value: 'custom' },
|
|
49
|
+
],
|
|
50
|
+
}]);
|
|
51
|
+
if (appIdChoice === 'use_config') {
|
|
52
|
+
extraParams['AppID'] = String(appConfig.AppID);
|
|
53
|
+
log.dim(` Using AppID ${appConfig.AppID} from config`);
|
|
54
|
+
} else if (appIdChoice === 'custom') {
|
|
55
|
+
const { customAppId } = await inquirer.prompt([{
|
|
56
|
+
type: 'input', name: 'customAppId',
|
|
57
|
+
message: 'Custom AppID:',
|
|
58
|
+
}]);
|
|
59
|
+
if (customAppId.trim()) extraParams['AppID'] = customAppId.trim();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.file.length > 0) {
|
|
65
|
+
// Multipart mode
|
|
66
|
+
const fields = { ...extraParams };
|
|
67
|
+
// Parse data expressions into flat key-value pairs for form fields
|
|
68
|
+
for (const expr of options.data) {
|
|
69
|
+
for (const op of expr.split('&')) {
|
|
70
|
+
const trimmed = op.trim();
|
|
71
|
+
const eqIdx = trimmed.indexOf('=');
|
|
72
|
+
if (eqIdx !== -1) {
|
|
73
|
+
fields[trimmed.substring(0, eqIdx)] = trimmed.substring(eqIdx + 1);
|
|
74
|
+
} else {
|
|
75
|
+
fields[trimmed] = '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const files = options.file.map(parseFileArg);
|
|
80
|
+
let result = await client.postMultipart('/api/input/submit', fields, files);
|
|
81
|
+
|
|
82
|
+
// Retry with prompted params if needed (ticket, user)
|
|
83
|
+
const retryParams = await checkSubmitErrors(result);
|
|
84
|
+
if (retryParams) {
|
|
85
|
+
Object.assign(fields, retryParams);
|
|
86
|
+
result = await client.postMultipart('/api/input/submit', fields, files);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
formatResponse(result, { json: options.json, jq: options.jq });
|
|
90
|
+
if (!result.successful) process.exit(1);
|
|
91
|
+
} else {
|
|
92
|
+
// URL-encoded mode
|
|
93
|
+
let body = await buildInputBody(options.data, extraParams);
|
|
94
|
+
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
95
|
+
|
|
96
|
+
// Retry with prompted params if needed (ticket, user)
|
|
97
|
+
const retryParams = await checkSubmitErrors(result);
|
|
98
|
+
if (retryParams) {
|
|
99
|
+
Object.assign(extraParams, retryParams);
|
|
100
|
+
body = await buildInputBody(options.data, extraParams);
|
|
101
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
formatResponse(result, { json: options.json, jq: options.jq });
|
|
105
|
+
if (!result.successful) process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
formatError(err);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readdir, readFile, writeFile, mkdir, access, copyFile } from 'fs/promises';
|
|
3
|
+
import { join, dirname, basename } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { log } from '../lib/logger.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PLUGINS_DIR = join(__dirname, '..', 'plugins', 'claudecommands');
|
|
10
|
+
|
|
11
|
+
async function fileExists(path) {
|
|
12
|
+
try { await access(path); return true; } catch { return false; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const installCommand = new Command('install')
|
|
16
|
+
.description('Install dbo-cli components (Claude Code commands, plugins)')
|
|
17
|
+
.argument('[target]', 'What to install: claudecode, claudecommands')
|
|
18
|
+
.option('--claudecommand <name>', 'Install a specific Claude command by name')
|
|
19
|
+
.action(async (target, options) => {
|
|
20
|
+
try {
|
|
21
|
+
if (options.claudecommand) {
|
|
22
|
+
await installSpecificCommand(options.claudecommand);
|
|
23
|
+
} else if (target === 'claudecode') {
|
|
24
|
+
await installClaudeCode();
|
|
25
|
+
} else if (target === 'claudecommands') {
|
|
26
|
+
await installClaudeCommands();
|
|
27
|
+
} else {
|
|
28
|
+
// Interactive
|
|
29
|
+
const inquirer = (await import('inquirer')).default;
|
|
30
|
+
const { choice } = await inquirer.prompt([{
|
|
31
|
+
type: 'list', name: 'choice',
|
|
32
|
+
message: 'What would you like to install?',
|
|
33
|
+
choices: [
|
|
34
|
+
'Claude Code commands (adds /dbo to Claude Code)',
|
|
35
|
+
'Claude Code CLI + commands',
|
|
36
|
+
],
|
|
37
|
+
}]);
|
|
38
|
+
if (choice.includes('CLI')) {
|
|
39
|
+
await installClaudeCode();
|
|
40
|
+
} else {
|
|
41
|
+
await installClaudeCommands();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error(err.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function installClaudeCode() {
|
|
51
|
+
// Check if claude is installed
|
|
52
|
+
try {
|
|
53
|
+
const version = execSync('claude --version 2>&1', { encoding: 'utf8' }).trim();
|
|
54
|
+
log.success(`Claude Code already installed: ${version}`);
|
|
55
|
+
} catch {
|
|
56
|
+
log.info('Claude Code not found. Installing...');
|
|
57
|
+
try {
|
|
58
|
+
execSync('npm install -g @anthropic-ai/claude-code', { stdio: 'inherit' });
|
|
59
|
+
log.success('Claude Code installed');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log.error('Failed to install Claude Code. Try manually: npm install -g @anthropic-ai/claude-code');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Then install commands
|
|
67
|
+
await installClaudeCommands();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function installClaudeCommands() {
|
|
71
|
+
const cwd = process.cwd();
|
|
72
|
+
const claudeDir = join(cwd, '.claude');
|
|
73
|
+
const commandsDir = join(claudeDir, 'commands');
|
|
74
|
+
|
|
75
|
+
// Check/create .claude/commands/
|
|
76
|
+
if (!await fileExists(claudeDir)) {
|
|
77
|
+
const inquirer = (await import('inquirer')).default;
|
|
78
|
+
const { create } = await inquirer.prompt([{
|
|
79
|
+
type: 'confirm', name: 'create',
|
|
80
|
+
message: 'No .claude/ directory found. Create it for Claude Code integration?',
|
|
81
|
+
default: true,
|
|
82
|
+
}]);
|
|
83
|
+
if (!create) {
|
|
84
|
+
log.dim('Skipped Claude Code setup.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await mkdir(commandsDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
// Find all plugin source files
|
|
91
|
+
let pluginFiles;
|
|
92
|
+
try {
|
|
93
|
+
pluginFiles = (await readdir(PLUGINS_DIR)).filter(f => f.endsWith('.md'));
|
|
94
|
+
} catch {
|
|
95
|
+
log.error(`Plugin source directory not found: ${PLUGINS_DIR}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (pluginFiles.length === 0) {
|
|
100
|
+
log.warn('No Claude command plugins found in source.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let installed = 0;
|
|
105
|
+
for (const file of pluginFiles) {
|
|
106
|
+
const srcPath = join(PLUGINS_DIR, file);
|
|
107
|
+
const destPath = join(commandsDir, file);
|
|
108
|
+
|
|
109
|
+
// Check for existing file
|
|
110
|
+
if (await fileExists(destPath)) {
|
|
111
|
+
const inquirer = (await import('inquirer')).default;
|
|
112
|
+
const { overwrite } = await inquirer.prompt([{
|
|
113
|
+
type: 'confirm', name: 'overwrite',
|
|
114
|
+
message: `Overwrite existing .claude/commands/${file}?`,
|
|
115
|
+
default: false,
|
|
116
|
+
}]);
|
|
117
|
+
if (!overwrite) {
|
|
118
|
+
log.dim(` Skipped ${file}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await copyFile(srcPath, destPath);
|
|
124
|
+
log.success(`Installed .claude/commands/${file}`);
|
|
125
|
+
installed++;
|
|
126
|
+
|
|
127
|
+
// Add to .gitignore
|
|
128
|
+
await addToGitignore(cwd, `.claude/commands/${file}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (installed > 0) {
|
|
132
|
+
log.info(`${installed} Claude command(s) installed. Use /dbo in Claude Code.`);
|
|
133
|
+
log.warn('Note: Commands will be available in new Claude Code sessions (restart any active session).');
|
|
134
|
+
} else {
|
|
135
|
+
log.dim('No commands installed.');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function installSpecificCommand(name) {
|
|
140
|
+
const fileName = name.endsWith('.md') ? name : `${name}.md`;
|
|
141
|
+
const srcPath = join(PLUGINS_DIR, fileName);
|
|
142
|
+
|
|
143
|
+
if (!await fileExists(srcPath)) {
|
|
144
|
+
log.error(`Command plugin "${name}" not found in ${PLUGINS_DIR}`);
|
|
145
|
+
const available = (await readdir(PLUGINS_DIR)).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''));
|
|
146
|
+
if (available.length > 0) log.dim(` Available: ${available.join(', ')}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const commandsDir = join(process.cwd(), '.claude', 'commands');
|
|
151
|
+
await mkdir(commandsDir, { recursive: true });
|
|
152
|
+
|
|
153
|
+
const destPath = join(commandsDir, fileName);
|
|
154
|
+
|
|
155
|
+
if (await fileExists(destPath)) {
|
|
156
|
+
const inquirer = (await import('inquirer')).default;
|
|
157
|
+
const { overwrite } = await inquirer.prompt([{
|
|
158
|
+
type: 'confirm', name: 'overwrite',
|
|
159
|
+
message: `Overwrite existing .claude/commands/${fileName}?`,
|
|
160
|
+
default: false,
|
|
161
|
+
}]);
|
|
162
|
+
if (!overwrite) return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await copyFile(srcPath, destPath);
|
|
166
|
+
log.success(`Installed .claude/commands/${fileName}`);
|
|
167
|
+
log.warn('Note: Commands will be available in new Claude Code sessions (restart any active session).');
|
|
168
|
+
await addToGitignore(process.cwd(), `.claude/commands/${fileName}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function addToGitignore(projectDir, pattern) {
|
|
172
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
173
|
+
let content = '';
|
|
174
|
+
try {
|
|
175
|
+
content = await readFile(gitignorePath, 'utf8');
|
|
176
|
+
} catch { /* no .gitignore yet */ }
|
|
177
|
+
|
|
178
|
+
if (content.includes(pattern)) return; // already there
|
|
179
|
+
|
|
180
|
+
const addition = content.endsWith('\n') || content === ''
|
|
181
|
+
? `\n# DBO CLI Claude Code commands (managed by dbo-cli)\n${pattern}\n`
|
|
182
|
+
: `\n\n# DBO CLI Claude Code commands (managed by dbo-cli)\n${pattern}\n`;
|
|
183
|
+
|
|
184
|
+
await writeFile(gitignorePath, content + addition);
|
|
185
|
+
log.dim(` Added ${pattern} to .gitignore`);
|
|
186
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { DboClient } from '../lib/client.js';
|
|
3
|
+
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
4
|
+
|
|
5
|
+
export const instanceCommand = new Command('instance')
|
|
6
|
+
.description('Manage DBO.io instances');
|
|
7
|
+
|
|
8
|
+
instanceCommand
|
|
9
|
+
.command('export')
|
|
10
|
+
.description('Export the current instance')
|
|
11
|
+
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
12
|
+
.option('--json', 'Output raw JSON')
|
|
13
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
14
|
+
.option('--domain <host>', 'Override domain')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
18
|
+
const result = await client.postUrlEncoded('/api/instance/export', `_confirm=${options.confirm}`);
|
|
19
|
+
formatResponse(result, { json: options.json });
|
|
20
|
+
if (!result.successful) process.exit(1);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
formatError(err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
instanceCommand
|
|
28
|
+
.command('build <uid>')
|
|
29
|
+
.description('Build an instance from a pending instance UID')
|
|
30
|
+
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
31
|
+
.option('--json', 'Output raw JSON')
|
|
32
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
33
|
+
.option('--domain <host>', 'Override domain')
|
|
34
|
+
.action(async (uid, options) => {
|
|
35
|
+
try {
|
|
36
|
+
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
37
|
+
const result = await client.postUrlEncoded(`/api/instance/build/${uid}`, `_confirm=${options.confirm}`);
|
|
38
|
+
formatResponse(result, { json: options.json });
|
|
39
|
+
if (!result.successful) process.exit(1);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
formatError(err);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|