@dboio/cli 0.6.10 → 0.6.12
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 +58 -3
- package/package.json +1 -1
- package/src/commands/content.js +71 -6
- package/src/commands/deploy.js +80 -9
- 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
|
@@ -618,6 +618,7 @@ dbo content deploy albain3dwkofbhnd1qtd1q assets/css/colors.css
|
|
|
618
618
|
dbo content deploy rncjivlghu65bmkbjnxynq assets/js/app.js
|
|
619
619
|
dbo content deploy abc123 docs/readme.md --ticket myTicket
|
|
620
620
|
dbo content deploy abc123 test.html --confirm false # validate only
|
|
621
|
+
dbo content deploy abc123 image.png --multipart # binary file upload
|
|
621
622
|
```
|
|
622
623
|
|
|
623
624
|
This is a shorthand for:
|
|
@@ -632,6 +633,7 @@ dbo input -d 'RowUID:albain3dwkofbhnd1qtd1q;column:content.Content@assets/css/co
|
|
|
632
633
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
633
634
|
| `--ticket <id>` | Override ticket ID |
|
|
634
635
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
636
|
+
| `--multipart` | Use multipart/form-data upload (for binary files) |
|
|
635
637
|
|
|
636
638
|
#### `dbo content pull`
|
|
637
639
|
|
|
@@ -1165,15 +1167,23 @@ The submission is then retried with `_OverrideTicketID`. To skip the prompt, pas
|
|
|
1165
1167
|
|
|
1166
1168
|
#### Pre-submission ticket prompt
|
|
1167
1169
|
|
|
1168
|
-
When a stored ticket exists in `.dbo/ticketing.local.json`, the CLI prompts before batch submissions:
|
|
1170
|
+
When a stored ticket exists in `.dbo/ticketing.local.json`, the CLI prompts before batch submissions (`push`, `input`, `add`, `content deploy`, `deploy`):
|
|
1169
1171
|
|
|
1170
1172
|
```
|
|
1171
1173
|
? Use stored Ticket ID "TICKET-123" for this submission?
|
|
1172
1174
|
❯ Yes, use "TICKET-123"
|
|
1175
|
+
Use a different ticket for this submission only
|
|
1176
|
+
Use a different ticket for this and future submissions
|
|
1173
1177
|
No, clear stored ticket
|
|
1174
1178
|
Cancel submission
|
|
1175
1179
|
```
|
|
1176
1180
|
|
|
1181
|
+
- **Yes** — applies the stored ticket to all records in this submission
|
|
1182
|
+
- **Different ticket (this submission only)** — prompts for a ticket ID, uses it for all records in this invocation without updating `ticketing.local.json`
|
|
1183
|
+
- **Different ticket (this and future)** — prompts for a ticket ID, updates `ticketing.local.json`, and uses it for this and all future submissions
|
|
1184
|
+
- **No, clear** — removes the stored `ticket_id` and proceeds without a ticket
|
|
1185
|
+
- **Cancel** — aborts the submission
|
|
1186
|
+
|
|
1177
1187
|
#### `.dbo/ticketing.local.json`
|
|
1178
1188
|
|
|
1179
1189
|
Stores ticket IDs for automatic application during submissions:
|
|
@@ -1327,6 +1337,11 @@ dbo deploy css:colors --confirm false
|
|
|
1327
1337
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1328
1338
|
| `--ticket <id>` | Override ticket ID |
|
|
1329
1339
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1340
|
+
| `--json` | Output raw JSON |
|
|
1341
|
+
| `-v, --verbose` | Show HTTP request details |
|
|
1342
|
+
| `--domain <host>` | Override domain |
|
|
1343
|
+
|
|
1344
|
+
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
1345
|
|
|
1331
1346
|
#### `dbo.deploy.json` manifest
|
|
1332
1347
|
|
|
@@ -1348,6 +1363,17 @@ Create a `dbo.deploy.json` file in your project root to define named deployments
|
|
|
1348
1363
|
"file": "docs/colors.md",
|
|
1349
1364
|
"entity": "extension",
|
|
1350
1365
|
"column": "Text"
|
|
1366
|
+
},
|
|
1367
|
+
"img:logo": {
|
|
1368
|
+
"uid": "x9fk2m3npqrs7tuvwyz1ab",
|
|
1369
|
+
"file": "assets/images/logo.png",
|
|
1370
|
+
"multipart": true
|
|
1371
|
+
},
|
|
1372
|
+
"upload:icons": {
|
|
1373
|
+
"uid": "7ddf10982a96457fa4f440",
|
|
1374
|
+
"file": "assets/css/launchpad-icons.css",
|
|
1375
|
+
"multipart": true,
|
|
1376
|
+
"filename": "launchpad-icons.css"
|
|
1351
1377
|
}
|
|
1352
1378
|
}
|
|
1353
1379
|
}
|
|
@@ -1357,11 +1383,40 @@ Create a `dbo.deploy.json` file in your project root to define named deployments
|
|
|
1357
1383
|
|-------|-------------|---------|
|
|
1358
1384
|
| `uid` | Target record UID (required) | — |
|
|
1359
1385
|
| `file` | Local file path (required) | — |
|
|
1360
|
-
| `entity` | Target entity name | `content` |
|
|
1361
|
-
| `column` | Target column name | `Content` |
|
|
1386
|
+
| `entity` | Target entity name | `content` (`media` when `multipart: true`) |
|
|
1387
|
+
| `column` | Target column name | `Content` (`File` when `multipart: true`) |
|
|
1388
|
+
| `multipart` | Use multipart/form-data upload (for binary files) | `false` |
|
|
1389
|
+
| `filename` | Set the `Filename` column on the target record | basename of `file` |
|
|
1362
1390
|
|
|
1363
1391
|
This replaces the curl commands typically embedded in `package.json` scripts.
|
|
1364
1392
|
|
|
1393
|
+
#### Non-interactive mode (npm scripts)
|
|
1394
|
+
|
|
1395
|
+
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:
|
|
1396
|
+
|
|
1397
|
+
- **Stored ticket** — auto-applied from `.dbo/ticketing.local.json`
|
|
1398
|
+
- **ModifyKey** — auto-applied from `.dbo/config.json` (`AppModifyKey`)
|
|
1399
|
+
- **User identity** — auto-applied from `.dbo/credentials.json`
|
|
1400
|
+
|
|
1401
|
+
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.
|
|
1402
|
+
|
|
1403
|
+
Example `package.json` scripts:
|
|
1404
|
+
|
|
1405
|
+
```json
|
|
1406
|
+
{
|
|
1407
|
+
"scripts": {
|
|
1408
|
+
"deploy:css": "dbo deploy css:colors && dbo deploy css:layout",
|
|
1409
|
+
"deploy:docs": "dbo deploy doc:readme && dbo deploy doc:api",
|
|
1410
|
+
"deploy:all": "dbo deploy --all"
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
To set up for non-interactive use:
|
|
1416
|
+
1. Run `dbo login` to store user credentials
|
|
1417
|
+
2. Run `dbo clone` to store the ModifyKey (if applicable)
|
|
1418
|
+
3. Set a ticket interactively once — it persists in `ticketing.local.json` for future npm script runs
|
|
1419
|
+
|
|
1365
1420
|
---
|
|
1366
1421
|
|
|
1367
1422
|
## 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) {
|
|
@@ -30,6 +31,7 @@ const deployCmd = new Command('deploy')
|
|
|
30
31
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
31
32
|
.option('--ticket <id>', 'Override ticket ID')
|
|
32
33
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
34
|
+
.option('--multipart', 'Use multipart/form-data upload (for binary files)')
|
|
33
35
|
.option('--json', 'Output raw JSON')
|
|
34
36
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
35
37
|
.option('--domain <host>', 'Override domain')
|
|
@@ -44,21 +46,84 @@ const deployCmd = new Command('deploy')
|
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// Pre-flight ticket validation (only if no --ticket flag)
|
|
50
|
+
let sessionTicketOverride = null;
|
|
51
|
+
if (!options.ticket) {
|
|
52
|
+
const ticketCheck = await checkStoredTicket(options);
|
|
53
|
+
if (ticketCheck.cancel) {
|
|
54
|
+
log.info('Submission cancelled');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (ticketCheck.clearTicket) {
|
|
58
|
+
await clearGlobalTicket();
|
|
59
|
+
log.dim(' Cleared stored ticket');
|
|
60
|
+
}
|
|
61
|
+
if (ticketCheck.overrideTicket) {
|
|
62
|
+
sessionTicketOverride = ticketCheck.overrideTicket;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
const extraParams = { '_confirm': options.confirm };
|
|
48
67
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
49
68
|
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
50
69
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
const isMultipart = options.multipart === true;
|
|
71
|
+
const dataExprs = isMultipart ? [] : [`RowUID:${uid};column:content.Content@${filepath}`];
|
|
72
|
+
|
|
73
|
+
// Apply stored ticket if no --ticket flag
|
|
74
|
+
if (!options.ticket) {
|
|
75
|
+
const storedTicket = sessionTicketOverride || await getRecordTicket(uid) || await getGlobalTicket();
|
|
76
|
+
if (storedTicket) {
|
|
77
|
+
extraParams['_OverrideTicketID'] = storedTicket;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await applyStoredTicketToSubmission(dataExprs, 'content', uid, uid, options, sessionTicketOverride);
|
|
81
|
+
|
|
82
|
+
// Submit helper — handles both URL-encoded and multipart modes
|
|
83
|
+
async function submit() {
|
|
84
|
+
if (isMultipart) {
|
|
85
|
+
const fields = { ...extraParams };
|
|
86
|
+
for (const expr of dataExprs) {
|
|
87
|
+
const eqIdx = expr.indexOf('=');
|
|
88
|
+
if (eqIdx !== -1) {
|
|
89
|
+
fields[expr.substring(0, eqIdx)] = expr.substring(eqIdx + 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const files = [{
|
|
93
|
+
fieldName: `RowUID:${uid};column:content.Content`,
|
|
94
|
+
filePath: filepath,
|
|
95
|
+
fileName: filepath.split('/').pop(),
|
|
96
|
+
}];
|
|
97
|
+
return client.postMultipart('/api/input/submit', fields, files);
|
|
98
|
+
} else {
|
|
99
|
+
const body = await buildInputBody(dataExprs, extraParams);
|
|
100
|
+
return client.postUrlEncoded('/api/input/submit', body);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let result = await submit();
|
|
54
105
|
|
|
55
106
|
// Reactive ModifyKey retry
|
|
56
107
|
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
57
108
|
const retryMK = await handleModifyKeyError();
|
|
58
109
|
if (retryMK.cancel) { log.info('Submission cancelled'); return; }
|
|
59
110
|
extraParams['_modify_key'] = retryMK.modifyKey;
|
|
60
|
-
|
|
61
|
-
|
|
111
|
+
result = await submit();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
115
|
+
const errorResult = await checkSubmitErrors(result);
|
|
116
|
+
if (errorResult) {
|
|
117
|
+
if (errorResult.skipRecord || errorResult.skipAll) {
|
|
118
|
+
log.info('Submission cancelled');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (errorResult.ticketExpressions?.length > 0) {
|
|
122
|
+
dataExprs.push(...errorResult.ticketExpressions);
|
|
123
|
+
}
|
|
124
|
+
const params = errorResult.retryParams || errorResult;
|
|
125
|
+
Object.assign(extraParams, params);
|
|
126
|
+
result = await submit();
|
|
62
127
|
}
|
|
63
128
|
|
|
64
129
|
formatResponse(result, { json: options.json });
|
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,16 +62,35 @@ 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}`);
|
|
67
85
|
continue;
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
88
|
+
const isMultipart = entry.multipart === true;
|
|
89
|
+
const entity = entry.entity || (isMultipart ? 'media' : 'content');
|
|
90
|
+
const column = entry.column || (isMultipart ? 'File' : 'Content');
|
|
72
91
|
const uid = entry.uid;
|
|
73
92
|
const file = entry.file;
|
|
93
|
+
const filename = entry.filename || file.split('/').pop();
|
|
74
94
|
|
|
75
95
|
if (!uid || !file) {
|
|
76
96
|
log.warn(`Skipping "${entryName}": missing uid or file.`);
|
|
@@ -83,9 +103,42 @@ export const deployCommand = new Command('deploy')
|
|
|
83
103
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
84
104
|
if (activeModifyKey) extraParams['_modify_key'] = activeModifyKey;
|
|
85
105
|
|
|
86
|
-
const dataExprs =
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
const dataExprs = isMultipart
|
|
107
|
+
? [`RowUID:${uid};column:${entity}.Filename=${filename}`]
|
|
108
|
+
: [`RowUID:${uid};column:${entity}.${column}@${file}`];
|
|
109
|
+
|
|
110
|
+
// Apply stored ticket if no --ticket flag
|
|
111
|
+
if (!options.ticket) {
|
|
112
|
+
const storedTicket = sessionTicketOverride || await getRecordTicket(uid) || await getGlobalTicket();
|
|
113
|
+
if (storedTicket) {
|
|
114
|
+
extraParams['_OverrideTicketID'] = storedTicket;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await applyStoredTicketToSubmission(dataExprs, entity, uid, uid, options, sessionTicketOverride);
|
|
118
|
+
|
|
119
|
+
// Submit helper — handles both URL-encoded and multipart modes
|
|
120
|
+
async function submit() {
|
|
121
|
+
if (isMultipart) {
|
|
122
|
+
const fields = { ...extraParams };
|
|
123
|
+
for (const expr of dataExprs) {
|
|
124
|
+
const eqIdx = expr.indexOf('=');
|
|
125
|
+
if (eqIdx !== -1) {
|
|
126
|
+
fields[expr.substring(0, eqIdx)] = expr.substring(eqIdx + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const files = [{
|
|
130
|
+
fieldName: `RowUID:${uid};column:${entity}.${column}`,
|
|
131
|
+
filePath: file,
|
|
132
|
+
fileName: file.split('/').pop(),
|
|
133
|
+
}];
|
|
134
|
+
return client.postMultipart('/api/input/submit', fields, files);
|
|
135
|
+
} else {
|
|
136
|
+
const body = await buildInputBody(dataExprs, extraParams);
|
|
137
|
+
return client.postUrlEncoded('/api/input/submit', body);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let result = await submit();
|
|
89
142
|
|
|
90
143
|
// Reactive ModifyKey retry
|
|
91
144
|
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
@@ -93,15 +146,33 @@ export const deployCommand = new Command('deploy')
|
|
|
93
146
|
if (retryMK.cancel) { log.info('Submission cancelled'); break; }
|
|
94
147
|
activeModifyKey = retryMK.modifyKey;
|
|
95
148
|
extraParams['_modify_key'] = activeModifyKey;
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
result = await submit();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
153
|
+
const errorResult = await checkSubmitErrors(result);
|
|
154
|
+
if (errorResult) {
|
|
155
|
+
if (errorResult.skipRecord) {
|
|
156
|
+
log.warn(` Skipping "${entryName}"`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (errorResult.skipAll) {
|
|
160
|
+
log.warn(` Skipping "${entryName}" and all remaining`);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (errorResult.ticketExpressions?.length > 0) {
|
|
164
|
+
dataExprs.push(...errorResult.ticketExpressions);
|
|
165
|
+
}
|
|
166
|
+
const params = errorResult.retryParams || errorResult;
|
|
167
|
+
Object.assign(extraParams, params);
|
|
168
|
+
result = await submit();
|
|
98
169
|
}
|
|
99
170
|
|
|
100
171
|
if (result.successful) {
|
|
101
172
|
log.success(`${entryName} deployed`);
|
|
102
173
|
} else {
|
|
103
174
|
log.error(`${entryName} failed`);
|
|
104
|
-
|
|
175
|
+
formatResponse(result, { json: options.json });
|
|
105
176
|
if (!options.all) process.exit(1);
|
|
106
177
|
}
|
|
107
178
|
}
|
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);
|