@dboio/cli 0.9.6 → 0.10.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 +38 -1
- package/bin/dbo.js +2 -0
- package/package.json +1 -1
- package/src/commands/add.js +46 -0
- package/src/commands/clone.js +560 -246
- package/src/commands/init.js +30 -32
- package/src/commands/pull.js +264 -87
- package/src/commands/push.js +502 -57
- package/src/commands/rm.js +1 -1
- package/src/commands/sync.js +68 -0
- package/src/lib/config.js +49 -8
- package/src/lib/delta.js +115 -28
- package/src/lib/diff.js +9 -3
- package/src/lib/folder-icon.js +120 -0
- package/src/lib/ignore.js +1 -1
- package/src/lib/input-parser.js +37 -10
- package/src/lib/scaffold.js +82 -2
- package/src/lib/structure.js +2 -0
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { readdir, mkdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset, saveTicketSuggestionOutput } from '../lib/config.js';
|
|
4
|
+
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset, saveTicketSuggestionOutput, loadConfig } from '../lib/config.js';
|
|
5
5
|
import { installOrUpdateClaudeCommands } from './install.js';
|
|
6
6
|
import { scaffoldProjectDirs, logScaffoldResult } from '../lib/scaffold.js';
|
|
7
7
|
import { createDboignore, loadIgnore } from '../lib/ignore.js';
|
|
@@ -40,7 +40,13 @@ export const initCommand = new Command('init')
|
|
|
40
40
|
|
|
41
41
|
if (await isInitialized() && !options.force) {
|
|
42
42
|
if (options.scaffold) {
|
|
43
|
-
|
|
43
|
+
// Try to pass app short name for media sub-directory creation
|
|
44
|
+
let shortName;
|
|
45
|
+
try {
|
|
46
|
+
const cfg = await loadConfig();
|
|
47
|
+
shortName = cfg.AppShortName;
|
|
48
|
+
} catch { /* no config yet */ }
|
|
49
|
+
const result = await scaffoldProjectDirs(process.cwd(), { appShortName: shortName });
|
|
44
50
|
logScaffoldResult(result);
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
@@ -99,13 +105,24 @@ export const initCommand = new Command('init')
|
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// Ensure sensitive files are gitignored
|
|
102
|
-
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', 'trash/']);
|
|
108
|
+
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', 'trash/', 'Icon\\r']);
|
|
103
109
|
|
|
104
110
|
const createdIgnore = await createDboignore();
|
|
105
111
|
if (createdIgnore) log.dim(' Created .dboignore');
|
|
106
112
|
|
|
113
|
+
// Ensure .claude/ directory structure exists
|
|
114
|
+
const claudeSubDirs = ['1_prompts', '2_specs', '3_plans', 'commands'];
|
|
115
|
+
for (const sub of claudeSubDirs) {
|
|
116
|
+
await mkdir(join(process.cwd(), '.claude', sub), { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
log.dim(' Created .claude/ directory structure');
|
|
119
|
+
|
|
107
120
|
log.success(`Initialized .dbo/ for ${domain}`);
|
|
108
|
-
|
|
121
|
+
|
|
122
|
+
// Authenticate early so the session is ready for subsequent operations
|
|
123
|
+
if (!options.nonInteractive && username) {
|
|
124
|
+
await performLogin(domain, username);
|
|
125
|
+
}
|
|
109
126
|
|
|
110
127
|
// Prompt for TransactionKeyPreset
|
|
111
128
|
if (!options.nonInteractive) {
|
|
@@ -167,7 +184,7 @@ export const initCommand = new Command('init')
|
|
|
167
184
|
logScaffoldResult(result);
|
|
168
185
|
}
|
|
169
186
|
|
|
170
|
-
// Clone if requested
|
|
187
|
+
// Clone if requested
|
|
171
188
|
if (options.clone || options.app) {
|
|
172
189
|
let appShortName = options.app;
|
|
173
190
|
if (!appShortName) {
|
|
@@ -180,38 +197,19 @@ export const initCommand = new Command('init')
|
|
|
180
197
|
appShortName = appName;
|
|
181
198
|
}
|
|
182
199
|
|
|
183
|
-
// Authenticate before fetching app data from the server
|
|
184
|
-
if (!options.nonInteractive) {
|
|
185
|
-
log.info('Login required to fetch app data from the server.');
|
|
186
|
-
await performLogin(domain, username);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
200
|
const { performClone } = await import('./clone.js');
|
|
190
201
|
await performClone(null, { app: appShortName, domain, mediaPlacement: options.mediaPlacement });
|
|
191
202
|
}
|
|
192
203
|
|
|
193
|
-
// Offer Claude Code
|
|
204
|
+
// Offer Claude Code plugin installation (skip in non-interactive mode)
|
|
194
205
|
if (!options.nonInteractive) {
|
|
195
|
-
const claudeDir = join(process.cwd(), '.claude');
|
|
196
206
|
const inquirer = (await import('inquirer')).default;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
message: 'Install DBO commands for Claude Code? (adds /dbo slash command)',
|
|
204
|
-
default: true,
|
|
205
|
-
}]);
|
|
206
|
-
if (installCmds) await installOrUpdateClaudeCommands(options);
|
|
207
|
-
} else {
|
|
208
|
-
const { setupClaude } = await inquirer.prompt([{
|
|
209
|
-
type: 'confirm', name: 'setupClaude',
|
|
210
|
-
message: 'Set up Claude Code integration? (creates .claude/commands/)',
|
|
211
|
-
default: false,
|
|
212
|
-
}]);
|
|
213
|
-
if (setupClaude) await installOrUpdateClaudeCommands(options);
|
|
214
|
-
}
|
|
207
|
+
const { installCmds } = await inquirer.prompt([{
|
|
208
|
+
type: 'confirm', name: 'installCmds',
|
|
209
|
+
message: 'Install DBO commands for Claude Code? (adds /dbo slash command)',
|
|
210
|
+
default: true,
|
|
211
|
+
}]);
|
|
212
|
+
if (installCmds) await installOrUpdateClaudeCommands(options);
|
|
215
213
|
}
|
|
216
214
|
} catch (err) {
|
|
217
215
|
log.error(err.message);
|
package/src/commands/pull.js
CHANGED
|
@@ -1,114 +1,291 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { access } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
2
4
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
4
|
-
import { loadConfig } from '../lib/config.js';
|
|
5
|
-
import {
|
|
6
|
-
import { saveToDisk } from '../lib/save-to-disk.js';
|
|
5
|
+
import { performClone, resolveRecordPaths, resolveMediaPaths, resolveEntityDirPaths, resolveEntityFilter, buildOutputFilename } from './clone.js';
|
|
6
|
+
import { loadConfig, loadClonePlacement } from '../lib/config.js';
|
|
7
|
+
import { loadStructureFile, resolveBinPath, BINS_DIR, ENTITY_DIR_NAMES, EXTENSION_DESCRIPTORS_DIR, EXTENSION_UNSUPPORTED_DIR } from '../lib/structure.js';
|
|
7
8
|
import { log } from '../lib/logger.js';
|
|
8
|
-
import {
|
|
9
|
+
import { formatError } from '../lib/formatter.js';
|
|
10
|
+
import { getLocalSyncTime, isServerNewer } from '../lib/diff.js';
|
|
11
|
+
import { DboClient } from '../lib/client.js';
|
|
9
12
|
|
|
10
|
-
function
|
|
11
|
-
|
|
13
|
+
async function fileExists(path) {
|
|
14
|
+
try { await access(path); return true; } catch { return false; }
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Fetch the full app JSON from the server (same endpoint as clone).
|
|
19
|
+
* Distinguishes between authentication failures and genuine "app not found".
|
|
20
|
+
*/
|
|
21
|
+
async function fetchAppJson(config, options) {
|
|
22
|
+
if (!config.AppShortName) {
|
|
23
|
+
throw new Error('No AppShortName in config. Run "dbo clone" first to set up the project.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const appShortName = config.AppShortName;
|
|
27
|
+
const client = new DboClient({ domain: options.domain || config.domain, verbose: options.verbose });
|
|
28
|
+
const ora = (await import('ora')).default;
|
|
29
|
+
const spinner = ora(`Fetching app "${appShortName}" from server...`).start();
|
|
30
|
+
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
result = await client.get(`/api/app/object/${appShortName}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
spinner.fail(`Failed to fetch app "${appShortName}"`);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for authentication / session errors before parsing app data
|
|
40
|
+
const AUTH_PATTERNS = ['LoggedInUser_UID', 'LoggedInUserID', 'CurrentUserID', 'UserID', 'not authenticated', 'session expired', 'login required'];
|
|
41
|
+
const messages = result.messages || [];
|
|
42
|
+
const allMsgText = messages.filter(m => typeof m === 'string').join(' ');
|
|
43
|
+
const isAuthError = !result.ok && (result.status === 401 || result.status === 403)
|
|
44
|
+
|| (!result.successful && AUTH_PATTERNS.some(p => allMsgText.includes(p)));
|
|
45
|
+
|
|
46
|
+
if (isAuthError) {
|
|
47
|
+
spinner.fail('Session expired or not authenticated');
|
|
48
|
+
log.warn('Your session appears to have expired.');
|
|
49
|
+
if (allMsgText) log.dim(` Server: ${allMsgText.substring(0, 200)}`);
|
|
50
|
+
|
|
51
|
+
if (process.stdin.isTTY) {
|
|
52
|
+
const inquirer = (await import('inquirer')).default;
|
|
53
|
+
const { action } = await inquirer.prompt([{
|
|
54
|
+
type: 'list',
|
|
55
|
+
name: 'action',
|
|
56
|
+
message: 'How would you like to proceed?',
|
|
57
|
+
choices: [
|
|
58
|
+
{ name: 'Re-login now (recommended)', value: 'relogin' },
|
|
59
|
+
{ name: 'Abort', value: 'abort' },
|
|
60
|
+
],
|
|
61
|
+
}]);
|
|
62
|
+
|
|
63
|
+
if (action === 'relogin') {
|
|
64
|
+
const { performLogin } = await import('./login.js');
|
|
65
|
+
await performLogin(options.domain || config.domain);
|
|
66
|
+
log.info('Retrying app fetch...');
|
|
67
|
+
return fetchAppJson(config, options);
|
|
42
68
|
}
|
|
69
|
+
} else {
|
|
70
|
+
log.dim(' Run "dbo login" to authenticate, then retry.');
|
|
71
|
+
}
|
|
72
|
+
throw new Error('Authentication required. Run "dbo login" first.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for non-auth server errors
|
|
76
|
+
if (!result.ok && result.status >= 500) {
|
|
77
|
+
spinner.fail(`Server error (HTTP ${result.status})`);
|
|
78
|
+
if (allMsgText) log.dim(` Server: ${allMsgText.substring(0, 200)}`);
|
|
79
|
+
throw new Error(`Server error (HTTP ${result.status}) fetching app "${appShortName}"`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!result.successful && allMsgText) {
|
|
83
|
+
spinner.fail(`Server returned an error`);
|
|
84
|
+
log.warn(` ${allMsgText.substring(0, 300)}`);
|
|
85
|
+
throw new Error(`Server error fetching app "${appShortName}": ${allMsgText.substring(0, 200)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = result.payload || result.data;
|
|
89
|
+
let appRecord;
|
|
90
|
+
if (Array.isArray(data)) {
|
|
91
|
+
appRecord = data.length > 0 ? data[0] : null;
|
|
92
|
+
} else if (data?.Rows?.length > 0) {
|
|
93
|
+
appRecord = data.Rows[0];
|
|
94
|
+
} else if (data?.rows?.length > 0) {
|
|
95
|
+
appRecord = data.rows[0];
|
|
96
|
+
} else if (data && typeof data === 'object' && (data.UID || data.ShortName)) {
|
|
97
|
+
appRecord = data;
|
|
98
|
+
} else {
|
|
99
|
+
appRecord = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!appRecord || !appRecord.children) {
|
|
103
|
+
spinner.fail(`No app found or invalid response for "${appShortName}"`);
|
|
104
|
+
throw new Error(`No app found with ShortName "${appShortName}"`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
spinner.succeed('Fetched app from server');
|
|
108
|
+
return appRecord;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Dry-run scan: compare server records against local metadata files.
|
|
113
|
+
* Reports counts of new, changed, and up-to-date records without writing anything.
|
|
114
|
+
*/
|
|
115
|
+
async function dryRunScan(appJson, options) {
|
|
116
|
+
const config = await loadConfig();
|
|
117
|
+
const serverTz = config.ServerTimezone || 'America/Los_Angeles';
|
|
118
|
+
const configWithTz = { ...config, ServerTimezone: serverTz };
|
|
119
|
+
|
|
120
|
+
const structure = await loadStructureFile();
|
|
121
|
+
const saved = await loadClonePlacement();
|
|
122
|
+
const contentPlacement = saved.contentPlacement || 'bin';
|
|
123
|
+
const mediaPlacement = saved.mediaPlacement || 'bin';
|
|
124
|
+
|
|
125
|
+
const entityFilter = resolveEntityFilter(options.entity);
|
|
126
|
+
|
|
127
|
+
const counts = { new: 0, changed: 0, upToDate: 0 };
|
|
128
|
+
const details = { new: [], changed: [] };
|
|
129
|
+
|
|
130
|
+
// Scan content records
|
|
131
|
+
if (!entityFilter || entityFilter.has('content')) {
|
|
132
|
+
for (const record of (appJson.children.content || [])) {
|
|
133
|
+
const { metaPath } = resolveRecordPaths('content', record, structure, contentPlacement);
|
|
134
|
+
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, 'content');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
43
137
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
138
|
+
// Scan media records
|
|
139
|
+
if (!entityFilter || entityFilter.has('media')) {
|
|
140
|
+
for (const record of (appJson.children.media || [])) {
|
|
141
|
+
const { metaPath } = resolveMediaPaths(record, structure, mediaPlacement);
|
|
142
|
+
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, 'media');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Scan entity-dir records (extension, site, data_source, etc.)
|
|
147
|
+
for (const [entityName, entries] of Object.entries(appJson.children)) {
|
|
148
|
+
if (['bin', 'content', 'media', 'output', 'output_value', 'output_value_filter', 'output_value_entity_column_rel'].includes(entityName)) continue;
|
|
149
|
+
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
150
|
+
if (entityFilter && !entityFilter.has(entityName)) continue;
|
|
151
|
+
|
|
152
|
+
if (entityName === 'extension') {
|
|
153
|
+
// Extensions use descriptor sub-directories
|
|
154
|
+
for (const record of entries) {
|
|
155
|
+
const dirName = resolveExtensionDir(record);
|
|
156
|
+
const { metaPath } = resolveEntityDirPaths(entityName, record, dirName);
|
|
157
|
+
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, entityName);
|
|
158
|
+
}
|
|
159
|
+
} else if (ENTITY_DIR_NAMES.has(entityName)) {
|
|
160
|
+
for (const record of entries) {
|
|
161
|
+
const { metaPath } = resolveEntityDirPaths(entityName, record, entityName);
|
|
162
|
+
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, entityName);
|
|
47
163
|
}
|
|
164
|
+
}
|
|
165
|
+
// Generic entities in bins — skip for simplicity in dry-run (they use content placement)
|
|
166
|
+
}
|
|
48
167
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
168
|
+
// Scan output hierarchy (compound single-file format)
|
|
169
|
+
if (!entityFilter || entityFilter.has('output')) {
|
|
170
|
+
const outputEntries = appJson.children.output || [];
|
|
171
|
+
for (const record of outputEntries) {
|
|
172
|
+
// Resolve bin directory for this output (same logic as clone)
|
|
173
|
+
let binDir = BINS_DIR;
|
|
174
|
+
if (record.BinID && structure[record.BinID]) {
|
|
175
|
+
binDir = resolveBinPath(record.BinID, structure);
|
|
54
176
|
}
|
|
177
|
+
const rootBasename = buildOutputFilename('output', record, 'Name');
|
|
178
|
+
const metaPath = join(binDir, `${rootBasename}.json`);
|
|
179
|
+
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, 'output');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
55
182
|
|
|
56
|
-
|
|
183
|
+
// Print summary
|
|
184
|
+
log.plain('');
|
|
185
|
+
log.info('Dry-run scan results:');
|
|
186
|
+
log.plain('');
|
|
57
187
|
|
|
58
|
-
|
|
188
|
+
const total = counts.new + counts.changed + counts.upToDate;
|
|
59
189
|
|
|
60
|
-
|
|
190
|
+
if (counts.new > 0) {
|
|
191
|
+
log.success(` ${chalk.green(counts.new)} new record(s)`);
|
|
192
|
+
for (const d of details.new.slice(0, 10)) {
|
|
193
|
+
log.dim(` + [${d.entity}] ${d.name}`);
|
|
194
|
+
}
|
|
195
|
+
if (details.new.length > 10) log.dim(` ... and ${details.new.length - 10} more`);
|
|
196
|
+
}
|
|
61
197
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
198
|
+
if (counts.changed > 0) {
|
|
199
|
+
log.warn(` ${chalk.yellow(counts.changed)} changed record(s)`);
|
|
200
|
+
for (const d of details.changed.slice(0, 10)) {
|
|
201
|
+
log.dim(` ~ [${d.entity}] ${d.name}`);
|
|
202
|
+
}
|
|
203
|
+
if (details.changed.length > 10) log.dim(` ... and ${details.changed.length - 10} more`);
|
|
204
|
+
}
|
|
68
205
|
|
|
69
|
-
|
|
206
|
+
if (counts.upToDate > 0) {
|
|
207
|
+
log.dim(` ${counts.upToDate} up-to-date record(s)`);
|
|
208
|
+
}
|
|
70
209
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
210
|
+
log.plain('');
|
|
211
|
+
if (counts.new === 0 && counts.changed === 0) {
|
|
212
|
+
log.success(`All records up to date (${total} checked)`);
|
|
213
|
+
} else {
|
|
214
|
+
log.info(`${total} record(s) checked. Run "dbo pull" (without --dry-run) to apply changes.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
75
217
|
|
|
76
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Check a single record against its local metadata file.
|
|
220
|
+
*/
|
|
221
|
+
async function scanRecord(record, metaPath, _serverTz, configWithTz, counts, details, entityName) {
|
|
222
|
+
const name = record.Name || record.UID || 'unknown';
|
|
223
|
+
const exists = await fileExists(metaPath);
|
|
77
224
|
|
|
78
|
-
|
|
225
|
+
if (!exists) {
|
|
226
|
+
counts.new++;
|
|
227
|
+
details.new.push({ entity: entityName, name });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
79
230
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
231
|
+
const localSyncTime = await getLocalSyncTime(metaPath);
|
|
232
|
+
if (record._LastUpdated && isServerNewer(localSyncTime, record._LastUpdated, configWithTz)) {
|
|
233
|
+
counts.changed++;
|
|
234
|
+
details.changed.push({ entity: entityName, name });
|
|
235
|
+
} else {
|
|
236
|
+
counts.upToDate++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the directory for an extension record (descriptor-based sub-dirs).
|
|
242
|
+
*/
|
|
243
|
+
function resolveExtensionDir(record) {
|
|
244
|
+
const descriptor = record.Descriptor;
|
|
245
|
+
if (!descriptor) return EXTENSION_UNSUPPORTED_DIR;
|
|
246
|
+
if (descriptor === 'descriptor_definition') return EXTENSION_DESCRIPTORS_DIR;
|
|
247
|
+
// Use the descriptor value as sub-directory name
|
|
248
|
+
return `${EXTENSION_DESCRIPTORS_DIR}/${descriptor}`;
|
|
249
|
+
}
|
|
84
250
|
|
|
85
|
-
|
|
86
|
-
|
|
251
|
+
export const pullCommand = new Command('pull')
|
|
252
|
+
.description('Pull updates from server into existing project')
|
|
253
|
+
.option('-e, --entity <type>', 'Only pull a specific entity type')
|
|
254
|
+
.option('-y, --yes', 'Auto-accept all incoming changes')
|
|
255
|
+
.option('-n, --dry-run', 'Show what would change without writing files')
|
|
256
|
+
.option('-v, --verbose', 'Show HTTP request details')
|
|
257
|
+
.option('--domain <host>', 'Override domain')
|
|
258
|
+
.option('--force', 'Force re-processing, skip change detection')
|
|
259
|
+
.option('--descriptor-types <bool>', 'Sort extensions into descriptor sub-directories (default: true)', 'true')
|
|
260
|
+
.option('--documentation-only', 'When used with -e extension, pull only documentation extensions')
|
|
261
|
+
.action(async (options) => {
|
|
262
|
+
try {
|
|
87
263
|
const config = await loadConfig();
|
|
88
264
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
changeDetection: true,
|
|
101
|
-
config,
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
// Other entities: interactive prompts for column selection
|
|
105
|
-
await saveToDisk(rows, columns, {
|
|
106
|
-
entity,
|
|
107
|
-
nonInteractive: false,
|
|
108
|
-
changeDetection: true,
|
|
109
|
-
config,
|
|
110
|
-
});
|
|
265
|
+
if (!config.AppShortName) {
|
|
266
|
+
log.error('No AppShortName found in .dbo/config.json.');
|
|
267
|
+
log.dim(' Run "dbo clone" first to set up the project.');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Dry-run mode: scan and report without writing
|
|
272
|
+
if (options.dryRun) {
|
|
273
|
+
const appJson = await fetchAppJson(config, options);
|
|
274
|
+
await dryRunScan(appJson, options);
|
|
275
|
+
return;
|
|
111
276
|
}
|
|
277
|
+
|
|
278
|
+
// Full pull: delegate to performClone with pullMode
|
|
279
|
+
await performClone(null, {
|
|
280
|
+
pullMode: true,
|
|
281
|
+
yes: options.yes,
|
|
282
|
+
entity: options.entity,
|
|
283
|
+
force: options.force,
|
|
284
|
+
domain: options.domain,
|
|
285
|
+
verbose: options.verbose,
|
|
286
|
+
descriptorTypes: options.descriptorTypes,
|
|
287
|
+
documentationOnly: options.documentationOnly,
|
|
288
|
+
});
|
|
112
289
|
} catch (err) {
|
|
113
290
|
formatError(err);
|
|
114
291
|
process.exit(1);
|