@dboio/cli 0.6.8 → 0.6.10
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 +37 -1
- package/package.json +1 -1
- package/src/commands/add.js +21 -0
- package/src/commands/clone.js +11 -1
- package/src/commands/content.js +25 -2
- package/src/commands/deploy.js +25 -2
- package/src/commands/input.js +27 -0
- package/src/commands/push.js +47 -16
- package/src/lib/config.js +28 -1
- package/src/lib/input-parser.js +2 -1
- package/src/lib/modify-key.js +96 -0
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 |
|
|
@@ -1192,6 +1197,36 @@ Stores ticket IDs for automatic application during submissions:
|
|
|
1192
1197
|
- `records` — Per-record tickets (auto-cleared after successful submission)
|
|
1193
1198
|
- `--ticket` flag always takes precedence over stored tickets
|
|
1194
1199
|
|
|
1200
|
+
#### ModifyKey protection (locked/production apps)
|
|
1201
|
+
|
|
1202
|
+
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:
|
|
1203
|
+
|
|
1204
|
+
```
|
|
1205
|
+
⚠ This app is locked (production mode). A ModifyKey is required to submit changes.
|
|
1206
|
+
? This app has a ModifyKey set. How would you like to proceed?
|
|
1207
|
+
❯ Enter the ModifyKey to proceed
|
|
1208
|
+
Cancel submission
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
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:
|
|
1212
|
+
|
|
1213
|
+
```
|
|
1214
|
+
⚠ This app requires a ModifyKey. The key has not been set locally (try re-running `dbo clone`).
|
|
1215
|
+
? A ModifyKey is required. How would you like to proceed?
|
|
1216
|
+
❯ Enter the ModifyKey to retry
|
|
1217
|
+
Cancel submission
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
On successful reactive entry, the key is saved to `.dbo/config.json` for future use.
|
|
1221
|
+
|
|
1222
|
+
To bypass the interactive prompt entirely, pass `--modify-key <key>` on any submission command:
|
|
1223
|
+
|
|
1224
|
+
```bash
|
|
1225
|
+
dbo push . --modify-key mySecretKey
|
|
1226
|
+
dbo input -d '...' --modify-key mySecretKey
|
|
1227
|
+
dbo add myfile.css --modify-key mySecretKey
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1195
1230
|
#### User identity required
|
|
1196
1231
|
|
|
1197
1232
|
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 +1326,7 @@ dbo deploy css:colors --confirm false
|
|
|
1291
1326
|
| `--all` | Deploy all entries in the manifest |
|
|
1292
1327
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1293
1328
|
| `--ticket <id>` | Override ticket ID |
|
|
1329
|
+
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1294
1330
|
|
|
1295
1331
|
#### `dbo.deploy.json` manifest
|
|
1296
1332
|
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -8,6 +8,7 @@ import { log } from '../lib/logger.js';
|
|
|
8
8
|
import { shouldSkipColumn } from '../lib/columns.js';
|
|
9
9
|
import { loadAppConfig } from '../lib/config.js';
|
|
10
10
|
import { checkStoredTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
11
|
+
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
11
12
|
|
|
12
13
|
// Directories and patterns to skip when scanning with `dbo add .`
|
|
13
14
|
const IGNORE_DIRS = new Set(['.dbo', '.git', 'node_modules', '.svn', '.hg']);
|
|
@@ -17,6 +18,7 @@ export const addCommand = new Command('add')
|
|
|
17
18
|
.argument('<path>', 'File or "." to scan current directory')
|
|
18
19
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
19
20
|
.option('--ticket <id>', 'Override ticket ID')
|
|
21
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
20
22
|
.option('-y, --yes', 'Auto-accept all prompts')
|
|
21
23
|
.option('--json', 'Output raw JSON')
|
|
22
24
|
.option('--jq <expr>', 'Filter JSON response')
|
|
@@ -26,6 +28,14 @@ export const addCommand = new Command('add')
|
|
|
26
28
|
try {
|
|
27
29
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
28
30
|
|
|
31
|
+
// ModifyKey guard
|
|
32
|
+
const modifyKeyResult = await checkModifyKey(options);
|
|
33
|
+
if (modifyKeyResult.cancel) {
|
|
34
|
+
log.info('Submission cancelled');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (modifyKeyResult.modifyKey) options._resolvedModifyKey = modifyKeyResult.modifyKey;
|
|
38
|
+
|
|
29
39
|
if (targetPath === '.') {
|
|
30
40
|
await addDirectory(process.cwd(), client, options);
|
|
31
41
|
} else {
|
|
@@ -242,10 +252,21 @@ async function submitAdd(meta, metaPath, filePath, client, options) {
|
|
|
242
252
|
|
|
243
253
|
const extraParams = { '_confirm': options.confirm };
|
|
244
254
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
255
|
+
if (options._resolvedModifyKey) extraParams['_modify_key'] = options._resolvedModifyKey;
|
|
245
256
|
|
|
246
257
|
let body = await buildInputBody(dataExprs, extraParams);
|
|
247
258
|
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
248
259
|
|
|
260
|
+
// Reactive ModifyKey retry
|
|
261
|
+
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
262
|
+
const retryMK = await handleModifyKeyError();
|
|
263
|
+
if (retryMK.cancel) { log.info('Submission cancelled'); return null; }
|
|
264
|
+
extraParams['_modify_key'] = retryMK.modifyKey;
|
|
265
|
+
options._resolvedModifyKey = retryMK.modifyKey;
|
|
266
|
+
body = await buildInputBody(dataExprs, extraParams);
|
|
267
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
268
|
+
}
|
|
269
|
+
|
|
249
270
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
250
271
|
const retryResult = await checkSubmitErrors(result);
|
|
251
272
|
if (retryResult) {
|
package/src/commands/clone.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
3
3
|
import { join, basename, extname } from 'path';
|
|
4
4
|
import { DboClient } from '../lib/client.js';
|
|
5
|
-
import { loadConfig, updateConfigWithApp, loadClonePlacement, saveClonePlacement, ensureGitignore, saveEntityDirPreference, loadEntityDirPreference, saveEntityContentExtractions, loadEntityContentExtractions, saveAppJsonBaseline, addDeleteEntry, loadCollisionResolutions, saveCollisionResolutions, loadSynchronize } from '../lib/config.js';
|
|
5
|
+
import { loadConfig, updateConfigWithApp, loadClonePlacement, saveClonePlacement, ensureGitignore, saveEntityDirPreference, loadEntityDirPreference, saveEntityContentExtractions, loadEntityContentExtractions, saveAppJsonBaseline, addDeleteEntry, loadCollisionResolutions, saveCollisionResolutions, loadSynchronize, saveAppModifyKey } from '../lib/config.js';
|
|
6
6
|
import { buildBinHierarchy, resolveBinPath, createDirectories, saveStructureFile, getBinName, findBinByPath, BINS_DIR, DEFAULT_PROJECT_DIRS, ENTITY_DIR_MAP } from '../lib/structure.js';
|
|
7
7
|
import { log } from '../lib/logger.js';
|
|
8
8
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
@@ -501,6 +501,16 @@ export async function performClone(source, options = {}) {
|
|
|
501
501
|
});
|
|
502
502
|
log.dim(' Updated .dbo/config.json with app metadata');
|
|
503
503
|
|
|
504
|
+
// Detect and store ModifyKey for locked/production apps
|
|
505
|
+
const modifyKey = appJson.ModifyKey || null;
|
|
506
|
+
await saveAppModifyKey(modifyKey);
|
|
507
|
+
if (modifyKey) {
|
|
508
|
+
log.warn('');
|
|
509
|
+
log.warn(' ⚠ This app has a ModifyKey set (production/locked mode).');
|
|
510
|
+
log.warn(' You will be prompted to enter the ModifyKey before any push, input, add, content deploy, or deploy command.');
|
|
511
|
+
log.warn('');
|
|
512
|
+
}
|
|
513
|
+
|
|
504
514
|
// Step 3: Update package.json
|
|
505
515
|
await updatePackageJson(appJson, config);
|
|
506
516
|
|
package/src/commands/content.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DboClient } from '../lib/client.js';
|
|
|
5
5
|
import { buildInputBody } 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
|
+
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
8
9
|
import { log } from '../lib/logger.js';
|
|
9
10
|
|
|
10
11
|
function collect(value, previous) {
|
|
@@ -28,16 +29,38 @@ const deployCmd = new Command('deploy')
|
|
|
28
29
|
.argument('<filepath>', 'Local file path')
|
|
29
30
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
30
31
|
.option('--ticket <id>', 'Override ticket ID')
|
|
32
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
31
33
|
.option('--json', 'Output raw JSON')
|
|
32
34
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
33
35
|
.option('--domain <host>', 'Override domain')
|
|
34
36
|
.action(async (uid, filepath, options) => {
|
|
35
37
|
try {
|
|
36
38
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
39
|
+
|
|
40
|
+
// ModifyKey guard
|
|
41
|
+
const modifyKeyResult = await checkModifyKey(options);
|
|
42
|
+
if (modifyKeyResult.cancel) {
|
|
43
|
+
log.info('Submission cancelled');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
const extraParams = { '_confirm': options.confirm };
|
|
38
48
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
50
|
+
|
|
51
|
+
const dataExprs = [`RowUID:${uid};column:content.Content@${filepath}`];
|
|
52
|
+
let body = await buildInputBody(dataExprs, extraParams);
|
|
53
|
+
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
54
|
+
|
|
55
|
+
// Reactive ModifyKey retry
|
|
56
|
+
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
57
|
+
const retryMK = await handleModifyKeyError();
|
|
58
|
+
if (retryMK.cancel) { log.info('Submission cancelled'); return; }
|
|
59
|
+
extraParams['_modify_key'] = retryMK.modifyKey;
|
|
60
|
+
body = await buildInputBody(dataExprs, extraParams);
|
|
61
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
formatResponse(result, { json: options.json });
|
|
42
65
|
if (!result.successful) process.exit(1);
|
|
43
66
|
} catch (err) {
|
package/src/commands/deploy.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from 'fs/promises';
|
|
|
3
3
|
import { DboClient } from '../lib/client.js';
|
|
4
4
|
import { buildInputBody } from '../lib/input-parser.js';
|
|
5
5
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
6
|
+
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
6
7
|
import { log } from '../lib/logger.js';
|
|
7
8
|
|
|
8
9
|
const MANIFEST_FILE = 'dbo.deploy.json';
|
|
@@ -13,6 +14,7 @@ export const deployCommand = new Command('deploy')
|
|
|
13
14
|
.option('--all', 'Deploy all entries in the manifest')
|
|
14
15
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
15
16
|
.option('--ticket <id>', 'Override ticket ID')
|
|
17
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
16
18
|
.option('--json', 'Output raw JSON')
|
|
17
19
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
18
20
|
.option('--domain <host>', 'Override domain')
|
|
@@ -51,6 +53,14 @@ export const deployCommand = new Command('deploy')
|
|
|
51
53
|
process.exit(1);
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
// ModifyKey guard — check once before any submissions
|
|
57
|
+
const modifyKeyResult = await checkModifyKey(options);
|
|
58
|
+
if (modifyKeyResult.cancel) {
|
|
59
|
+
log.info('Submission cancelled');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let activeModifyKey = modifyKeyResult.modifyKey;
|
|
63
|
+
|
|
54
64
|
for (const [entryName, entry] of entries) {
|
|
55
65
|
if (!entry) {
|
|
56
66
|
log.warn(`Skipping unknown deployment: ${entryName}`);
|
|
@@ -71,8 +81,21 @@ export const deployCommand = new Command('deploy')
|
|
|
71
81
|
|
|
72
82
|
const extraParams = { '_confirm': options.confirm };
|
|
73
83
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
if (activeModifyKey) extraParams['_modify_key'] = activeModifyKey;
|
|
85
|
+
|
|
86
|
+
const dataExprs = [`RowUID:${uid};column:${entity}.${column}@${file}`];
|
|
87
|
+
let body = await buildInputBody(dataExprs, extraParams);
|
|
88
|
+
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
89
|
+
|
|
90
|
+
// Reactive ModifyKey retry
|
|
91
|
+
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
92
|
+
const retryMK = await handleModifyKeyError();
|
|
93
|
+
if (retryMK.cancel) { log.info('Submission cancelled'); break; }
|
|
94
|
+
activeModifyKey = retryMK.modifyKey;
|
|
95
|
+
extraParams['_modify_key'] = activeModifyKey;
|
|
96
|
+
body = await buildInputBody(dataExprs, extraParams);
|
|
97
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
98
|
+
}
|
|
76
99
|
|
|
77
100
|
if (result.successful) {
|
|
78
101
|
log.success(`${entryName} deployed`);
|
package/src/commands/input.js
CHANGED
|
@@ -4,6 +4,7 @@ import { buildInputBody, parseFileArg, checkSubmitErrors } from '../lib/input-pa
|
|
|
4
4
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
5
5
|
import { loadAppConfig } from '../lib/config.js';
|
|
6
6
|
import { checkStoredTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
7
|
+
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
7
8
|
import { log } from '../lib/logger.js';
|
|
8
9
|
|
|
9
10
|
function collect(value, previous) {
|
|
@@ -16,6 +17,7 @@ export const inputCommand = new Command('input')
|
|
|
16
17
|
.option('-f, --file <field=@path>', 'File attachment for multipart upload (repeatable)', collect, [])
|
|
17
18
|
.option('-C, --confirm <value>', 'Commit changes: true (default) or false for validation only', 'true')
|
|
18
19
|
.option('--ticket <id>', 'Override ticket ID (_OverrideTicketID)')
|
|
20
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
19
21
|
.option('--login', 'Auto-login user created by this submission')
|
|
20
22
|
.option('--transactional', 'Use transactional processing')
|
|
21
23
|
.option('--json', 'Output raw JSON response')
|
|
@@ -45,6 +47,14 @@ export const inputCommand = new Command('input')
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
// ModifyKey guard
|
|
51
|
+
const modifyKeyResult = await checkModifyKey(options);
|
|
52
|
+
if (modifyKeyResult.cancel) {
|
|
53
|
+
log.info('Submission cancelled');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
57
|
+
|
|
48
58
|
// Check if data expressions include AppID; if not and config has one, prompt
|
|
49
59
|
const allDataText = options.data.join(' ');
|
|
50
60
|
const hasAppId = /\.AppID[=@]/.test(allDataText) || /AppID=/.test(allDataText);
|
|
@@ -93,6 +103,14 @@ export const inputCommand = new Command('input')
|
|
|
93
103
|
const files = options.file.map(parseFileArg);
|
|
94
104
|
let result = await client.postMultipart('/api/input/submit', fields, files);
|
|
95
105
|
|
|
106
|
+
// Reactive ModifyKey retry
|
|
107
|
+
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
108
|
+
const retryMK = await handleModifyKeyError();
|
|
109
|
+
if (retryMK.cancel) { log.info('Submission cancelled'); return; }
|
|
110
|
+
fields['_modify_key'] = retryMK.modifyKey;
|
|
111
|
+
result = await client.postMultipart('/api/input/submit', fields, files);
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
97
115
|
const retryResult = await checkSubmitErrors(result);
|
|
98
116
|
if (retryResult) {
|
|
@@ -112,6 +130,15 @@ export const inputCommand = new Command('input')
|
|
|
112
130
|
let body = await buildInputBody(options.data, extraParams);
|
|
113
131
|
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
114
132
|
|
|
133
|
+
// Reactive ModifyKey retry
|
|
134
|
+
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
135
|
+
const retryMK = await handleModifyKeyError();
|
|
136
|
+
if (retryMK.cancel) { log.info('Submission cancelled'); return; }
|
|
137
|
+
extraParams['_modify_key'] = retryMK.modifyKey;
|
|
138
|
+
body = await buildInputBody(options.data, extraParams);
|
|
139
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
140
|
+
}
|
|
141
|
+
|
|
115
142
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
116
143
|
const retryResult = await checkSubmitErrors(result);
|
|
117
144
|
if (retryResult) {
|
package/src/commands/push.js
CHANGED
|
@@ -8,6 +8,7 @@ import { log } from '../lib/logger.js';
|
|
|
8
8
|
import { shouldSkipColumn } from '../lib/columns.js';
|
|
9
9
|
import { loadConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline } from '../lib/config.js';
|
|
10
10
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
11
|
+
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
11
12
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
12
13
|
import { findMetadataFiles } from '../lib/diff.js';
|
|
13
14
|
import { detectChangedColumns, findBaselineEntry } from '../lib/delta.js';
|
|
@@ -18,6 +19,7 @@ export const pushCommand = new Command('push')
|
|
|
18
19
|
.argument('<path>', 'File or directory to push')
|
|
19
20
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
20
21
|
.option('--ticket <id>', 'Override ticket ID')
|
|
22
|
+
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
21
23
|
.option('--meta-only', 'Only push metadata changes, skip file content')
|
|
22
24
|
.option('--content-only', 'Only push file content, skip metadata columns')
|
|
23
25
|
.option('-y, --yes', 'Auto-accept all prompts (path refactoring, etc.)')
|
|
@@ -29,15 +31,23 @@ export const pushCommand = new Command('push')
|
|
|
29
31
|
try {
|
|
30
32
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
31
33
|
|
|
34
|
+
// ModifyKey guard — check once before any submissions
|
|
35
|
+
const modifyKeyResult = await checkModifyKey(options);
|
|
36
|
+
if (modifyKeyResult.cancel) {
|
|
37
|
+
log.info('Submission cancelled');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const modifyKey = modifyKeyResult.modifyKey;
|
|
41
|
+
|
|
32
42
|
// Process pending deletions from synchronize.json
|
|
33
|
-
await processPendingDeletes(client, options);
|
|
43
|
+
await processPendingDeletes(client, options, modifyKey);
|
|
34
44
|
|
|
35
45
|
const pathStat = await stat(targetPath);
|
|
36
46
|
|
|
37
47
|
if (pathStat.isDirectory()) {
|
|
38
|
-
await pushDirectory(targetPath, client, options);
|
|
48
|
+
await pushDirectory(targetPath, client, options, modifyKey);
|
|
39
49
|
} else {
|
|
40
|
-
await pushSingleFile(targetPath, client, options);
|
|
50
|
+
await pushSingleFile(targetPath, client, options, modifyKey);
|
|
41
51
|
}
|
|
42
52
|
} catch (err) {
|
|
43
53
|
formatError(err);
|
|
@@ -48,7 +58,7 @@ export const pushCommand = new Command('push')
|
|
|
48
58
|
/**
|
|
49
59
|
* Process pending delete entries from .dbo/synchronize.json
|
|
50
60
|
*/
|
|
51
|
-
async function processPendingDeletes(client, options) {
|
|
61
|
+
async function processPendingDeletes(client, options, modifyKey = null) {
|
|
52
62
|
const sync = await loadSynchronize();
|
|
53
63
|
if (!sync.delete || sync.delete.length === 0) return;
|
|
54
64
|
|
|
@@ -62,6 +72,7 @@ async function processPendingDeletes(client, options) {
|
|
|
62
72
|
|
|
63
73
|
const extraParams = { '_confirm': options.confirm || 'true' };
|
|
64
74
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
75
|
+
if (modifyKey) extraParams['_modify_key'] = modifyKey;
|
|
65
76
|
|
|
66
77
|
const body = await buildInputBody([entry.expression], extraParams);
|
|
67
78
|
|
|
@@ -69,23 +80,33 @@ async function processPendingDeletes(client, options) {
|
|
|
69
80
|
const result = await client.postUrlEncoded('/api/input/submit', body);
|
|
70
81
|
|
|
71
82
|
// Retry with prompted params if needed
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
if (
|
|
83
|
+
const errorResult = await checkSubmitErrors(result);
|
|
84
|
+
if (errorResult) {
|
|
85
|
+
if (errorResult.skipRecord) {
|
|
75
86
|
log.warn(` Skipping deletion of "${entry.name}"`);
|
|
76
87
|
remaining.push(entry);
|
|
77
88
|
continue;
|
|
78
89
|
}
|
|
79
|
-
|
|
90
|
+
if (errorResult.skipAll) {
|
|
91
|
+
log.warn(` Skipping deletion of "${entry.name}" and all remaining`);
|
|
92
|
+
remaining.push(entry);
|
|
93
|
+
// Push all remaining entries too
|
|
94
|
+
const currentIdx = sync.delete.indexOf(entry);
|
|
95
|
+
for (let i = currentIdx + 1; i < sync.delete.length; i++) {
|
|
96
|
+
remaining.push(sync.delete[i]);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const params = errorResult.retryParams || errorResult;
|
|
80
101
|
Object.assign(extraParams, params);
|
|
81
102
|
const retryBody = await buildInputBody([entry.expression], extraParams);
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
103
|
+
const retryResponse = await client.postUrlEncoded('/api/input/submit', retryBody);
|
|
104
|
+
if (retryResponse.successful) {
|
|
84
105
|
log.success(` Deleted "${entry.name}" from server`);
|
|
85
106
|
deletedUids.push(entry.UID);
|
|
86
107
|
} else {
|
|
87
108
|
log.error(` Failed to delete "${entry.name}"`);
|
|
88
|
-
formatResponse(
|
|
109
|
+
formatResponse(retryResponse, { json: options.json, jq: options.jq });
|
|
89
110
|
remaining.push(entry);
|
|
90
111
|
}
|
|
91
112
|
} else if (result.successful) {
|
|
@@ -119,7 +140,7 @@ async function processPendingDeletes(client, options) {
|
|
|
119
140
|
/**
|
|
120
141
|
* Push a single file using its companion .metadata.json
|
|
121
142
|
*/
|
|
122
|
-
async function pushSingleFile(filePath, client, options) {
|
|
143
|
+
async function pushSingleFile(filePath, client, options, modifyKey = null) {
|
|
123
144
|
// Find the metadata file
|
|
124
145
|
const dir = dirname(filePath);
|
|
125
146
|
const base = basename(filePath, extname(filePath));
|
|
@@ -133,13 +154,13 @@ async function pushSingleFile(filePath, client, options) {
|
|
|
133
154
|
process.exit(1);
|
|
134
155
|
}
|
|
135
156
|
|
|
136
|
-
await pushFromMetadata(meta, metaPath, client, options);
|
|
157
|
+
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey);
|
|
137
158
|
}
|
|
138
159
|
|
|
139
160
|
/**
|
|
140
161
|
* Push all records found in a directory (recursive)
|
|
141
162
|
*/
|
|
142
|
-
async function pushDirectory(dirPath, client, options) {
|
|
163
|
+
async function pushDirectory(dirPath, client, options, modifyKey = null) {
|
|
143
164
|
const metaFiles = await findMetadataFiles(dirPath);
|
|
144
165
|
|
|
145
166
|
if (metaFiles.length === 0) {
|
|
@@ -251,7 +272,7 @@ async function pushDirectory(dirPath, client, options) {
|
|
|
251
272
|
|
|
252
273
|
for (const item of toPush) {
|
|
253
274
|
try {
|
|
254
|
-
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns);
|
|
275
|
+
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns, modifyKey);
|
|
255
276
|
if (success) {
|
|
256
277
|
succeeded++;
|
|
257
278
|
successfulPushes.push(item);
|
|
@@ -285,7 +306,7 @@ async function pushDirectory(dirPath, client, options) {
|
|
|
285
306
|
* @param {string[]|null} changedColumns - Optional array of changed column names (for delta sync)
|
|
286
307
|
* @returns {Promise<boolean>} - True if push succeeded
|
|
287
308
|
*/
|
|
288
|
-
async function pushFromMetadata(meta, metaPath, client, options, changedColumns = null) {
|
|
309
|
+
async function pushFromMetadata(meta, metaPath, client, options, changedColumns = null, modifyKey = null) {
|
|
289
310
|
const uid = meta.UID || meta._id;
|
|
290
311
|
const entity = meta._entity;
|
|
291
312
|
const contentCols = new Set(meta._contentColumns || []);
|
|
@@ -349,10 +370,20 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
349
370
|
|
|
350
371
|
const extraParams = { '_confirm': options.confirm };
|
|
351
372
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
373
|
+
if (modifyKey) extraParams['_modify_key'] = modifyKey;
|
|
352
374
|
|
|
353
375
|
let body = await buildInputBody(dataExprs, extraParams);
|
|
354
376
|
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
355
377
|
|
|
378
|
+
// Reactive ModifyKey retry — server rejected because key wasn't set locally
|
|
379
|
+
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
380
|
+
const retryMK = await handleModifyKeyError();
|
|
381
|
+
if (retryMK.cancel) { log.info('Submission cancelled'); return false; }
|
|
382
|
+
extraParams['_modify_key'] = retryMK.modifyKey;
|
|
383
|
+
body = await buildInputBody(dataExprs, extraParams);
|
|
384
|
+
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
385
|
+
}
|
|
386
|
+
|
|
356
387
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
357
388
|
const retryResult = await checkSubmitErrors(result);
|
|
358
389
|
if (retryResult) {
|
package/src/lib/config.js
CHANGED
|
@@ -181,9 +181,10 @@ export async function loadAppConfig() {
|
|
|
181
181
|
AppUID: config.AppUID || null,
|
|
182
182
|
AppName: config.AppName || null,
|
|
183
183
|
AppShortName: config.AppShortName || null,
|
|
184
|
+
AppModifyKey: config.AppModifyKey || null,
|
|
184
185
|
};
|
|
185
186
|
} catch {
|
|
186
|
-
return { AppID: null, AppUID: null, AppName: null, AppShortName: null };
|
|
187
|
+
return { AppID: null, AppUID: null, AppName: null, AppShortName: null, AppModifyKey: null };
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -528,6 +529,32 @@ export async function getAllPluginScopes() {
|
|
|
528
529
|
return result;
|
|
529
530
|
}
|
|
530
531
|
|
|
532
|
+
// ─── AppModifyKey ─────────────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Save AppModifyKey to .dbo/config.json.
|
|
536
|
+
* Pass null to remove the key (e.g. when server no longer has one).
|
|
537
|
+
*/
|
|
538
|
+
export async function saveAppModifyKey(modifyKey) {
|
|
539
|
+
await mkdir(dboDir(), { recursive: true });
|
|
540
|
+
let existing = {};
|
|
541
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
542
|
+
if (modifyKey != null) existing.AppModifyKey = modifyKey;
|
|
543
|
+
else delete existing.AppModifyKey;
|
|
544
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Load AppModifyKey from .dbo/config.json.
|
|
549
|
+
* Returns the key string or null if not set.
|
|
550
|
+
*/
|
|
551
|
+
export async function loadAppModifyKey() {
|
|
552
|
+
try {
|
|
553
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
554
|
+
return JSON.parse(raw).AppModifyKey || null;
|
|
555
|
+
} catch { return null; }
|
|
556
|
+
}
|
|
557
|
+
|
|
531
558
|
// ─── Gitignore ────────────────────────────────────────────────────────────
|
|
532
559
|
|
|
533
560
|
/**
|
package/src/lib/input-parser.js
CHANGED
|
@@ -116,7 +116,8 @@ export async function checkSubmitErrors(result) {
|
|
|
116
116
|
const allText = messages.filter(m => typeof m === 'string').join(' ');
|
|
117
117
|
|
|
118
118
|
// --- Ticket error detection (new interactive handling) ---
|
|
119
|
-
|
|
119
|
+
// Match ticket_error, ticket_lookup_error, and similar variants
|
|
120
|
+
const hasTicketError = allText.includes('ticket_error') || allText.includes('ticket_lookup_error');
|
|
120
121
|
const hasRepoMismatch = allText.includes('repo_mismatch');
|
|
121
122
|
|
|
122
123
|
if (hasTicketError) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { loadAppModifyKey, saveAppModifyKey } from './config.js';
|
|
2
|
+
import { log } from './logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pre-submission ModifyKey guard.
|
|
6
|
+
* - If options.modifyKey flag is set, it takes precedence — no prompt, no config check.
|
|
7
|
+
* - Returns { modifyKey: null } if no key is set in config (no-op).
|
|
8
|
+
* - Prompts user to type the key; returns { modifyKey: string } on match.
|
|
9
|
+
* - Returns { modifyKey: null, cancel: true } on mismatch or cancel.
|
|
10
|
+
*/
|
|
11
|
+
export async function checkModifyKey(options = {}) {
|
|
12
|
+
// --modify-key flag takes precedence (mirrors --ticket / --confirm behavior)
|
|
13
|
+
if (options.modifyKey) return { modifyKey: options.modifyKey, cancel: false };
|
|
14
|
+
|
|
15
|
+
const storedKey = await loadAppModifyKey();
|
|
16
|
+
if (!storedKey) return { modifyKey: null, cancel: false };
|
|
17
|
+
|
|
18
|
+
log.warn('');
|
|
19
|
+
log.warn(' ⚠ This app is locked (production mode). A ModifyKey is required to submit changes.');
|
|
20
|
+
log.warn('');
|
|
21
|
+
|
|
22
|
+
const inquirer = (await import('inquirer')).default;
|
|
23
|
+
const { action } = await inquirer.prompt([{
|
|
24
|
+
type: 'list',
|
|
25
|
+
name: 'action',
|
|
26
|
+
message: 'This app has a ModifyKey set. How would you like to proceed?',
|
|
27
|
+
choices: [
|
|
28
|
+
{ name: 'Enter the ModifyKey to proceed', value: 'enter' },
|
|
29
|
+
{ name: 'Cancel submission', value: 'cancel' },
|
|
30
|
+
],
|
|
31
|
+
}]);
|
|
32
|
+
|
|
33
|
+
if (action === 'cancel') return { modifyKey: null, cancel: true };
|
|
34
|
+
|
|
35
|
+
const { enteredKey } = await inquirer.prompt([{
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'enteredKey',
|
|
38
|
+
message: 'ModifyKey:',
|
|
39
|
+
}]);
|
|
40
|
+
|
|
41
|
+
const key = enteredKey.trim();
|
|
42
|
+
if (!key) {
|
|
43
|
+
log.error(' No ModifyKey entered. Submission cancelled.');
|
|
44
|
+
return { modifyKey: null, cancel: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { modifyKey: key, cancel: false };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detects whether a server response error message requires a ModifyKey.
|
|
52
|
+
* Server error format: "Error:The '<app>' app (UID=...) has a ModifyKey – ..."
|
|
53
|
+
*/
|
|
54
|
+
export function isModifyKeyError(responseMessage) {
|
|
55
|
+
return typeof responseMessage === 'string' && responseMessage.includes('has a ModifyKey');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reactive ModifyKey handler — called after a submission fails with a ModifyKey error.
|
|
60
|
+
* Prompts the user to enter the key, saves it to config, and returns it for retry.
|
|
61
|
+
* Returns { modifyKey: string } on success or { modifyKey: null, cancel: true } on cancel.
|
|
62
|
+
*/
|
|
63
|
+
export async function handleModifyKeyError() {
|
|
64
|
+
log.warn('');
|
|
65
|
+
log.warn(' ⚠ This app requires a ModifyKey. The key has not been set locally (try re-running `dbo clone`).');
|
|
66
|
+
log.warn('');
|
|
67
|
+
|
|
68
|
+
const inquirer = (await import('inquirer')).default;
|
|
69
|
+
const { action } = await inquirer.prompt([{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'action',
|
|
72
|
+
message: 'A ModifyKey is required. How would you like to proceed?',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: 'Enter the ModifyKey to retry', value: 'enter' },
|
|
75
|
+
{ name: 'Cancel submission', value: 'cancel' },
|
|
76
|
+
],
|
|
77
|
+
}]);
|
|
78
|
+
|
|
79
|
+
if (action === 'cancel') return { modifyKey: null, cancel: true };
|
|
80
|
+
|
|
81
|
+
const { enteredKey } = await inquirer.prompt([{
|
|
82
|
+
type: 'input',
|
|
83
|
+
name: 'enteredKey',
|
|
84
|
+
message: 'ModifyKey:',
|
|
85
|
+
}]);
|
|
86
|
+
|
|
87
|
+
const key = enteredKey.trim();
|
|
88
|
+
if (!key) {
|
|
89
|
+
log.error(' No ModifyKey entered. Submission cancelled.');
|
|
90
|
+
return { modifyKey: null, cancel: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await saveAppModifyKey(key);
|
|
94
|
+
log.info(' ModifyKey saved to .dbo/config.json for future use.');
|
|
95
|
+
return { modifyKey: key, cancel: false };
|
|
96
|
+
}
|