@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.
@@ -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 = 'Bins';
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
- BINS_DIR,
12
- 'Automations',
13
- 'App Versions',
14
- 'Documentation',
15
- 'Sites',
16
- 'Extensions',
17
- 'Data Sources',
18
- 'Groups',
19
- 'Integrations',
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
- /** Map from server entity key local project directory name */
39
- export const ENTITY_DIR_MAP = {
40
- extension: 'Extensions',
41
- app_version: 'App Versions',
42
- data_source: 'Data Sources',
43
- site: 'Sites',
44
- group: 'Groups',
45
- integration: 'Integrations',
46
- automation: 'Automations',
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 "Bins/" prefix.
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 normalized = dirPath.replace(/^Bins\//, '').replace(/^\/+|\/+$/g, '');
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 = 'Extensions';
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 = 'Extensions/Unsupported';
193
+ export const EXTENSION_UNSUPPORTED_DIR = 'extension/_unsupported';
190
194
 
191
195
  /** Root-level documentation directory for alternate placement */
192
- export const DOCUMENTATION_DIR = 'Documentation';
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 "Extensions/<MappedName>" or "Extensions/Unsupported".
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)
@@ -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
- if (!data.ticket_id) {
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
- log.info(`Using stored ticket "${data.ticket_id}" (non-interactive mode)`);
162
- return { useTicket: true, clearTicket: false, cancel: false };
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
  }