@dboio/cli 0.6.10 → 0.6.11
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 +41 -1
- package/package.json +1 -1
- package/src/commands/content.js +45 -1
- package/src/commands/deploy.js +50 -2
- package/src/lib/input-parser.js +55 -6
- package/src/lib/modify-key.js +13 -0
- package/src/lib/ticketing.js +43 -5
package/README.md
CHANGED
|
@@ -1165,15 +1165,23 @@ The submission is then retried with `_OverrideTicketID`. To skip the prompt, pas
|
|
|
1165
1165
|
|
|
1166
1166
|
#### Pre-submission ticket prompt
|
|
1167
1167
|
|
|
1168
|
-
When a stored ticket exists in `.dbo/ticketing.local.json`, the CLI prompts before batch submissions:
|
|
1168
|
+
When a stored ticket exists in `.dbo/ticketing.local.json`, the CLI prompts before batch submissions (`push`, `input`, `add`, `content deploy`, `deploy`):
|
|
1169
1169
|
|
|
1170
1170
|
```
|
|
1171
1171
|
? Use stored Ticket ID "TICKET-123" for this submission?
|
|
1172
1172
|
❯ Yes, use "TICKET-123"
|
|
1173
|
+
Use a different ticket for this submission only
|
|
1174
|
+
Use a different ticket for this and future submissions
|
|
1173
1175
|
No, clear stored ticket
|
|
1174
1176
|
Cancel submission
|
|
1175
1177
|
```
|
|
1176
1178
|
|
|
1179
|
+
- **Yes** — applies the stored ticket to all records in this submission
|
|
1180
|
+
- **Different ticket (this submission only)** — prompts for a ticket ID, uses it for all records in this invocation without updating `ticketing.local.json`
|
|
1181
|
+
- **Different ticket (this and future)** — prompts for a ticket ID, updates `ticketing.local.json`, and uses it for this and all future submissions
|
|
1182
|
+
- **No, clear** — removes the stored `ticket_id` and proceeds without a ticket
|
|
1183
|
+
- **Cancel** — aborts the submission
|
|
1184
|
+
|
|
1177
1185
|
#### `.dbo/ticketing.local.json`
|
|
1178
1186
|
|
|
1179
1187
|
Stores ticket IDs for automatic application during submissions:
|
|
@@ -1327,6 +1335,11 @@ dbo deploy css:colors --confirm false
|
|
|
1327
1335
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1328
1336
|
| `--ticket <id>` | Override ticket ID |
|
|
1329
1337
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1338
|
+
| `--json` | Output raw JSON |
|
|
1339
|
+
| `-v, --verbose` | Show HTTP request details |
|
|
1340
|
+
| `--domain <host>` | Override domain |
|
|
1341
|
+
|
|
1342
|
+
The `deploy` command includes the same automatic error recovery as `push`, `input`, and `add` — including ticket error handling, repository mismatch recovery, user identity prompts, and ModifyKey protection. When a stored ticket exists, you'll be prompted before the first submission (see [Pre-submission ticket prompt](#pre-submission-ticket-prompt)).
|
|
1330
1343
|
|
|
1331
1344
|
#### `dbo.deploy.json` manifest
|
|
1332
1345
|
|
|
@@ -1362,6 +1375,33 @@ Create a `dbo.deploy.json` file in your project root to define named deployments
|
|
|
1362
1375
|
|
|
1363
1376
|
This replaces the curl commands typically embedded in `package.json` scripts.
|
|
1364
1377
|
|
|
1378
|
+
#### Non-interactive mode (npm scripts)
|
|
1379
|
+
|
|
1380
|
+
When running `dbo deploy` (or any submission command) inside npm scripts or piped commands where stdin is not a TTY, the CLI automatically skips all interactive prompts and uses stored credentials:
|
|
1381
|
+
|
|
1382
|
+
- **Stored ticket** — auto-applied from `.dbo/ticketing.local.json`
|
|
1383
|
+
- **ModifyKey** — auto-applied from `.dbo/config.json` (`AppModifyKey`)
|
|
1384
|
+
- **User identity** — auto-applied from `.dbo/credentials.json`
|
|
1385
|
+
|
|
1386
|
+
If a required value is missing and no interactive prompt is possible, the command fails with a clear error message explaining how to set up the missing value.
|
|
1387
|
+
|
|
1388
|
+
Example `package.json` scripts:
|
|
1389
|
+
|
|
1390
|
+
```json
|
|
1391
|
+
{
|
|
1392
|
+
"scripts": {
|
|
1393
|
+
"deploy:css": "dbo deploy css:colors && dbo deploy css:layout",
|
|
1394
|
+
"deploy:docs": "dbo deploy doc:readme && dbo deploy doc:api",
|
|
1395
|
+
"deploy:all": "dbo deploy --all"
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
To set up for non-interactive use:
|
|
1401
|
+
1. Run `dbo login` to store user credentials
|
|
1402
|
+
2. Run `dbo clone` to store the ModifyKey (if applicable)
|
|
1403
|
+
3. Set a ticket interactively once — it persists in `ticketing.local.json` for future npm script runs
|
|
1404
|
+
|
|
1365
1405
|
---
|
|
1366
1406
|
|
|
1367
1407
|
## Global Flags
|
package/package.json
CHANGED
package/src/commands/content.js
CHANGED
|
@@ -2,10 +2,11 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { writeFile } from 'fs/promises';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { DboClient } from '../lib/client.js';
|
|
5
|
-
import { buildInputBody } from '../lib/input-parser.js';
|
|
5
|
+
import { buildInputBody, checkSubmitErrors } from '../lib/input-parser.js';
|
|
6
6
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
7
7
|
import { saveToDisk } from '../lib/save-to-disk.js';
|
|
8
8
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
9
|
+
import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
|
|
9
10
|
import { log } from '../lib/logger.js';
|
|
10
11
|
|
|
11
12
|
function collect(value, previous) {
|
|
@@ -44,11 +45,38 @@ const deployCmd = new Command('deploy')
|
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// Pre-flight ticket validation (only if no --ticket flag)
|
|
49
|
+
let sessionTicketOverride = null;
|
|
50
|
+
if (!options.ticket) {
|
|
51
|
+
const ticketCheck = await checkStoredTicket(options);
|
|
52
|
+
if (ticketCheck.cancel) {
|
|
53
|
+
log.info('Submission cancelled');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (ticketCheck.clearTicket) {
|
|
57
|
+
await clearGlobalTicket();
|
|
58
|
+
log.dim(' Cleared stored ticket');
|
|
59
|
+
}
|
|
60
|
+
if (ticketCheck.overrideTicket) {
|
|
61
|
+
sessionTicketOverride = ticketCheck.overrideTicket;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
const extraParams = { '_confirm': options.confirm };
|
|
48
66
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
49
67
|
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
50
68
|
|
|
51
69
|
const dataExprs = [`RowUID:${uid};column:content.Content@${filepath}`];
|
|
70
|
+
|
|
71
|
+
// Apply stored ticket if no --ticket flag
|
|
72
|
+
if (!options.ticket) {
|
|
73
|
+
const storedTicket = sessionTicketOverride || await getRecordTicket(uid) || await getGlobalTicket();
|
|
74
|
+
if (storedTicket) {
|
|
75
|
+
extraParams['_OverrideTicketID'] = storedTicket;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await applyStoredTicketToSubmission(dataExprs, 'content', uid, uid, options, sessionTicketOverride);
|
|
79
|
+
|
|
52
80
|
let body = await buildInputBody(dataExprs, extraParams);
|
|
53
81
|
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
54
82
|
|
|
@@ -61,6 +89,22 @@ const deployCmd = new Command('deploy')
|
|
|
61
89
|
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
62
90
|
}
|
|
63
91
|
|
|
92
|
+
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
93
|
+
const errorResult = await checkSubmitErrors(result);
|
|
94
|
+
if (errorResult) {
|
|
95
|
+
if (errorResult.skipRecord || errorResult.skipAll) {
|
|
96
|
+
log.info('Submission cancelled');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (errorResult.ticketExpressions?.length > 0) {
|
|
100
|
+
dataExprs.push(...errorResult.ticketExpressions);
|
|
101
|
+
}
|
|
102
|
+
const params = errorResult.retryParams || errorResult;
|
|
103
|
+
Object.assign(extraParams, params);
|
|
104
|
+
body = await buildInputBody(dataExprs, extraParams);
|
|
105
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
formatResponse(result, { json: options.json });
|
|
65
109
|
if (!result.successful) process.exit(1);
|
|
66
110
|
} catch (err) {
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { readFile } from 'fs/promises';
|
|
3
3
|
import { DboClient } from '../lib/client.js';
|
|
4
|
-
import { buildInputBody } from '../lib/input-parser.js';
|
|
4
|
+
import { buildInputBody, checkSubmitErrors } from '../lib/input-parser.js';
|
|
5
5
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
6
6
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
7
|
+
import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
|
|
7
8
|
import { log } from '../lib/logger.js';
|
|
8
9
|
|
|
9
10
|
const MANIFEST_FILE = 'dbo.deploy.json';
|
|
@@ -61,6 +62,23 @@ export const deployCommand = new Command('deploy')
|
|
|
61
62
|
}
|
|
62
63
|
let activeModifyKey = modifyKeyResult.modifyKey;
|
|
63
64
|
|
|
65
|
+
// Pre-flight ticket validation (only if no --ticket flag)
|
|
66
|
+
let sessionTicketOverride = null;
|
|
67
|
+
if (!options.ticket) {
|
|
68
|
+
const ticketCheck = await checkStoredTicket(options);
|
|
69
|
+
if (ticketCheck.cancel) {
|
|
70
|
+
log.info('Submission cancelled');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (ticketCheck.clearTicket) {
|
|
74
|
+
await clearGlobalTicket();
|
|
75
|
+
log.dim(' Cleared stored ticket');
|
|
76
|
+
}
|
|
77
|
+
if (ticketCheck.overrideTicket) {
|
|
78
|
+
sessionTicketOverride = ticketCheck.overrideTicket;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
for (const [entryName, entry] of entries) {
|
|
65
83
|
if (!entry) {
|
|
66
84
|
log.warn(`Skipping unknown deployment: ${entryName}`);
|
|
@@ -84,6 +102,16 @@ export const deployCommand = new Command('deploy')
|
|
|
84
102
|
if (activeModifyKey) extraParams['_modify_key'] = activeModifyKey;
|
|
85
103
|
|
|
86
104
|
const dataExprs = [`RowUID:${uid};column:${entity}.${column}@${file}`];
|
|
105
|
+
|
|
106
|
+
// Apply stored ticket if no --ticket flag
|
|
107
|
+
if (!options.ticket) {
|
|
108
|
+
const storedTicket = sessionTicketOverride || await getRecordTicket(uid) || await getGlobalTicket();
|
|
109
|
+
if (storedTicket) {
|
|
110
|
+
extraParams['_OverrideTicketID'] = storedTicket;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
await applyStoredTicketToSubmission(dataExprs, entity, uid, uid, options, sessionTicketOverride);
|
|
114
|
+
|
|
87
115
|
let body = await buildInputBody(dataExprs, extraParams);
|
|
88
116
|
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
89
117
|
|
|
@@ -97,11 +125,31 @@ export const deployCommand = new Command('deploy')
|
|
|
97
125
|
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
98
126
|
}
|
|
99
127
|
|
|
128
|
+
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
129
|
+
const errorResult = await checkSubmitErrors(result);
|
|
130
|
+
if (errorResult) {
|
|
131
|
+
if (errorResult.skipRecord) {
|
|
132
|
+
log.warn(` Skipping "${entryName}"`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (errorResult.skipAll) {
|
|
136
|
+
log.warn(` Skipping "${entryName}" and all remaining`);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (errorResult.ticketExpressions?.length > 0) {
|
|
140
|
+
dataExprs.push(...errorResult.ticketExpressions);
|
|
141
|
+
}
|
|
142
|
+
const params = errorResult.retryParams || errorResult;
|
|
143
|
+
Object.assign(extraParams, params);
|
|
144
|
+
body = await buildInputBody(dataExprs, extraParams);
|
|
145
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
146
|
+
}
|
|
147
|
+
|
|
100
148
|
if (result.successful) {
|
|
101
149
|
log.success(`${entryName} deployed`);
|
|
102
150
|
} else {
|
|
103
151
|
log.error(`${entryName} failed`);
|
|
104
|
-
|
|
152
|
+
formatResponse(result, { json: options.json });
|
|
105
153
|
if (!options.all) process.exit(1);
|
|
106
154
|
}
|
|
107
155
|
}
|
package/src/lib/input-parser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { log } from './logger.js';
|
|
3
3
|
import { loadUserInfo } from './config.js';
|
|
4
|
-
import { buildTicketExpression, setGlobalTicket, setRecordTicket } from './ticketing.js';
|
|
4
|
+
import { buildTicketExpression, setGlobalTicket, setRecordTicket, getGlobalTicket, getRecordTicket } from './ticketing.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Parse DBO input syntax and build form data.
|
|
@@ -136,9 +136,36 @@ export async function checkSubmitErrors(result) {
|
|
|
136
136
|
|
|
137
137
|
if (!needsTicket && !needsUser) return null;
|
|
138
138
|
|
|
139
|
-
const prompts = [];
|
|
140
139
|
const retryParams = {};
|
|
141
140
|
|
|
141
|
+
// Non-interactive mode: use stored credentials automatically
|
|
142
|
+
if (!process.stdin.isTTY) {
|
|
143
|
+
if (needsUser) {
|
|
144
|
+
const stored = await loadUserInfo();
|
|
145
|
+
const storedValue = needsUserUid ? stored.userUid : (stored.userId || stored.userUid);
|
|
146
|
+
if (storedValue) {
|
|
147
|
+
log.info(`Using stored user ${needsUserUid ? 'UID' : 'ID'}: ${storedValue} (non-interactive mode)`);
|
|
148
|
+
if (needsUserUid) {
|
|
149
|
+
retryParams['_OverrideUserUID'] = storedValue;
|
|
150
|
+
} else {
|
|
151
|
+
retryParams['_OverrideUserID'] = storedValue;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
log.error(`This operation requires an authenticated user (${matchedUserPattern}).`);
|
|
155
|
+
log.dim(' Run "dbo login" first, or use an interactive terminal.');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (needsTicket) {
|
|
160
|
+
log.error('This operation requires a Ticket ID.');
|
|
161
|
+
log.dim(' Use --ticket <id> or run interactively first to set a ticket.');
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return Object.keys(retryParams).length > 0 ? retryParams : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const prompts = [];
|
|
168
|
+
|
|
142
169
|
if (needsUser) {
|
|
143
170
|
const idType = needsUserUid ? 'UID' : 'ID';
|
|
144
171
|
log.warn(`This operation requires an authenticated user (${matchedUserPattern}).`);
|
|
@@ -212,8 +239,6 @@ export async function checkSubmitErrors(result) {
|
|
|
212
239
|
* Prompts the user with 4 recovery options.
|
|
213
240
|
*/
|
|
214
241
|
async function handleTicketError(allText) {
|
|
215
|
-
const inquirer = (await import('inquirer')).default;
|
|
216
|
-
|
|
217
242
|
// Try to extract record details from error text
|
|
218
243
|
const entityMatch = allText.match(/entity:(\w+)/);
|
|
219
244
|
const rowIdMatch = allText.match(/RowID:(\d+)/);
|
|
@@ -222,8 +247,24 @@ async function handleTicketError(allText) {
|
|
|
222
247
|
const rowId = rowIdMatch?.[1];
|
|
223
248
|
const uid = uidMatch?.[1];
|
|
224
249
|
|
|
250
|
+
// Non-interactive mode: try stored ticket, otherwise fail with instructions
|
|
251
|
+
if (!process.stdin.isTTY) {
|
|
252
|
+
const storedTicket = (uid && await getRecordTicket(uid)) || await getGlobalTicket();
|
|
253
|
+
if (storedTicket) {
|
|
254
|
+
log.info(`Using stored ticket "${storedTicket}" (non-interactive mode)`);
|
|
255
|
+
return {
|
|
256
|
+
retryParams: { '_OverrideTicketID': storedTicket },
|
|
257
|
+
ticketExpressions: entity && rowId ? [buildTicketExpression(entity, rowId, storedTicket)] : [],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
log.error('This record requires a Ticket ID but no stored ticket is available.');
|
|
261
|
+
log.dim(' Run interactively first to set a ticket, or use --ticket <id>');
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
225
265
|
log.warn('This record update requires a Ticket ID.');
|
|
226
266
|
|
|
267
|
+
const inquirer = (await import('inquirer')).default;
|
|
227
268
|
const answers = await inquirer.prompt([
|
|
228
269
|
{
|
|
229
270
|
type: 'list',
|
|
@@ -279,8 +320,6 @@ async function handleTicketError(allText) {
|
|
|
279
320
|
* Prompts the user with 6 recovery options.
|
|
280
321
|
*/
|
|
281
322
|
async function handleRepoMismatch(allText) {
|
|
282
|
-
const inquirer = (await import('inquirer')).default;
|
|
283
|
-
|
|
284
323
|
// Try to extract ticket ID from error text
|
|
285
324
|
const ticketMatch = allText.match(/Ticket(?:\s+ID)?\s+(?:of\s+)?([A-Za-z0-9_-]+)/i);
|
|
286
325
|
const ticketId = ticketMatch?.[1] || 'unknown';
|
|
@@ -293,8 +332,18 @@ async function handleRepoMismatch(allText) {
|
|
|
293
332
|
const rowId = rowIdMatch?.[1];
|
|
294
333
|
const uid = uidMatch?.[1];
|
|
295
334
|
|
|
335
|
+
// Non-interactive mode: commit anyway with the existing ticket
|
|
336
|
+
if (!process.stdin.isTTY) {
|
|
337
|
+
log.warn(`Ticket "${ticketId}" is for another repository — committing anyway (non-interactive mode)`);
|
|
338
|
+
return {
|
|
339
|
+
retryParams: { '_OverrideTicketID': ticketId },
|
|
340
|
+
ticketExpressions: [],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
296
344
|
log.warn(`Ticket "${ticketId}" is for another repository.`);
|
|
297
345
|
|
|
346
|
+
const inquirer = (await import('inquirer')).default;
|
|
298
347
|
const answers = await inquirer.prompt([
|
|
299
348
|
{
|
|
300
349
|
type: 'list',
|
package/src/lib/modify-key.js
CHANGED
|
@@ -15,6 +15,12 @@ export async function checkModifyKey(options = {}) {
|
|
|
15
15
|
const storedKey = await loadAppModifyKey();
|
|
16
16
|
if (!storedKey) return { modifyKey: null, cancel: false };
|
|
17
17
|
|
|
18
|
+
// Non-interactive mode: use stored key automatically
|
|
19
|
+
if (!process.stdin.isTTY) {
|
|
20
|
+
log.info('Using stored ModifyKey (non-interactive mode)');
|
|
21
|
+
return { modifyKey: storedKey, cancel: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
log.warn('');
|
|
19
25
|
log.warn(' ⚠ This app is locked (production mode). A ModifyKey is required to submit changes.');
|
|
20
26
|
log.warn('');
|
|
@@ -61,6 +67,13 @@ export function isModifyKeyError(responseMessage) {
|
|
|
61
67
|
* Returns { modifyKey: string } on success or { modifyKey: null, cancel: true } on cancel.
|
|
62
68
|
*/
|
|
63
69
|
export async function handleModifyKeyError() {
|
|
70
|
+
// Non-interactive mode: cannot recover without user input
|
|
71
|
+
if (!process.stdin.isTTY) {
|
|
72
|
+
log.error('This app requires a ModifyKey but none is stored locally.');
|
|
73
|
+
log.dim(' Run "dbo clone" to fetch the key, or use --modify-key <key>');
|
|
74
|
+
return { modifyKey: null, cancel: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
log.warn('');
|
|
65
78
|
log.warn(' ⚠ This app requires a ModifyKey. The key has not been set locally (try re-running `dbo clone`).');
|
|
66
79
|
log.warn('');
|
package/src/lib/ticketing.js
CHANGED
|
@@ -21,7 +21,13 @@ const DEFAULT_TICKETING = { ticket_id: null, records: [] };
|
|
|
21
21
|
export async function loadTicketing() {
|
|
22
22
|
try {
|
|
23
23
|
const raw = await readFile(ticketingPath(), 'utf8');
|
|
24
|
-
const
|
|
24
|
+
const trimmed = raw.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
const defaults = { ...DEFAULT_TICKETING, records: [] };
|
|
27
|
+
await saveTicketing(defaults);
|
|
28
|
+
return defaults;
|
|
29
|
+
}
|
|
30
|
+
const data = JSON.parse(trimmed);
|
|
25
31
|
return {
|
|
26
32
|
ticket_id: data.ticket_id || null,
|
|
27
33
|
records: Array.isArray(data.records) ? data.records : [],
|
|
@@ -30,7 +36,9 @@ export async function loadTicketing() {
|
|
|
30
36
|
if (err.code !== 'ENOENT') {
|
|
31
37
|
log.warn('Ticketing config is corrupted or unreadable — starting fresh.');
|
|
32
38
|
}
|
|
33
|
-
|
|
39
|
+
const defaults = { ...DEFAULT_TICKETING, records: [] };
|
|
40
|
+
try { await saveTicketing(defaults); } catch { /* ignore write errors */ }
|
|
41
|
+
return defaults;
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
44
|
|
|
@@ -123,9 +131,12 @@ export async function clearAllRecordTickets() {
|
|
|
123
131
|
|
|
124
132
|
/**
|
|
125
133
|
* Build a ticket column expression for DBO input syntax.
|
|
134
|
+
* Uses RowUID when rowId is non-numeric (a UID string).
|
|
126
135
|
*/
|
|
127
136
|
export function buildTicketExpression(entity, rowId, ticketId) {
|
|
128
|
-
|
|
137
|
+
const isNumeric = /^-?\d+$/.test(String(rowId));
|
|
138
|
+
const rowKey = isNumeric ? 'RowID' : 'RowUID';
|
|
139
|
+
return `${rowKey}:${rowId};column:${entity}._LastUpdatedTicketID=${ticketId}`;
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
/**
|
|
@@ -145,6 +156,12 @@ export async function checkStoredTicket(options) {
|
|
|
145
156
|
return { useTicket: false, clearTicket: false, cancel: false };
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
// Non-interactive mode: auto-use stored ticket
|
|
160
|
+
if (!process.stdin.isTTY) {
|
|
161
|
+
log.info(`Using stored ticket "${data.ticket_id}" (non-interactive mode)`);
|
|
162
|
+
return { useTicket: true, clearTicket: false, cancel: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
const inquirer = (await import('inquirer')).default;
|
|
149
166
|
const { action } = await inquirer.prompt([{
|
|
150
167
|
type: 'list',
|
|
@@ -152,11 +169,31 @@ export async function checkStoredTicket(options) {
|
|
|
152
169
|
message: `Use stored Ticket ID "${data.ticket_id}" for this submission?`,
|
|
153
170
|
choices: [
|
|
154
171
|
{ name: `Yes, use "${data.ticket_id}"`, value: 'use' },
|
|
172
|
+
{ name: 'Use a different ticket for this submission only', value: 'alt_once' },
|
|
173
|
+
{ name: 'Use a different ticket for this and future submissions', value: 'alt_save' },
|
|
155
174
|
{ name: 'No, clear stored ticket', value: 'clear' },
|
|
156
175
|
{ name: 'Cancel submission', value: 'cancel' },
|
|
157
176
|
],
|
|
158
177
|
}]);
|
|
159
178
|
|
|
179
|
+
if (action === 'alt_once' || action === 'alt_save') {
|
|
180
|
+
const { altTicket } = await inquirer.prompt([{
|
|
181
|
+
type: 'input',
|
|
182
|
+
name: 'altTicket',
|
|
183
|
+
message: 'Ticket ID:',
|
|
184
|
+
}]);
|
|
185
|
+
const ticket = altTicket.trim();
|
|
186
|
+
if (!ticket) {
|
|
187
|
+
log.error(' No Ticket ID entered. Submission cancelled.');
|
|
188
|
+
return { useTicket: false, clearTicket: false, cancel: true };
|
|
189
|
+
}
|
|
190
|
+
if (action === 'alt_save') {
|
|
191
|
+
await saveTicketing({ ...data, ticket_id: ticket });
|
|
192
|
+
log.dim(` Stored ticket updated to "${ticket}"`);
|
|
193
|
+
}
|
|
194
|
+
return { useTicket: true, clearTicket: false, cancel: false, overrideTicket: ticket };
|
|
195
|
+
}
|
|
196
|
+
|
|
160
197
|
return {
|
|
161
198
|
useTicket: action === 'use',
|
|
162
199
|
clearTicket: action === 'clear',
|
|
@@ -173,13 +210,14 @@ export async function checkStoredTicket(options) {
|
|
|
173
210
|
* @param {string|number} rowId - Row ID or UID used in the submission
|
|
174
211
|
* @param {string} uid - Record UID for per-record lookup
|
|
175
212
|
* @param {Object} options - Command options
|
|
213
|
+
* @param {string|null} [sessionOverride] - One-time ticket override from pre-flight prompt
|
|
176
214
|
*/
|
|
177
|
-
export async function applyStoredTicketToSubmission(dataExprs, entity, rowId, uid, options) {
|
|
215
|
+
export async function applyStoredTicketToSubmission(dataExprs, entity, rowId, uid, options, sessionOverride = null) {
|
|
178
216
|
if (options.ticket) return; // --ticket flag takes precedence
|
|
179
217
|
|
|
180
218
|
const recordTicket = await getRecordTicket(uid);
|
|
181
219
|
const globalTicket = await getGlobalTicket();
|
|
182
|
-
const ticketToUse = recordTicket || globalTicket;
|
|
220
|
+
const ticketToUse = sessionOverride || recordTicket || globalTicket;
|
|
183
221
|
|
|
184
222
|
if (ticketToUse) {
|
|
185
223
|
const ticketExpr = buildTicketExpression(entity, rowId, ticketToUse);
|