@dboio/cli 0.8.2 → 0.9.3
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 +134 -56
- package/package.json +1 -1
- package/src/commands/add.js +114 -10
- package/src/commands/clone.js +245 -85
- package/src/commands/deploy.js +1 -1
- package/src/commands/init.js +4 -3
- package/src/commands/input.js +2 -2
- package/src/commands/pull.js +1 -0
- package/src/commands/push.js +127 -27
- package/src/commands/rm.js +48 -16
- 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 +73 -5
package/src/lib/ignore.js
CHANGED
|
@@ -19,7 +19,9 @@ app.json
|
|
|
19
19
|
.app.json
|
|
20
20
|
dbo.deploy.json
|
|
21
21
|
|
|
22
|
-
# Editor / IDE
|
|
22
|
+
# Editor / IDE / OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
23
25
|
.idea/
|
|
24
26
|
.vscode/
|
|
25
27
|
*.codekit3
|
|
@@ -39,6 +41,10 @@ node_modules/
|
|
|
39
41
|
package.json
|
|
40
42
|
package-lock.json
|
|
41
43
|
|
|
44
|
+
# Local development (not pushed to DBO server)
|
|
45
|
+
src/
|
|
46
|
+
tests/
|
|
47
|
+
|
|
42
48
|
# Documentation (repo scaffolding)
|
|
43
49
|
SETUP.md
|
|
44
50
|
README.md
|
|
@@ -58,7 +64,7 @@ export function getDefaultFileContent() {
|
|
|
58
64
|
/**
|
|
59
65
|
* Extract just the active pattern lines from DEFAULT_FILE_CONTENT.
|
|
60
66
|
*/
|
|
61
|
-
function getDefaultPatternLines() {
|
|
67
|
+
export function getDefaultPatternLines() {
|
|
62
68
|
return DEFAULT_FILE_CONTENT
|
|
63
69
|
.split('\n')
|
|
64
70
|
.filter(l => l && !l.startsWith('#'));
|
package/src/lib/input-parser.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { log } from './logger.js';
|
|
3
3
|
import { loadUserInfo, loadTicketSuggestionOutput } from './config.js';
|
|
4
|
-
import { buildTicketExpression, setGlobalTicket, setRecordTicket, getGlobalTicket, getRecordTicket } from './ticketing.js';
|
|
4
|
+
import { buildTicketExpression, setGlobalTicket, setRecordTicket, getGlobalTicket, getRecordTicket, setTicketingRequired } from './ticketing.js';
|
|
5
5
|
import { DboClient } from './client.js';
|
|
6
6
|
|
|
7
|
+
// Session-level cache: once the user resolves a UserID prompt, reuse it for all
|
|
8
|
+
// subsequent records in the same CLI invocation (one process = one push batch).
|
|
9
|
+
let _sessionUserOverride = null;
|
|
10
|
+
|
|
11
|
+
/** Get cached UserID override (set after first interactive prompt). */
|
|
12
|
+
export function getSessionUserOverride() {
|
|
13
|
+
return _sessionUserOverride;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Reset session caches (for tests). */
|
|
17
|
+
export function resetSessionCache() {
|
|
18
|
+
_sessionUserOverride = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
/**
|
|
8
22
|
* Parse DBO input syntax and build form data.
|
|
9
23
|
*
|
|
@@ -160,9 +174,16 @@ export async function checkSubmitErrors(result, context = {}) {
|
|
|
160
174
|
return Object.keys(retryParams).length > 0 ? retryParams : null;
|
|
161
175
|
}
|
|
162
176
|
|
|
177
|
+
// Session cache: reuse UserID from a previous prompt in this batch
|
|
178
|
+
const userResolved = needsUser && _sessionUserOverride;
|
|
179
|
+
if (userResolved) {
|
|
180
|
+
retryParams['_OverrideUserID'] = _sessionUserOverride;
|
|
181
|
+
log.dim(` Using cached user ID: ${_sessionUserOverride}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
163
184
|
const prompts = [];
|
|
164
185
|
|
|
165
|
-
if (needsUser) {
|
|
186
|
+
if (needsUser && !userResolved) {
|
|
166
187
|
log.warn(`This operation requires an authenticated user (${matchedUserPattern}).`);
|
|
167
188
|
log.dim(' Your session may have expired, or you may not be logged in.');
|
|
168
189
|
log.dim(' You can log in with "dbo login" to avoid this prompt in the future.');
|
|
@@ -215,7 +236,9 @@ export async function checkSubmitErrors(result, context = {}) {
|
|
|
215
236
|
const userValue = answers.userValue
|
|
216
237
|
|| (answers.userChoice === '_custom' ? answers.customUserValue : answers.userChoice);
|
|
217
238
|
if (userValue) {
|
|
218
|
-
|
|
239
|
+
const resolved = userValue.trim();
|
|
240
|
+
retryParams['_OverrideUserID'] = resolved;
|
|
241
|
+
_sessionUserOverride = resolved; // cache for remaining records in this batch
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
return retryParams;
|
|
@@ -226,6 +249,9 @@ export async function checkSubmitErrors(result, context = {}) {
|
|
|
226
249
|
* Prompts the user with 4 recovery options.
|
|
227
250
|
*/
|
|
228
251
|
async function handleTicketError(allText, context = {}) {
|
|
252
|
+
// Mark this app as requiring ticketing (detected on first ticket_error)
|
|
253
|
+
await setTicketingRequired();
|
|
254
|
+
|
|
229
255
|
// Try to extract record details from error text
|
|
230
256
|
const entityMatch = allText.match(/entity:(\w+)/);
|
|
231
257
|
const rowIdMatch = allText.match(/RowID:(\d+)/);
|
|
@@ -312,8 +338,8 @@ async function handleTicketError(allText, context = {}) {
|
|
|
312
338
|
name: 'ticketAction',
|
|
313
339
|
message: 'Record update requires a Ticket ID:',
|
|
314
340
|
choices: [
|
|
315
|
-
{ name: 'Apply a Ticket ID to this record and resubmit', value: 'apply_one' },
|
|
316
341
|
{ name: 'Apply a Ticket ID to all updates in this transaction, and update my current Ticket ID reference', value: 'apply_all' },
|
|
342
|
+
{ name: 'Apply a Ticket ID to this record only', value: 'apply_one' },
|
|
317
343
|
{ name: 'Skip this record update', value: 'skip_one' },
|
|
318
344
|
{ name: 'Skip all updates that require a Ticket ID', value: 'skip_all' },
|
|
319
345
|
],
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { relative, basename, extname } from 'path';
|
|
3
|
+
import { EXTENSION_DESCRIPTORS_DIR, ENTITY_DIR_NAMES } from './structure.js';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
|
|
6
|
+
const METADATA_TEMPLATES_FILE = '.dbo/metadata_templates.json';
|
|
7
|
+
|
|
8
|
+
const STATIC_DIRECTIVE_MAP = {
|
|
9
|
+
docs: { entity: 'extension', descriptor: 'documentation' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert a name to snake_case.
|
|
14
|
+
*/
|
|
15
|
+
export function toSnakeCase(name) {
|
|
16
|
+
return name
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[\s-]+/g, '_')
|
|
19
|
+
.replace(/[^a-z0-9_]/g, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve entity/descriptor directive from a file path.
|
|
24
|
+
* Returns { entity, descriptor } or null.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDirective(filePath) {
|
|
27
|
+
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
28
|
+
const parts = rel.split('/');
|
|
29
|
+
const topDir = parts[0];
|
|
30
|
+
|
|
31
|
+
// docs/ prefix → static mapping
|
|
32
|
+
if (topDir === 'docs') {
|
|
33
|
+
return { ...STATIC_DIRECTIVE_MAP.docs };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// extension/<descriptor>/ — need at least 3 parts (extension/descriptor/file)
|
|
37
|
+
if (topDir === 'extension') {
|
|
38
|
+
if (parts.length < 3) return null;
|
|
39
|
+
const secondDir = parts[1];
|
|
40
|
+
if (secondDir.startsWith('.')) return null;
|
|
41
|
+
return { entity: 'extension', descriptor: secondDir };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Other entity-dir types
|
|
45
|
+
if (ENTITY_DIR_NAMES.has(topDir)) {
|
|
46
|
+
return { entity: topDir, descriptor: null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load metadata templates from .dbo/metadata_templates.json.
|
|
54
|
+
*/
|
|
55
|
+
export async function loadMetadataTemplates() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await readFile(METADATA_TEMPLATES_FILE, 'utf8');
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
console.warn('Warning: .dbo/metadata_templates.json is malformed JSON — falling back to generic wizard');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === 'ENOENT') return {};
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Save metadata templates to .dbo/metadata_templates.json.
|
|
72
|
+
*/
|
|
73
|
+
export async function saveMetadataTemplates(templates) {
|
|
74
|
+
await writeFile(METADATA_TEMPLATES_FILE, JSON.stringify(templates, null, 2) + '\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get template cols for a given entity and optional descriptor.
|
|
79
|
+
* Returns string[] or null.
|
|
80
|
+
*/
|
|
81
|
+
export function getTemplateCols(templates, entity, descriptor) {
|
|
82
|
+
if (!templates || !templates[entity]) return null;
|
|
83
|
+
const entry = templates[entity];
|
|
84
|
+
|
|
85
|
+
if (descriptor) {
|
|
86
|
+
// entry must be an object (not array)
|
|
87
|
+
if (Array.isArray(entry)) return null;
|
|
88
|
+
return entry[descriptor] ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// no descriptor — entry must be an array
|
|
92
|
+
if (!Array.isArray(entry)) return null;
|
|
93
|
+
return entry;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set template cols for a given entity and optional descriptor.
|
|
98
|
+
* Mutates templates in place.
|
|
99
|
+
*/
|
|
100
|
+
export function setTemplateCols(templates, entity, descriptor, cols) {
|
|
101
|
+
if (descriptor) {
|
|
102
|
+
if (!templates[entity] || Array.isArray(templates[entity])) {
|
|
103
|
+
templates[entity] = {};
|
|
104
|
+
}
|
|
105
|
+
templates[entity][descriptor] = cols;
|
|
106
|
+
} else {
|
|
107
|
+
templates[entity] = cols;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build cols from a record's keys, excluding _ prefixed and array values.
|
|
113
|
+
*/
|
|
114
|
+
function buildColsFromRecord(record) {
|
|
115
|
+
return Object.keys(record).filter(key => {
|
|
116
|
+
if (key.startsWith('_')) return false;
|
|
117
|
+
if (Array.isArray(record[key])) return false;
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build baseline minimum viable cols for an entity type.
|
|
124
|
+
*/
|
|
125
|
+
function buildBaselineCols(entity, descriptor) {
|
|
126
|
+
const cols = ['AppID', 'Name'];
|
|
127
|
+
if (entity === 'extension') {
|
|
128
|
+
cols.push('ShortName');
|
|
129
|
+
if (descriptor) cols.push(`Descriptor=${descriptor}`);
|
|
130
|
+
cols.push('Active');
|
|
131
|
+
}
|
|
132
|
+
return cols;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve template cols via three-level lookup:
|
|
137
|
+
* 1. metadata_templates.json
|
|
138
|
+
* 2. appJson sample record
|
|
139
|
+
* 3. baseline defaults
|
|
140
|
+
*/
|
|
141
|
+
export async function resolveTemplateCols(entity, descriptor, appConfig, appJson) {
|
|
142
|
+
const templates = await loadMetadataTemplates();
|
|
143
|
+
if (templates === null) return null;
|
|
144
|
+
|
|
145
|
+
// Level 1: existing template
|
|
146
|
+
const existing = getTemplateCols(templates, entity, descriptor);
|
|
147
|
+
if (existing) return { cols: existing, templates, isNew: false };
|
|
148
|
+
|
|
149
|
+
// Level 2: derive from appJson
|
|
150
|
+
if (appJson) {
|
|
151
|
+
let records = null;
|
|
152
|
+
|
|
153
|
+
if (entity === 'extension' && descriptor) {
|
|
154
|
+
// appJson[entity] might be an object keyed by descriptor
|
|
155
|
+
const entityData = appJson[entity];
|
|
156
|
+
if (entityData) {
|
|
157
|
+
if (Array.isArray(entityData)) {
|
|
158
|
+
records = entityData;
|
|
159
|
+
} else if (typeof entityData === 'object' && entityData[descriptor]) {
|
|
160
|
+
const descData = entityData[descriptor];
|
|
161
|
+
if (Array.isArray(descData)) records = descData;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const entityData = appJson[entity];
|
|
166
|
+
if (Array.isArray(entityData)) records = entityData;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (records && records.length > 0) {
|
|
170
|
+
const cols = buildColsFromRecord(records[0]);
|
|
171
|
+
setTemplateCols(templates, entity, descriptor, cols);
|
|
172
|
+
await saveMetadataTemplates(templates);
|
|
173
|
+
return { cols, templates, isNew: true };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Level 3: baseline defaults
|
|
178
|
+
const cols = buildBaselineCols(entity, descriptor);
|
|
179
|
+
setTemplateCols(templates, entity, descriptor, cols);
|
|
180
|
+
await saveMetadataTemplates(templates);
|
|
181
|
+
return { cols, templates, isNew: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Assemble metadata object from cols and file info.
|
|
186
|
+
*/
|
|
187
|
+
export function assembleMetadata(cols, filePath, entity, descriptor, appConfig) {
|
|
188
|
+
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
189
|
+
const base = basename(filePath, extname(filePath));
|
|
190
|
+
const fileName = basename(filePath);
|
|
191
|
+
const isDocsFile = rel.startsWith('docs/');
|
|
192
|
+
const meta = { _entity: entity };
|
|
193
|
+
const contentColumns = [];
|
|
194
|
+
|
|
195
|
+
for (const col of cols) {
|
|
196
|
+
if (col.includes('=')) {
|
|
197
|
+
const eqIdx = col.indexOf('=');
|
|
198
|
+
const key = col.substring(0, eqIdx);
|
|
199
|
+
const val = col.substring(eqIdx + 1);
|
|
200
|
+
|
|
201
|
+
if (val === '@reference') {
|
|
202
|
+
const refPath = isDocsFile ? '@/' + rel : '@' + fileName;
|
|
203
|
+
meta[key] = refPath;
|
|
204
|
+
contentColumns.push(key);
|
|
205
|
+
} else {
|
|
206
|
+
meta[key] = val;
|
|
207
|
+
}
|
|
208
|
+
} else if (col === 'AppID') {
|
|
209
|
+
meta.AppID = appConfig?.AppID ?? '';
|
|
210
|
+
} else if (col === 'Name') {
|
|
211
|
+
meta.Name = base;
|
|
212
|
+
} else if (col === 'ShortName') {
|
|
213
|
+
meta.ShortName = toSnakeCase(base);
|
|
214
|
+
} else {
|
|
215
|
+
meta[col] = '';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let refColMissing = false;
|
|
220
|
+
if (contentColumns.length === 0) {
|
|
221
|
+
refColMissing = true;
|
|
222
|
+
} else {
|
|
223
|
+
meta._contentColumns = contentColumns;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { meta, contentColumns, refColMissing };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Prompt user to select a reference column from available string cols.
|
|
231
|
+
*/
|
|
232
|
+
export async function promptReferenceColumn(cols, entity, descriptor) {
|
|
233
|
+
const stringCols = cols.filter(c => !c.includes('=') && c !== 'AppID' && c !== 'Name' && c !== 'ShortName');
|
|
234
|
+
if (stringCols.length === 0) return null;
|
|
235
|
+
|
|
236
|
+
const { selected } = await inquirer.prompt([{
|
|
237
|
+
type: 'list',
|
|
238
|
+
name: 'selected',
|
|
239
|
+
message: `Select the content/reference column for ${entity}${descriptor ? '/' + descriptor : ''}:`,
|
|
240
|
+
choices: stringCols,
|
|
241
|
+
}]);
|
|
242
|
+
|
|
243
|
+
return selected;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Build a template cols array from a clone record.
|
|
248
|
+
*/
|
|
249
|
+
export function buildTemplateFromCloneRecord(record, contentColsExtracted = []) {
|
|
250
|
+
const cols = [];
|
|
251
|
+
for (const key of Object.keys(record)) {
|
|
252
|
+
if (key.startsWith('_')) continue;
|
|
253
|
+
if (Array.isArray(record[key])) continue;
|
|
254
|
+
|
|
255
|
+
if (contentColsExtracted.includes(key)) {
|
|
256
|
+
cols.push(key + '=@reference');
|
|
257
|
+
} else if (key === 'Descriptor' && record[key]) {
|
|
258
|
+
cols.push('Descriptor=' + record[key]);
|
|
259
|
+
} else {
|
|
260
|
+
cols.push(key);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return cols;
|
|
264
|
+
}
|
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
|
*/
|
|
@@ -152,18 +172,66 @@ export async function checkStoredTicket(options, context = '') {
|
|
|
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
|
|
|
165
193
|
const suffix = context ? ` (${context})` : '';
|
|
166
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
|
|
167
235
|
const { action } = await inquirer.prompt([{
|
|
168
236
|
type: 'list',
|
|
169
237
|
name: 'action',
|