@dboio/cli 0.8.0 → 0.9.2
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 +157 -57
- package/package.json +1 -1
- package/src/commands/add.js +122 -10
- package/src/commands/clone.js +351 -99
- package/src/commands/deploy.js +1 -1
- package/src/commands/init.js +13 -4
- package/src/commands/input.js +2 -2
- package/src/commands/login.js +69 -0
- package/src/commands/pull.js +1 -0
- package/src/commands/push.js +202 -34
- package/src/commands/rm.js +48 -16
- package/src/lib/delta.js +14 -1
- package/src/lib/diff.js +4 -2
- package/src/lib/filenames.js +151 -0
- package/src/lib/formatter.js +93 -11
- package/src/lib/ignore.js +8 -2
- package/src/lib/input-parser.js +30 -4
- package/src/lib/metadata-templates.js +264 -0
- package/src/lib/structure.js +30 -26
- package/src/lib/ticketing.js +79 -8
package/src/lib/structure.js
CHANGED
|
@@ -4,19 +4,22 @@ import { join } from 'path';
|
|
|
4
4
|
const STRUCTURE_FILE = '.dbo/structure.json';
|
|
5
5
|
|
|
6
6
|
/** All bin-placed files go under this directory at project root */
|
|
7
|
-
export const BINS_DIR = '
|
|
7
|
+
export const BINS_DIR = 'bins';
|
|
8
8
|
|
|
9
9
|
/** Default top-level directories created at project root during clone */
|
|
10
10
|
export const DEFAULT_PROJECT_DIRS = [
|
|
11
|
-
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
11
|
+
'bins',
|
|
12
|
+
'automation',
|
|
13
|
+
'app_version',
|
|
14
|
+
'docs',
|
|
15
|
+
'site',
|
|
16
|
+
'extension',
|
|
17
|
+
'data_source',
|
|
18
|
+
'group',
|
|
19
|
+
'integration',
|
|
20
|
+
'src',
|
|
21
|
+
'tests',
|
|
22
|
+
'trash',
|
|
20
23
|
];
|
|
21
24
|
|
|
22
25
|
/** Map from physical output table names → documentation/display names */
|
|
@@ -35,16 +38,16 @@ export const OUTPUT_HIERARCHY_ENTITIES = [
|
|
|
35
38
|
'output_value_entity_column_rel',
|
|
36
39
|
];
|
|
37
40
|
|
|
38
|
-
/**
|
|
39
|
-
export const
|
|
40
|
-
extension
|
|
41
|
-
app_version
|
|
42
|
-
data_source
|
|
43
|
-
site
|
|
44
|
-
group
|
|
45
|
-
integration
|
|
46
|
-
automation
|
|
47
|
-
|
|
41
|
+
/** Entity keys that correspond to project directories (key IS the dir name) */
|
|
42
|
+
export const ENTITY_DIR_NAMES = new Set([
|
|
43
|
+
'extension',
|
|
44
|
+
'app_version',
|
|
45
|
+
'data_source',
|
|
46
|
+
'site',
|
|
47
|
+
'group',
|
|
48
|
+
'integration',
|
|
49
|
+
'automation',
|
|
50
|
+
]);
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* Build a bin hierarchy from an array of bin objects.
|
|
@@ -160,11 +163,12 @@ export function getBinName(binId, structure) {
|
|
|
160
163
|
|
|
161
164
|
/**
|
|
162
165
|
* Reverse-lookup: find a bin entry by its directory path.
|
|
163
|
-
* Accepts paths with or without the "
|
|
166
|
+
* Accepts paths with or without the "bins/" prefix.
|
|
164
167
|
* Returns { binId, name, path, segment, parentBinID, uid, fullPath } or null.
|
|
165
168
|
*/
|
|
166
169
|
export function findBinByPath(dirPath, structure) {
|
|
167
|
-
const
|
|
170
|
+
const binsPrefix = BINS_DIR + '/';
|
|
171
|
+
const normalized = dirPath.replace(new RegExp('^' + binsPrefix), '').replace(/^\/+|\/+$/g, '');
|
|
168
172
|
for (const [binId, entry] of Object.entries(structure)) {
|
|
169
173
|
if (entry.fullPath === normalized) {
|
|
170
174
|
return { binId: Number(binId), ...entry };
|
|
@@ -183,13 +187,13 @@ export function findChildBins(binId, structure) {
|
|
|
183
187
|
// ─── Extension Descriptor Sub-directory Support ───────────────────────────
|
|
184
188
|
|
|
185
189
|
/** Root for all extension descriptor-grouped sub-directories */
|
|
186
|
-
export const EXTENSION_DESCRIPTORS_DIR = '
|
|
190
|
+
export const EXTENSION_DESCRIPTORS_DIR = 'extension';
|
|
187
191
|
|
|
188
192
|
/** Extensions that cannot be mapped go here (always created, even if empty) */
|
|
189
|
-
export const EXTENSION_UNSUPPORTED_DIR = '
|
|
193
|
+
export const EXTENSION_UNSUPPORTED_DIR = 'extension/_unsupported';
|
|
190
194
|
|
|
191
195
|
/** Root-level documentation directory for alternate placement */
|
|
192
|
-
export const DOCUMENTATION_DIR = '
|
|
196
|
+
export const DOCUMENTATION_DIR = 'docs';
|
|
193
197
|
|
|
194
198
|
/**
|
|
195
199
|
* Build a descriptor→dirName mapping from a flat array of extension records.
|
|
@@ -280,7 +284,7 @@ export async function loadDescriptorMapping() {
|
|
|
280
284
|
|
|
281
285
|
/**
|
|
282
286
|
* Resolve the sub-directory path for a single extension record.
|
|
283
|
-
* Returns "
|
|
287
|
+
* Returns "extension/<MappedName>" or "extension/_unsupported".
|
|
284
288
|
*
|
|
285
289
|
* @param {Object} record - Extension record with a .Descriptor field
|
|
286
290
|
* @param {Object} mapping - descriptor key → dir name (from buildDescriptorMapping)
|
package/src/lib/ticketing.js
CHANGED
|
@@ -13,7 +13,7 @@ function ticketingPath() {
|
|
|
13
13
|
return join(dboDir(), TICKETING_FILE);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const DEFAULT_TICKETING = { ticket_id: null, records: [] };
|
|
16
|
+
const DEFAULT_TICKETING = { ticket_id: null, ticketing_required: false, records: [] };
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Load ticketing.local.json. Returns default structure if missing or corrupted.
|
|
@@ -30,6 +30,7 @@ export async function loadTicketing() {
|
|
|
30
30
|
const data = JSON.parse(trimmed);
|
|
31
31
|
return {
|
|
32
32
|
ticket_id: data.ticket_id || null,
|
|
33
|
+
ticketing_required: !!data.ticketing_required,
|
|
33
34
|
records: Array.isArray(data.records) ? data.records : [],
|
|
34
35
|
};
|
|
35
36
|
} catch (err) {
|
|
@@ -97,6 +98,25 @@ export async function setRecordTicket(uid, rowId, entity, ticketId) {
|
|
|
97
98
|
await saveTicketing(data);
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Mark ticketing as required for this app (detected after first ticket_error).
|
|
103
|
+
*/
|
|
104
|
+
export async function setTicketingRequired() {
|
|
105
|
+
const data = await loadTicketing();
|
|
106
|
+
if (!data.ticketing_required) {
|
|
107
|
+
data.ticketing_required = true;
|
|
108
|
+
await saveTicketing(data);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if ticketing is required for this app.
|
|
114
|
+
*/
|
|
115
|
+
export async function isTicketingRequired() {
|
|
116
|
+
const data = await loadTicketing();
|
|
117
|
+
return !!data.ticketing_required;
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
/**
|
|
101
121
|
* Clear the global ticket_id (preserves records).
|
|
102
122
|
*/
|
|
@@ -145,28 +165,77 @@ export function buildTicketExpression(entity, rowId, ticketId) {
|
|
|
145
165
|
*
|
|
146
166
|
* @param {Object} options - Command options (checks options.ticket for flag override)
|
|
147
167
|
*/
|
|
148
|
-
export async function checkStoredTicket(options) {
|
|
168
|
+
export async function checkStoredTicket(options, context = '') {
|
|
149
169
|
// --ticket flag takes precedence; skip stored-ticket prompt
|
|
150
170
|
if (options.ticket) {
|
|
151
171
|
return { useTicket: false, clearTicket: false, cancel: false };
|
|
152
172
|
}
|
|
153
173
|
|
|
154
174
|
const data = await loadTicketing();
|
|
155
|
-
|
|
175
|
+
|
|
176
|
+
// No stored ticket and ticketing not known to be required — skip prompt
|
|
177
|
+
if (!data.ticket_id && !data.ticketing_required) {
|
|
156
178
|
return { useTicket: false, clearTicket: false, cancel: false };
|
|
157
179
|
}
|
|
158
180
|
|
|
159
|
-
// Non-interactive mode: auto-use stored ticket
|
|
181
|
+
// Non-interactive mode: auto-use stored ticket if available
|
|
160
182
|
if (!process.stdin.isTTY) {
|
|
161
|
-
|
|
162
|
-
|
|
183
|
+
if (data.ticket_id) {
|
|
184
|
+
log.info(`Using stored ticket "${data.ticket_id}" (non-interactive mode)`);
|
|
185
|
+
return { useTicket: true, clearTicket: false, cancel: false };
|
|
186
|
+
}
|
|
187
|
+
// ticketing_required but no stored ticket — can't prompt in non-interactive
|
|
188
|
+
log.warn('This app requires a Ticket ID but none is stored.');
|
|
189
|
+
log.dim(' Use --ticket <id> or run interactively first to set a ticket.');
|
|
190
|
+
return { useTicket: false, clearTicket: false, cancel: false };
|
|
163
191
|
}
|
|
164
192
|
|
|
193
|
+
const suffix = context ? ` (${context})` : '';
|
|
165
194
|
const inquirer = (await import('inquirer')).default;
|
|
195
|
+
|
|
196
|
+
// Ticketing required but no stored ticket — prompt for one
|
|
197
|
+
if (!data.ticket_id && data.ticketing_required) {
|
|
198
|
+
const { action } = await inquirer.prompt([{
|
|
199
|
+
type: 'list',
|
|
200
|
+
name: 'action',
|
|
201
|
+
message: `This app requires a Ticket ID. Enter one for this submission?${suffix}`,
|
|
202
|
+
choices: [
|
|
203
|
+
{ name: 'Enter a Ticket ID for this and future submissions', value: 'enter_save' },
|
|
204
|
+
{ name: 'Enter a Ticket ID for this submission only', value: 'enter_once' },
|
|
205
|
+
{ name: 'Continue without a ticket', value: 'skip' },
|
|
206
|
+
{ name: 'Cancel submission', value: 'cancel' },
|
|
207
|
+
],
|
|
208
|
+
}]);
|
|
209
|
+
|
|
210
|
+
if (action === 'cancel') {
|
|
211
|
+
return { useTicket: false, clearTicket: false, cancel: true };
|
|
212
|
+
}
|
|
213
|
+
if (action === 'skip') {
|
|
214
|
+
return { useTicket: false, clearTicket: false, cancel: false };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const { ticketInput } = await inquirer.prompt([{
|
|
218
|
+
type: 'input',
|
|
219
|
+
name: 'ticketInput',
|
|
220
|
+
message: 'Ticket ID:',
|
|
221
|
+
}]);
|
|
222
|
+
const ticket = ticketInput.trim();
|
|
223
|
+
if (!ticket) {
|
|
224
|
+
log.error(' No Ticket ID entered. Submission cancelled.');
|
|
225
|
+
return { useTicket: false, clearTicket: false, cancel: true };
|
|
226
|
+
}
|
|
227
|
+
if (action === 'enter_save') {
|
|
228
|
+
await saveTicketing({ ...data, ticket_id: ticket });
|
|
229
|
+
log.dim(` Stored ticket set to "${ticket}"`);
|
|
230
|
+
}
|
|
231
|
+
return { useTicket: true, clearTicket: false, cancel: false, overrideTicket: ticket };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Stored ticket exists — prompt to use, change, or clear
|
|
166
235
|
const { action } = await inquirer.prompt([{
|
|
167
236
|
type: 'list',
|
|
168
237
|
name: 'action',
|
|
169
|
-
message: `Use stored Ticket ID "${data.ticket_id}" for this submission
|
|
238
|
+
message: `Use stored Ticket ID "${data.ticket_id}" for this submission?${suffix}`,
|
|
170
239
|
choices: [
|
|
171
240
|
{ name: `Yes, use "${data.ticket_id}"`, value: 'use' },
|
|
172
241
|
{ name: 'Use a different ticket for this submission only', value: 'alt_once' },
|
|
@@ -213,7 +282,7 @@ export async function checkStoredTicket(options) {
|
|
|
213
282
|
* @param {string|null} [sessionOverride] - One-time ticket override from pre-flight prompt
|
|
214
283
|
*/
|
|
215
284
|
export async function applyStoredTicketToSubmission(dataExprs, entity, rowId, uid, options, sessionOverride = null) {
|
|
216
|
-
if (options.ticket) return; // --ticket flag takes precedence
|
|
285
|
+
if (options.ticket) return null; // --ticket flag takes precedence
|
|
217
286
|
|
|
218
287
|
const recordTicket = await getRecordTicket(uid);
|
|
219
288
|
const globalTicket = await getGlobalTicket();
|
|
@@ -223,5 +292,7 @@ export async function applyStoredTicketToSubmission(dataExprs, entity, rowId, ui
|
|
|
223
292
|
const ticketExpr = buildTicketExpression(entity, rowId, ticketToUse);
|
|
224
293
|
dataExprs.push(ticketExpr);
|
|
225
294
|
log.dim(` Applying ticket: ${ticketToUse}`);
|
|
295
|
+
return ticketToUse;
|
|
226
296
|
}
|
|
297
|
+
return null;
|
|
227
298
|
}
|