@dboio/cli 0.6.9 → 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 +78 -2
- package/package.json +1 -1
- package/src/commands/add.js +1 -1
- package/src/commands/content.js +46 -2
- package/src/commands/deploy.js +51 -3
- package/src/commands/input.js +1 -1
- package/src/commands/push.js +1 -1
- package/src/lib/input-parser.js +55 -6
- package/src/lib/modify-key.js +14 -1
- package/src/lib/ticketing.js +43 -5
package/README.md
CHANGED
|
@@ -156,6 +156,7 @@ All configuration is **directory-scoped**. Each project folder maintains its own
|
|
|
156
156
|
| `AppUID` | string | App UID |
|
|
157
157
|
| `AppName` | string | App display name |
|
|
158
158
|
| `AppShortName` | string | App short name (used for `dbo clone --app`) |
|
|
159
|
+
| `AppModifyKey` | string | ModifyKey for locked/production apps (set by `dbo clone`, used for submission guards) |
|
|
159
160
|
| `ContentPlacement` | `bin` \| `path` \| `ask` | Where to place content files during clone |
|
|
160
161
|
| `MediaPlacement` | `bin` \| `fullpath` \| `ask` | Where to place media files during clone |
|
|
161
162
|
| `<Entity>FilenameCol` | column name | Filename column for entity-dir records (e.g., `ExtensionFilenameCol`) |
|
|
@@ -239,7 +240,7 @@ dbo init --domain my-domain.com --app myapp --clone
|
|
|
239
240
|
#### What clone does
|
|
240
241
|
|
|
241
242
|
1. **Loads app JSON** — from a local file, server API, or interactive prompt
|
|
242
|
-
2. **Updates `.dbo/config.json`** — saves `AppID`, `AppUID`, `AppName`, `AppShortName`
|
|
243
|
+
2. **Updates `.dbo/config.json`** — saves `AppID`, `AppUID`, `AppName`, `AppShortName`, and `AppModifyKey` (if the app is locked)
|
|
243
244
|
3. **Updates `package.json`** — populates `name`, `productName`, `description`, `homepage`, and `deploy` script
|
|
244
245
|
4. **Creates directories** — processes `children.bin` to build the directory hierarchy based on `ParentBinID` relationships
|
|
245
246
|
5. **Saves `.dbo/structure.json`** — maps BinIDs to directory paths for file placement
|
|
@@ -487,6 +488,7 @@ dbo input -d 'RowUID:abc;column:content.Content@file.css' -v
|
|
|
487
488
|
| `-f, --file <field=@path>` | File attachment for multipart upload (repeatable) |
|
|
488
489
|
| `-C, --confirm <true\|false>` | Commit changes (default: `true`). Use `false` for validation only |
|
|
489
490
|
| `--ticket <id>` | Override ticket ID (`_OverrideTicketID`) |
|
|
491
|
+
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
490
492
|
| `--login` | Auto-login user created by this submission |
|
|
491
493
|
| `--transactional` | Use transactional processing |
|
|
492
494
|
| `--json` | Output raw JSON response |
|
|
@@ -629,6 +631,7 @@ dbo input -d 'RowUID:albain3dwkofbhnd1qtd1q;column:content.Content@assets/css/co
|
|
|
629
631
|
| `<filepath>` | Local file path |
|
|
630
632
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
631
633
|
| `--ticket <id>` | Override ticket ID |
|
|
634
|
+
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
632
635
|
|
|
633
636
|
#### `dbo content pull`
|
|
634
637
|
|
|
@@ -939,6 +942,7 @@ The `@colors.css` reference tells push to read the content from that file. All o
|
|
|
939
942
|
| `<path>` | File or directory to push |
|
|
940
943
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
941
944
|
| `--ticket <id>` | Override ticket ID |
|
|
945
|
+
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
942
946
|
| `--meta-only` | Only push metadata, skip file content |
|
|
943
947
|
| `--content-only` | Only push file content, skip metadata |
|
|
944
948
|
| `-y, --yes` | Auto-accept all prompts |
|
|
@@ -1101,6 +1105,7 @@ The `@colors.css` reference tells the CLI to read the file content from `colors.
|
|
|
1101
1105
|
| `<path>` | File or `.` to scan current directory |
|
|
1102
1106
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1103
1107
|
| `--ticket <id>` | Override ticket ID |
|
|
1108
|
+
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1104
1109
|
| `-y, --yes` | Auto-accept all prompts |
|
|
1105
1110
|
| `--json` | Output raw JSON |
|
|
1106
1111
|
| `--jq <expr>` | Filter JSON response |
|
|
@@ -1160,15 +1165,23 @@ The submission is then retried with `_OverrideTicketID`. To skip the prompt, pas
|
|
|
1160
1165
|
|
|
1161
1166
|
#### Pre-submission ticket prompt
|
|
1162
1167
|
|
|
1163
|
-
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`):
|
|
1164
1169
|
|
|
1165
1170
|
```
|
|
1166
1171
|
? Use stored Ticket ID "TICKET-123" for this submission?
|
|
1167
1172
|
❯ Yes, use "TICKET-123"
|
|
1173
|
+
Use a different ticket for this submission only
|
|
1174
|
+
Use a different ticket for this and future submissions
|
|
1168
1175
|
No, clear stored ticket
|
|
1169
1176
|
Cancel submission
|
|
1170
1177
|
```
|
|
1171
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
|
+
|
|
1172
1185
|
#### `.dbo/ticketing.local.json`
|
|
1173
1186
|
|
|
1174
1187
|
Stores ticket IDs for automatic application during submissions:
|
|
@@ -1192,6 +1205,36 @@ Stores ticket IDs for automatic application during submissions:
|
|
|
1192
1205
|
- `records` — Per-record tickets (auto-cleared after successful submission)
|
|
1193
1206
|
- `--ticket` flag always takes precedence over stored tickets
|
|
1194
1207
|
|
|
1208
|
+
#### ModifyKey protection (locked/production apps)
|
|
1209
|
+
|
|
1210
|
+
When an app has a `ModifyKey` set (production/locked mode), the CLI guards all submission commands (`push`, `input`, `add`, `content deploy`, `deploy`) with an interactive prompt:
|
|
1211
|
+
|
|
1212
|
+
```
|
|
1213
|
+
⚠ This app is locked (production mode). A ModifyKey is required to submit changes.
|
|
1214
|
+
? This app has a ModifyKey set. How would you like to proceed?
|
|
1215
|
+
❯ Enter the ModifyKey to proceed
|
|
1216
|
+
Cancel submission
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
The ModifyKey is detected and stored during `dbo clone`. If the key wasn't stored locally but the server requires one, the CLI reactively prompts after the first failed submission:
|
|
1220
|
+
|
|
1221
|
+
```
|
|
1222
|
+
⚠ This app requires a ModifyKey. The key has not been set locally (try re-running `dbo clone`).
|
|
1223
|
+
? A ModifyKey is required. How would you like to proceed?
|
|
1224
|
+
❯ Enter the ModifyKey to retry
|
|
1225
|
+
Cancel submission
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
On successful reactive entry, the key is saved to `.dbo/config.json` for future use.
|
|
1229
|
+
|
|
1230
|
+
To bypass the interactive prompt entirely, pass `--modify-key <key>` on any submission command:
|
|
1231
|
+
|
|
1232
|
+
```bash
|
|
1233
|
+
dbo push . --modify-key mySecretKey
|
|
1234
|
+
dbo input -d '...' --modify-key mySecretKey
|
|
1235
|
+
dbo add myfile.css --modify-key mySecretKey
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1195
1238
|
#### User identity required
|
|
1196
1239
|
|
|
1197
1240
|
When the server returns an error mentioning `LoggedInUser_UID`, `LoggedInUserID`, `CurrentUserID`, or `UserID`, the CLI checks for a stored user identity from `dbo login`:
|
|
@@ -1291,6 +1334,12 @@ dbo deploy css:colors --confirm false
|
|
|
1291
1334
|
| `--all` | Deploy all entries in the manifest |
|
|
1292
1335
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1293
1336
|
| `--ticket <id>` | Override ticket ID |
|
|
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)).
|
|
1294
1343
|
|
|
1295
1344
|
#### `dbo.deploy.json` manifest
|
|
1296
1345
|
|
|
@@ -1326,6 +1375,33 @@ Create a `dbo.deploy.json` file in your project root to define named deployments
|
|
|
1326
1375
|
|
|
1327
1376
|
This replaces the curl commands typically embedded in `package.json` scripts.
|
|
1328
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
|
+
|
|
1329
1405
|
---
|
|
1330
1406
|
|
|
1331
1407
|
## Global Flags
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -18,7 +18,7 @@ export const addCommand = new Command('add')
|
|
|
18
18
|
.argument('<path>', 'File or "." to scan current directory')
|
|
19
19
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
20
20
|
.option('--ticket <id>', 'Override ticket ID')
|
|
21
|
-
.option('--
|
|
21
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
22
22
|
.option('-y, --yes', 'Auto-accept all prompts')
|
|
23
23
|
.option('--json', 'Output raw JSON')
|
|
24
24
|
.option('--jq <expr>', 'Filter JSON response')
|
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) {
|
|
@@ -29,7 +30,7 @@ const deployCmd = new Command('deploy')
|
|
|
29
30
|
.argument('<filepath>', 'Local file path')
|
|
30
31
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
31
32
|
.option('--ticket <id>', 'Override ticket ID')
|
|
32
|
-
.option('--
|
|
33
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
33
34
|
.option('--json', 'Output raw JSON')
|
|
34
35
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
35
36
|
.option('--domain <host>', 'Override domain')
|
|
@@ -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';
|
|
@@ -14,7 +15,7 @@ export const deployCommand = new Command('deploy')
|
|
|
14
15
|
.option('--all', 'Deploy all entries in the manifest')
|
|
15
16
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
16
17
|
.option('--ticket <id>', 'Override ticket ID')
|
|
17
|
-
.option('--
|
|
18
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
18
19
|
.option('--json', 'Output raw JSON')
|
|
19
20
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
20
21
|
.option('--domain <host>', 'Override domain')
|
|
@@ -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/commands/input.js
CHANGED
|
@@ -17,7 +17,7 @@ export const inputCommand = new Command('input')
|
|
|
17
17
|
.option('-f, --file <field=@path>', 'File attachment for multipart upload (repeatable)', collect, [])
|
|
18
18
|
.option('-C, --confirm <value>', 'Commit changes: true (default) or false for validation only', 'true')
|
|
19
19
|
.option('--ticket <id>', 'Override ticket ID (_OverrideTicketID)')
|
|
20
|
-
.option('--
|
|
20
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
21
21
|
.option('--login', 'Auto-login user created by this submission')
|
|
22
22
|
.option('--transactional', 'Use transactional processing')
|
|
23
23
|
.option('--json', 'Output raw JSON response')
|
package/src/commands/push.js
CHANGED
|
@@ -19,7 +19,7 @@ export const pushCommand = new Command('push')
|
|
|
19
19
|
.argument('<path>', 'File or directory to push')
|
|
20
20
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
21
21
|
.option('--ticket <id>', 'Override ticket ID')
|
|
22
|
-
.option('--
|
|
22
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
23
23
|
.option('--meta-only', 'Only push metadata changes, skip file content')
|
|
24
24
|
.option('--content-only', 'Only push file content, skip metadata columns')
|
|
25
25
|
.option('-y, --yes', 'Auto-accept all prompts (path refactoring, etc.)')
|
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
|
@@ -9,12 +9,18 @@ import { log } from './logger.js';
|
|
|
9
9
|
* - Returns { modifyKey: null, cancel: true } on mismatch or cancel.
|
|
10
10
|
*/
|
|
11
11
|
export async function checkModifyKey(options = {}) {
|
|
12
|
-
// --
|
|
12
|
+
// --modify-key flag takes precedence (mirrors --ticket / --confirm behavior)
|
|
13
13
|
if (options.modifyKey) return { modifyKey: options.modifyKey, cancel: false };
|
|
14
14
|
|
|
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);
|