@dboio/cli 0.6.12 → 0.6.14
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 +25 -4
- package/package.json +1 -1
- package/src/commands/add.js +1 -0
- package/src/commands/clone.js +37 -3
- package/src/commands/content.js +6 -2
- package/src/commands/deploy.js +9 -3
- package/src/commands/init.js +66 -3
- package/src/commands/input.js +1 -0
- package/src/commands/push.js +45 -17
- package/src/commands/status.js +4 -2
- package/src/lib/config.js +25 -0
- package/src/lib/domain-guard.js +95 -0
- package/src/lib/scaffold.js +62 -0
- package/src/lib/transaction-key.js +46 -0
package/README.md
CHANGED
|
@@ -157,6 +157,7 @@ All configuration is **directory-scoped**. Each project folder maintains its own
|
|
|
157
157
|
| `AppName` | string | App display name |
|
|
158
158
|
| `AppShortName` | string | App short name (used for `dbo clone --app`) |
|
|
159
159
|
| `AppModifyKey` | string | ModifyKey for locked/production apps (set by `dbo clone`, used for submission guards) |
|
|
160
|
+
| `TransactionKeyPreset` | `RowUID` \| `RowID` | Row key type for auto-assembled expressions (set during `dbo init`/`dbo clone`, default `RowUID`) |
|
|
160
161
|
| `ContentPlacement` | `bin` \| `path` \| `ask` | Where to place content files during clone |
|
|
161
162
|
| `MediaPlacement` | `bin` \| `fullpath` \| `ask` | Where to place media files during clone |
|
|
162
163
|
| `<Entity>FilenameCol` | column name | Filename column for entity-dir records (e.g., `ExtensionFilenameCol`) |
|
|
@@ -168,6 +169,10 @@ All configuration is **directory-scoped**. Each project folder maintains its own
|
|
|
168
169
|
|
|
169
170
|
These are set interactively on first clone and saved for future use. Pre-set them in `config.json` to skip the prompt entirely.
|
|
170
171
|
|
|
172
|
+
#### `app.json._domain`
|
|
173
|
+
|
|
174
|
+
The `_domain` field in `app.json` stores the project's reference domain (set during `dbo clone`). This is committed to git and used for domain-change detection when running `dbo init --force` or `dbo clone --domain`. It provides a stable cross-user baseline — all collaborators share the same reference domain.
|
|
175
|
+
|
|
171
176
|
### Legacy migration
|
|
172
177
|
|
|
173
178
|
If your project uses the older `.domain`, `.username`, `.password`, `.cookies` files, `dbo init` will detect them and offer to migrate automatically.
|
|
@@ -195,17 +200,20 @@ dbo init --domain my-domain.com --username me@co.io # with credentials
|
|
|
195
200
|
dbo init --force # overwrite existing config
|
|
196
201
|
dbo init --domain my-domain.com --app myapp --clone # init + clone an app
|
|
197
202
|
dbo init --domain my-domain.com -y # skip all prompts
|
|
203
|
+
dbo init --scaffold # scaffold dirs (prompts for domain)
|
|
204
|
+
dbo init --scaffold --yes # scaffold dirs non-interactively
|
|
198
205
|
```
|
|
199
206
|
|
|
200
207
|
| Flag | Description |
|
|
201
208
|
|------|-------------|
|
|
202
209
|
| `--domain <host>` | DBO instance domain |
|
|
203
210
|
| `--username <user>` | DBO username (stored for login default) |
|
|
204
|
-
| `--force` | Overwrite existing configuration |
|
|
211
|
+
| `--force` | Overwrite existing configuration. Triggers a domain-change confirmation prompt when the new domain differs from the project reference domain |
|
|
205
212
|
| `--app <shortName>` | App short name (triggers clone after init) |
|
|
206
213
|
| `--clone` | Clone the app after initialization |
|
|
207
214
|
| `-g, --global` | Install Claude commands globally (`~/.claude/commands/`) |
|
|
208
215
|
| `--local` | Install Claude commands to project (`.claude/commands/`) |
|
|
216
|
+
| `--scaffold` | Pre-create standard project directories (`App Versions`, `Automations`, `Bins`, `Data Sources`, `Documentation`, `Extensions`, `Groups`, `Integrations`, `Sites`) |
|
|
209
217
|
| `-y, --yes` | Skip all interactive prompts (legacy migration, Claude Code setup) |
|
|
210
218
|
| `--non-interactive` | Alias for `--yes` |
|
|
211
219
|
|
|
@@ -233,10 +241,16 @@ dbo init --domain my-domain.com --app myapp --clone
|
|
|
233
241
|
|------|-------------|
|
|
234
242
|
| `<source>` | Local JSON file path (optional positional argument) |
|
|
235
243
|
| `--app <name>` | App short name to fetch from server |
|
|
236
|
-
| `--domain <host>` | Override domain |
|
|
244
|
+
| `--domain <host>` | Override domain. Triggers a domain-change confirmation prompt when it differs from the project reference domain |
|
|
237
245
|
| `-y, --yes` | Auto-accept all prompts |
|
|
238
246
|
| `-v, --verbose` | Show HTTP request details |
|
|
239
247
|
|
|
248
|
+
#### Domain change detection
|
|
249
|
+
|
|
250
|
+
When cloning with a different domain than the project's reference domain (`app.json._domain` or `config.json.domain`), the CLI warns before proceeding. When `TransactionKeyPreset=RowID`, this escalates to a critical error because numeric IDs are not unique across domains — pushing to the wrong domain can corrupt records. In non-interactive mode (`-y`), RowUID domain changes proceed with a warning, but RowID domain changes throw a hard error.
|
|
251
|
+
|
|
252
|
+
The project's reference domain is stored in `app.json._domain` (committed to git) during clone, giving the CLI a stable cross-user baseline.
|
|
253
|
+
|
|
240
254
|
#### What clone does
|
|
241
255
|
|
|
242
256
|
1. **Loads app JSON** — from a local file, server API, or interactive prompt
|
|
@@ -437,7 +451,6 @@ Output:
|
|
|
437
451
|
Domain: my-domain.com
|
|
438
452
|
Username: user@example.com
|
|
439
453
|
User ID: 10296
|
|
440
|
-
User UID: albain3dwkofbhnd1qtd1q
|
|
441
454
|
Directory: /Users/me/projects/operator
|
|
442
455
|
Session: Active (expires: 2026-03-15T10:30:00.000Z)
|
|
443
456
|
Cookies: /Users/me/projects/operator/.dbo/cookies.txt
|
|
@@ -446,7 +459,7 @@ Output:
|
|
|
446
459
|
dbo: ✓ global (~/.claude/commands/dbo.md)
|
|
447
460
|
```
|
|
448
461
|
|
|
449
|
-
User ID
|
|
462
|
+
User ID is populated by `dbo login`. If it shows "(not set)", run `dbo login` to fetch it.
|
|
450
463
|
|
|
451
464
|
Plugin scopes (project or global) are displayed when plugins have been installed. Scopes are stored in `.dbo/config.local.json`.
|
|
452
465
|
|
|
@@ -489,6 +502,7 @@ dbo input -d 'RowUID:abc;column:content.Content@file.css' -v
|
|
|
489
502
|
| `-C, --confirm <true\|false>` | Commit changes (default: `true`). Use `false` for validation only |
|
|
490
503
|
| `--ticket <id>` | Override ticket ID (`_OverrideTicketID`) |
|
|
491
504
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
505
|
+
| `--row-key <type>` | Row key type (`RowUID` or `RowID`) — no-op for `-d` passthrough, available for consistency |
|
|
492
506
|
| `--login` | Auto-login user created by this submission |
|
|
493
507
|
| `--transactional` | Use transactional processing |
|
|
494
508
|
| `--json` | Output raw JSON response |
|
|
@@ -634,6 +648,7 @@ dbo input -d 'RowUID:albain3dwkofbhnd1qtd1q;column:content.Content@assets/css/co
|
|
|
634
648
|
| `--ticket <id>` | Override ticket ID |
|
|
635
649
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
636
650
|
| `--multipart` | Use multipart/form-data upload (for binary files) |
|
|
651
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
637
652
|
|
|
638
653
|
#### `dbo content pull`
|
|
639
654
|
|
|
@@ -945,6 +960,7 @@ The `@colors.css` reference tells push to read the content from that file. All o
|
|
|
945
960
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
946
961
|
| `--ticket <id>` | Override ticket ID |
|
|
947
962
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
963
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
948
964
|
| `--meta-only` | Only push metadata, skip file content |
|
|
949
965
|
| `--content-only` | Only push file content, skip metadata |
|
|
950
966
|
| `-y, --yes` | Auto-accept all prompts |
|
|
@@ -1108,6 +1124,7 @@ The `@colors.css` reference tells the CLI to read the file content from `colors.
|
|
|
1108
1124
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1109
1125
|
| `--ticket <id>` | Override ticket ID |
|
|
1110
1126
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1127
|
+
| `--row-key <type>` | Row key type (`RowUID` or `RowID`) — `add` always uses `RowID:add1` for new records regardless |
|
|
1111
1128
|
| `-y, --yes` | Auto-accept all prompts |
|
|
1112
1129
|
| `--json` | Output raw JSON |
|
|
1113
1130
|
| `--jq <expr>` | Filter JSON response |
|
|
@@ -1328,6 +1345,9 @@ dbo deploy js:app --ticket abc123
|
|
|
1328
1345
|
|
|
1329
1346
|
# Validate without deploying
|
|
1330
1347
|
dbo deploy css:colors --confirm false
|
|
1348
|
+
|
|
1349
|
+
# Deploy a multipart (binary) entry from manifest
|
|
1350
|
+
dbo deploy img:logo
|
|
1331
1351
|
```
|
|
1332
1352
|
|
|
1333
1353
|
| Flag | Description |
|
|
@@ -1337,6 +1357,7 @@ dbo deploy css:colors --confirm false
|
|
|
1337
1357
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1338
1358
|
| `--ticket <id>` | Override ticket ID |
|
|
1339
1359
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1360
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
1340
1361
|
| `--json` | Output raw JSON |
|
|
1341
1362
|
| `-v, --verbose` | Show HTTP request details |
|
|
1342
1363
|
| `--domain <host>` | Override domain |
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -19,6 +19,7 @@ export const addCommand = new Command('add')
|
|
|
19
19
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
20
20
|
.option('--ticket <id>', 'Override ticket ID')
|
|
21
21
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
22
|
+
.option('--row-key <type>', 'Row key type (RowUID or RowID) — add uses RowID:add1 for new records regardless')
|
|
22
23
|
.option('-y, --yes', 'Auto-accept all prompts')
|
|
23
24
|
.option('--json', 'Output raw JSON')
|
|
24
25
|
.option('--jq <expr>', 'Filter JSON response')
|
package/src/commands/clone.js
CHANGED
|
@@ -2,11 +2,12 @@ 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, saveAppModifyKey } from '../lib/config.js';
|
|
5
|
+
import { loadConfig, updateConfigWithApp, loadClonePlacement, saveClonePlacement, ensureGitignore, saveEntityDirPreference, loadEntityDirPreference, saveEntityContentExtractions, loadEntityContentExtractions, saveAppJsonBaseline, addDeleteEntry, loadCollisionResolutions, saveCollisionResolutions, loadSynchronize, saveAppModifyKey, loadTransactionKeyPreset, saveTransactionKeyPreset } 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';
|
|
9
9
|
import { getLocalSyncTime, isServerNewer, hasLocalModifications, promptChangeDetection, inlineDiffAndMerge } from '../lib/diff.js';
|
|
10
|
+
import { checkDomainChange } from '../lib/domain-guard.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Resolve a column value that may be base64-encoded.
|
|
@@ -437,6 +438,7 @@ export const cloneCommand = new Command('clone')
|
|
|
437
438
|
*/
|
|
438
439
|
export async function performClone(source, options = {}) {
|
|
439
440
|
const config = await loadConfig();
|
|
441
|
+
const effectiveDomain = options.domain || config.domain;
|
|
440
442
|
let appJson;
|
|
441
443
|
|
|
442
444
|
// Step 1: Load the app JSON
|
|
@@ -487,6 +489,15 @@ export async function performClone(source, options = {}) {
|
|
|
487
489
|
throw new Error('Invalid app JSON: missing UID or children');
|
|
488
490
|
}
|
|
489
491
|
|
|
492
|
+
// Domain change detection
|
|
493
|
+
if (effectiveDomain) {
|
|
494
|
+
const { changed, proceed } = await checkDomainChange(effectiveDomain, options);
|
|
495
|
+
if (changed && !proceed) {
|
|
496
|
+
log.info('Clone aborted: domain change denied.');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
490
501
|
log.success(`Cloning "${appJson.Name}" (${appJson.ShortName})`);
|
|
491
502
|
|
|
492
503
|
// Ensure sensitive files are gitignored
|
|
@@ -511,6 +522,28 @@ export async function performClone(source, options = {}) {
|
|
|
511
522
|
log.warn('');
|
|
512
523
|
}
|
|
513
524
|
|
|
525
|
+
// Prompt for TransactionKeyPreset if not already set
|
|
526
|
+
const existingPreset = await loadTransactionKeyPreset();
|
|
527
|
+
if (!existingPreset) {
|
|
528
|
+
if (options.yes || !process.stdin.isTTY) {
|
|
529
|
+
await saveTransactionKeyPreset('RowUID');
|
|
530
|
+
log.dim(' TransactionKeyPreset: RowUID (default)');
|
|
531
|
+
} else {
|
|
532
|
+
const inquirer = (await import('inquirer')).default;
|
|
533
|
+
const { preset } = await inquirer.prompt([{
|
|
534
|
+
type: 'list',
|
|
535
|
+
name: 'preset',
|
|
536
|
+
message: 'Which row key should the CLI use when building input expressions?',
|
|
537
|
+
choices: [
|
|
538
|
+
{ name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
|
|
539
|
+
{ name: 'RowID (numeric IDs)', value: 'RowID' },
|
|
540
|
+
],
|
|
541
|
+
}]);
|
|
542
|
+
await saveTransactionKeyPreset(preset);
|
|
543
|
+
log.dim(` TransactionKeyPreset: ${preset}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
514
547
|
// Step 3: Update package.json
|
|
515
548
|
await updatePackageJson(appJson, config);
|
|
516
549
|
|
|
@@ -593,7 +626,7 @@ export async function performClone(source, options = {}) {
|
|
|
593
626
|
}
|
|
594
627
|
|
|
595
628
|
// Step 7: Save app.json with references
|
|
596
|
-
await saveAppJson(appJson, contentRefs, otherRefs);
|
|
629
|
+
await saveAppJson(appJson, contentRefs, otherRefs, effectiveDomain);
|
|
597
630
|
|
|
598
631
|
// Step 8: Create .app.json baseline for delta tracking
|
|
599
632
|
await saveBaselineFile(appJson);
|
|
@@ -1670,8 +1703,9 @@ export function guessExtensionForColumn(columnName) {
|
|
|
1670
1703
|
/**
|
|
1671
1704
|
* Save app.json to project root with @ references replacing processed entries.
|
|
1672
1705
|
*/
|
|
1673
|
-
async function saveAppJson(appJson, contentRefs, otherRefs) {
|
|
1706
|
+
async function saveAppJson(appJson, contentRefs, otherRefs, domain) {
|
|
1674
1707
|
const output = { ...appJson };
|
|
1708
|
+
if (domain) output._domain = domain;
|
|
1675
1709
|
output.children = { ...appJson.children };
|
|
1676
1710
|
|
|
1677
1711
|
// Replace content array with references
|
package/src/commands/content.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
9
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
|
|
10
|
+
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
10
11
|
import { log } from '../lib/logger.js';
|
|
11
12
|
|
|
12
13
|
function collect(value, previous) {
|
|
@@ -32,6 +33,7 @@ const deployCmd = new Command('deploy')
|
|
|
32
33
|
.option('--ticket <id>', 'Override ticket ID')
|
|
33
34
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
34
35
|
.option('--multipart', 'Use multipart/form-data upload (for binary files)')
|
|
36
|
+
.option('--row-key <type>', 'Override row key type for this invocation (RowUID or RowID)')
|
|
35
37
|
.option('--json', 'Output raw JSON')
|
|
36
38
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
37
39
|
.option('--domain <host>', 'Override domain')
|
|
@@ -63,12 +65,14 @@ const deployCmd = new Command('deploy')
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
const transactionKey = await resolveTransactionKey(options);
|
|
66
69
|
const extraParams = { '_confirm': options.confirm };
|
|
67
70
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
68
71
|
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
69
72
|
|
|
70
73
|
const isMultipart = options.multipart === true;
|
|
71
|
-
const
|
|
74
|
+
const rowKeyExpr = transactionKey === 'RowID' ? `RowID:${uid}` : `RowUID:${uid}`;
|
|
75
|
+
const dataExprs = isMultipart ? [] : [`${rowKeyExpr};column:content.Content@${filepath}`];
|
|
72
76
|
|
|
73
77
|
// Apply stored ticket if no --ticket flag
|
|
74
78
|
if (!options.ticket) {
|
|
@@ -90,7 +94,7 @@ const deployCmd = new Command('deploy')
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
const files = [{
|
|
93
|
-
fieldName:
|
|
97
|
+
fieldName: `${rowKeyExpr};column:content.Content`,
|
|
94
98
|
filePath: filepath,
|
|
95
99
|
fileName: filepath.split('/').pop(),
|
|
96
100
|
}];
|
package/src/commands/deploy.js
CHANGED
|
@@ -5,6 +5,7 @@ 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
7
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
|
|
8
|
+
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
8
9
|
import { log } from '../lib/logger.js';
|
|
9
10
|
|
|
10
11
|
const MANIFEST_FILE = 'dbo.deploy.json';
|
|
@@ -16,6 +17,7 @@ export const deployCommand = new Command('deploy')
|
|
|
16
17
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
17
18
|
.option('--ticket <id>', 'Override ticket ID')
|
|
18
19
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
20
|
+
.option('--row-key <type>', 'Override row key type for this invocation (RowUID or RowID)')
|
|
19
21
|
.option('--json', 'Output raw JSON')
|
|
20
22
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
21
23
|
.option('--domain <host>', 'Override domain')
|
|
@@ -54,6 +56,9 @@ export const deployCommand = new Command('deploy')
|
|
|
54
56
|
process.exit(1);
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// Resolve transaction key preset once before the entry loop
|
|
60
|
+
const transactionKey = await resolveTransactionKey(options);
|
|
61
|
+
|
|
57
62
|
// ModifyKey guard — check once before any submissions
|
|
58
63
|
const modifyKeyResult = await checkModifyKey(options);
|
|
59
64
|
if (modifyKeyResult.cancel) {
|
|
@@ -103,9 +108,10 @@ export const deployCommand = new Command('deploy')
|
|
|
103
108
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
104
109
|
if (activeModifyKey) extraParams['_modify_key'] = activeModifyKey;
|
|
105
110
|
|
|
111
|
+
const rowKeyExpr = transactionKey === 'RowID' ? `RowID:${uid}` : `RowUID:${uid}`;
|
|
106
112
|
const dataExprs = isMultipart
|
|
107
|
-
? [
|
|
108
|
-
: [
|
|
113
|
+
? [`${rowKeyExpr};column:${entity}.Filename=${filename}`]
|
|
114
|
+
: [`${rowKeyExpr};column:${entity}.${column}@${file}`];
|
|
109
115
|
|
|
110
116
|
// Apply stored ticket if no --ticket flag
|
|
111
117
|
if (!options.ticket) {
|
|
@@ -127,7 +133,7 @@ export const deployCommand = new Command('deploy')
|
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
const files = [{
|
|
130
|
-
fieldName:
|
|
136
|
+
fieldName: `${rowKeyExpr};column:${entity}.${column}`,
|
|
131
137
|
filePath: file,
|
|
132
138
|
fileName: file.split('/').pop(),
|
|
133
139
|
}];
|
package/src/commands/init.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { access } from 'fs/promises';
|
|
2
|
+
import { access, readdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore } from '../lib/config.js';
|
|
4
|
+
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset } from '../lib/config.js';
|
|
5
5
|
import { installOrUpdateClaudeCommands } from './install.js';
|
|
6
|
+
import { scaffoldProjectDirs, logScaffoldResult } from '../lib/scaffold.js';
|
|
6
7
|
import { log } from '../lib/logger.js';
|
|
8
|
+
import { checkDomainChange, writeAppJsonDomain } from '../lib/domain-guard.js';
|
|
7
9
|
|
|
8
10
|
export const initCommand = new Command('init')
|
|
9
11
|
.description('Initialize DBO CLI configuration for the current directory')
|
|
@@ -16,11 +18,17 @@ export const initCommand = new Command('init')
|
|
|
16
18
|
.option('--non-interactive', 'Skip all interactive prompts')
|
|
17
19
|
.option('-g, --global', 'Install Claude commands to user home directory (~/.claude/commands/)')
|
|
18
20
|
.option('--local', 'Install Claude commands to project directory (.claude/commands/)')
|
|
21
|
+
.option('--scaffold', 'Create standard project directories (App Versions, Automations, Bins, …)')
|
|
19
22
|
.action(async (options) => {
|
|
20
23
|
// Merge --yes into nonInteractive
|
|
21
24
|
if (options.yes) options.nonInteractive = true;
|
|
22
25
|
try {
|
|
23
26
|
if (await isInitialized() && !options.force) {
|
|
27
|
+
if (options.scaffold) {
|
|
28
|
+
const result = await scaffoldProjectDirs();
|
|
29
|
+
logScaffoldResult(result);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
24
32
|
log.warn('Already initialized. Use --force to overwrite.');
|
|
25
33
|
return;
|
|
26
34
|
}
|
|
@@ -49,24 +57,79 @@ export const initCommand = new Command('init')
|
|
|
49
57
|
if (!domain || !username) {
|
|
50
58
|
const inquirer = (await import('inquirer')).default;
|
|
51
59
|
const answers = await inquirer.prompt([
|
|
52
|
-
{ type: 'input', name: 'domain', message: 'Domain (e.g., myapp.dbo.io):', default: domain || undefined, when: !domain
|
|
60
|
+
{ type: 'input', name: 'domain', message: 'Domain (e.g., myapp.dbo.io):', default: domain || undefined, when: !domain },
|
|
53
61
|
{ type: 'input', name: 'username', message: 'Username (email):', when: !username },
|
|
54
62
|
]);
|
|
55
63
|
domain = domain || answers.domain;
|
|
56
64
|
username = username || answers.username;
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
let domainChanged = false;
|
|
68
|
+
if (options.force) {
|
|
69
|
+
const { changed, proceed } = await checkDomainChange(domain, options);
|
|
70
|
+
if (changed && !proceed) {
|
|
71
|
+
log.info('Init aborted: domain change denied.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
domainChanged = changed;
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
await initConfig(domain);
|
|
60
78
|
if (username) {
|
|
61
79
|
await saveCredentials(username);
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
if (options.force && domainChanged) {
|
|
83
|
+
await writeAppJsonDomain(domain);
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
// Ensure sensitive files are gitignored
|
|
65
87
|
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json']);
|
|
66
88
|
|
|
67
89
|
log.success(`Initialized .dbo/ for ${domain}`);
|
|
68
90
|
log.dim(' Run "dbo login" to authenticate.');
|
|
69
91
|
|
|
92
|
+
// Prompt for TransactionKeyPreset
|
|
93
|
+
if (!options.nonInteractive) {
|
|
94
|
+
const inquirer = (await import('inquirer')).default;
|
|
95
|
+
const { preset } = await inquirer.prompt([{
|
|
96
|
+
type: 'list',
|
|
97
|
+
name: 'preset',
|
|
98
|
+
message: 'Which row key should the CLI use when building input expressions?',
|
|
99
|
+
choices: [
|
|
100
|
+
{ name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
|
|
101
|
+
{ name: 'RowID (numeric IDs)', value: 'RowID' },
|
|
102
|
+
],
|
|
103
|
+
}]);
|
|
104
|
+
await saveTransactionKeyPreset(preset);
|
|
105
|
+
log.dim(` TransactionKeyPreset: ${preset}`);
|
|
106
|
+
} else {
|
|
107
|
+
await saveTransactionKeyPreset('RowUID');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Scaffold ─────────────────────────────────────────────────────────────
|
|
111
|
+
let shouldScaffold = options.scaffold;
|
|
112
|
+
|
|
113
|
+
if (!shouldScaffold && !options.nonInteractive) {
|
|
114
|
+
const entries = await readdir(process.cwd());
|
|
115
|
+
const IGNORED = new Set(['.dbo', '.claude', '.idea', '.vscode']);
|
|
116
|
+
const isEmpty = entries.every(e => IGNORED.has(e));
|
|
117
|
+
|
|
118
|
+
const inquirer = (await import('inquirer')).default;
|
|
119
|
+
const { doScaffold } = await inquirer.prompt([{
|
|
120
|
+
type: 'confirm',
|
|
121
|
+
name: 'doScaffold',
|
|
122
|
+
message: 'Would you like to scaffold the standard project directory structure?',
|
|
123
|
+
default: isEmpty,
|
|
124
|
+
}]);
|
|
125
|
+
shouldScaffold = doScaffold;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (shouldScaffold) {
|
|
129
|
+
const result = await scaffoldProjectDirs();
|
|
130
|
+
logScaffoldResult(result);
|
|
131
|
+
}
|
|
132
|
+
|
|
70
133
|
// Clone if requested
|
|
71
134
|
if (options.clone || options.app) {
|
|
72
135
|
let appShortName = options.app;
|
package/src/commands/input.js
CHANGED
|
@@ -18,6 +18,7 @@ export const inputCommand = new Command('input')
|
|
|
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
20
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
21
|
+
.option('--row-key <type>', 'Row key type (RowUID or RowID) — no-op for -d passthrough, available for consistency')
|
|
21
22
|
.option('--login', 'Auto-login user created by this submission')
|
|
22
23
|
.option('--transactional', 'Use transactional processing')
|
|
23
24
|
.option('--json', 'Output raw JSON response')
|
package/src/commands/push.js
CHANGED
|
@@ -9,6 +9,7 @@ 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
11
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
12
|
+
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
12
13
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
13
14
|
import { findMetadataFiles } from '../lib/diff.js';
|
|
14
15
|
import { detectChangedColumns, findBaselineEntry } from '../lib/delta.js';
|
|
@@ -23,6 +24,7 @@ export const pushCommand = new Command('push')
|
|
|
23
24
|
.option('--meta-only', 'Only push metadata changes, skip file content')
|
|
24
25
|
.option('--content-only', 'Only push file content, skip metadata columns')
|
|
25
26
|
.option('-y, --yes', 'Auto-accept all prompts (path refactoring, etc.)')
|
|
27
|
+
.option('--row-key <type>', 'Override row key type for this invocation (RowUID or RowID)')
|
|
26
28
|
.option('--json', 'Output raw JSON')
|
|
27
29
|
.option('--jq <expr>', 'Filter JSON response')
|
|
28
30
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
@@ -39,15 +41,18 @@ export const pushCommand = new Command('push')
|
|
|
39
41
|
}
|
|
40
42
|
const modifyKey = modifyKeyResult.modifyKey;
|
|
41
43
|
|
|
44
|
+
// Resolve transaction key preset
|
|
45
|
+
const transactionKey = await resolveTransactionKey(options);
|
|
46
|
+
|
|
42
47
|
// Process pending deletions from synchronize.json
|
|
43
|
-
await processPendingDeletes(client, options, modifyKey);
|
|
48
|
+
await processPendingDeletes(client, options, modifyKey, transactionKey);
|
|
44
49
|
|
|
45
50
|
const pathStat = await stat(targetPath);
|
|
46
51
|
|
|
47
52
|
if (pathStat.isDirectory()) {
|
|
48
|
-
await pushDirectory(targetPath, client, options, modifyKey);
|
|
53
|
+
await pushDirectory(targetPath, client, options, modifyKey, transactionKey);
|
|
49
54
|
} else {
|
|
50
|
-
await pushSingleFile(targetPath, client, options, modifyKey);
|
|
55
|
+
await pushSingleFile(targetPath, client, options, modifyKey, transactionKey);
|
|
51
56
|
}
|
|
52
57
|
} catch (err) {
|
|
53
58
|
formatError(err);
|
|
@@ -58,7 +63,7 @@ export const pushCommand = new Command('push')
|
|
|
58
63
|
/**
|
|
59
64
|
* Process pending delete entries from .dbo/synchronize.json
|
|
60
65
|
*/
|
|
61
|
-
async function processPendingDeletes(client, options, modifyKey = null) {
|
|
66
|
+
async function processPendingDeletes(client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
62
67
|
const sync = await loadSynchronize();
|
|
63
68
|
if (!sync.delete || sync.delete.length === 0) return;
|
|
64
69
|
|
|
@@ -140,7 +145,7 @@ async function processPendingDeletes(client, options, modifyKey = null) {
|
|
|
140
145
|
/**
|
|
141
146
|
* Push a single file using its companion .metadata.json
|
|
142
147
|
*/
|
|
143
|
-
async function pushSingleFile(filePath, client, options, modifyKey = null) {
|
|
148
|
+
async function pushSingleFile(filePath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
144
149
|
// Find the metadata file
|
|
145
150
|
const dir = dirname(filePath);
|
|
146
151
|
const base = basename(filePath, extname(filePath));
|
|
@@ -154,13 +159,13 @@ async function pushSingleFile(filePath, client, options, modifyKey = null) {
|
|
|
154
159
|
process.exit(1);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey);
|
|
162
|
+
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
/**
|
|
161
166
|
* Push all records found in a directory (recursive)
|
|
162
167
|
*/
|
|
163
|
-
async function pushDirectory(dirPath, client, options, modifyKey = null) {
|
|
168
|
+
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
164
169
|
const metaFiles = await findMetadataFiles(dirPath);
|
|
165
170
|
|
|
166
171
|
if (metaFiles.length === 0) {
|
|
@@ -272,7 +277,7 @@ async function pushDirectory(dirPath, client, options, modifyKey = null) {
|
|
|
272
277
|
|
|
273
278
|
for (const item of toPush) {
|
|
274
279
|
try {
|
|
275
|
-
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns, modifyKey);
|
|
280
|
+
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns, modifyKey, transactionKey);
|
|
276
281
|
if (success) {
|
|
277
282
|
succeeded++;
|
|
278
283
|
successfulPushes.push(item);
|
|
@@ -306,14 +311,37 @@ async function pushDirectory(dirPath, client, options, modifyKey = null) {
|
|
|
306
311
|
* @param {string[]|null} changedColumns - Optional array of changed column names (for delta sync)
|
|
307
312
|
* @returns {Promise<boolean>} - True if push succeeded
|
|
308
313
|
*/
|
|
309
|
-
async function pushFromMetadata(meta, metaPath, client, options, changedColumns = null, modifyKey = null) {
|
|
310
|
-
const uid = meta.UID
|
|
314
|
+
async function pushFromMetadata(meta, metaPath, client, options, changedColumns = null, modifyKey = null, transactionKey = 'RowUID') {
|
|
315
|
+
const uid = meta.UID;
|
|
316
|
+
const id = meta._id;
|
|
311
317
|
const entity = meta._entity;
|
|
312
318
|
const contentCols = new Set(meta._contentColumns || []);
|
|
313
319
|
const metaDir = dirname(metaPath);
|
|
314
320
|
|
|
315
|
-
|
|
316
|
-
|
|
321
|
+
// Determine the row key prefix and value based on the preset
|
|
322
|
+
let rowKeyPrefix, rowKeyValue;
|
|
323
|
+
if (transactionKey === 'RowID') {
|
|
324
|
+
if (id) {
|
|
325
|
+
rowKeyPrefix = 'RowID';
|
|
326
|
+
rowKeyValue = id;
|
|
327
|
+
} else {
|
|
328
|
+
log.warn(` ⚠ Preset is RowID but no _id found in ${basename(metaPath)} — falling back to RowUID`);
|
|
329
|
+
rowKeyPrefix = 'RowUID';
|
|
330
|
+
rowKeyValue = uid;
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
if (uid) {
|
|
334
|
+
rowKeyPrefix = 'RowUID';
|
|
335
|
+
rowKeyValue = uid;
|
|
336
|
+
} else if (id) {
|
|
337
|
+
log.warn(` ⚠ Preset is RowUID but no UID found in ${basename(metaPath)} — falling back to RowID`);
|
|
338
|
+
rowKeyPrefix = 'RowID';
|
|
339
|
+
rowKeyValue = id;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!rowKeyValue) {
|
|
344
|
+
throw new Error(`No UID or _id found in ${metaPath}`);
|
|
317
345
|
}
|
|
318
346
|
if (!entity) {
|
|
319
347
|
throw new Error(`No _entity found in ${metaPath}`);
|
|
@@ -351,9 +379,9 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
351
379
|
// @filename reference — resolve to actual file path
|
|
352
380
|
const refFile = strValue.substring(1);
|
|
353
381
|
const refPath = join(metaDir, refFile);
|
|
354
|
-
dataExprs.push(
|
|
382
|
+
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${entity}.${key}@${refPath}`);
|
|
355
383
|
} else {
|
|
356
|
-
dataExprs.push(
|
|
384
|
+
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${entity}.${key}=${strValue}`);
|
|
357
385
|
}
|
|
358
386
|
}
|
|
359
387
|
|
|
@@ -363,10 +391,10 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
363
391
|
}
|
|
364
392
|
|
|
365
393
|
const fieldLabel = changedColumns ? `${dataExprs.length} changed field(s)` : `${dataExprs.length} field(s)`;
|
|
366
|
-
log.info(`Pushing ${basename(metaPath, '.metadata.json')} (${entity}:${
|
|
394
|
+
log.info(`Pushing ${basename(metaPath, '.metadata.json')} (${entity}:${rowKeyValue}) — ${fieldLabel}`);
|
|
367
395
|
|
|
368
396
|
// Apply stored ticket if no --ticket flag
|
|
369
|
-
await applyStoredTicketToSubmission(dataExprs, entity, uid, uid, options);
|
|
397
|
+
await applyStoredTicketToSubmission(dataExprs, entity, uid || id, uid || id, options);
|
|
370
398
|
|
|
371
399
|
const extraParams = { '_confirm': options.confirm };
|
|
372
400
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
@@ -422,7 +450,7 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
422
450
|
}
|
|
423
451
|
|
|
424
452
|
// Clean up per-record ticket on success
|
|
425
|
-
await clearRecordTicket(uid);
|
|
453
|
+
await clearRecordTicket(uid || id);
|
|
426
454
|
|
|
427
455
|
// Update file timestamps from server response
|
|
428
456
|
try {
|
package/src/commands/status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { loadConfig, isInitialized, getActiveCookiesPath, loadUserInfo, getAllPluginScopes } from '../lib/config.js';
|
|
2
|
+
import { loadConfig, isInitialized, getActiveCookiesPath, loadUserInfo, getAllPluginScopes, loadTransactionKeyPreset } from '../lib/config.js';
|
|
3
3
|
import { loadCookies } from '../lib/cookie-jar.js';
|
|
4
4
|
import { access } from 'fs/promises';
|
|
5
5
|
import { join } from 'path';
|
|
@@ -18,7 +18,6 @@ export const statusCommand = new Command('status')
|
|
|
18
18
|
log.label('Username', config.username || '(not set)');
|
|
19
19
|
const userInfo = await loadUserInfo();
|
|
20
20
|
log.label('User ID', userInfo.userId || '(not set)');
|
|
21
|
-
log.label('User UID', userInfo.userUid || '(not set — run "dbo login")');
|
|
22
21
|
log.label('Directory', process.cwd());
|
|
23
22
|
|
|
24
23
|
const cookiesPath = await getActiveCookiesPath();
|
|
@@ -38,6 +37,9 @@ export const statusCommand = new Command('status')
|
|
|
38
37
|
log.label('Session', 'No active session. Run "dbo login".');
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
const transactionKeyPreset = await loadTransactionKeyPreset();
|
|
41
|
+
log.label('Transaction Key', transactionKeyPreset || '(not set — defaults to RowUID)');
|
|
42
|
+
|
|
41
43
|
// Display plugin status
|
|
42
44
|
const scopes = await getAllPluginScopes();
|
|
43
45
|
const pluginNames = Object.keys(scopes);
|
package/src/lib/config.js
CHANGED
|
@@ -555,6 +555,31 @@ export async function loadAppModifyKey() {
|
|
|
555
555
|
} catch { return null; }
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
// ─── TransactionKeyPreset ─────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Save TransactionKeyPreset to .dbo/config.json.
|
|
562
|
+
* @param {'RowUID'|'RowID'} preset
|
|
563
|
+
*/
|
|
564
|
+
export async function saveTransactionKeyPreset(preset) {
|
|
565
|
+
await mkdir(dboDir(), { recursive: true });
|
|
566
|
+
let existing = {};
|
|
567
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
568
|
+
existing.TransactionKeyPreset = preset;
|
|
569
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Load TransactionKeyPreset from .dbo/config.json.
|
|
574
|
+
* Returns 'RowUID', 'RowID', or null if not set.
|
|
575
|
+
*/
|
|
576
|
+
export async function loadTransactionKeyPreset() {
|
|
577
|
+
try {
|
|
578
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
579
|
+
return JSON.parse(raw).TransactionKeyPreset || null;
|
|
580
|
+
} catch { return null; }
|
|
581
|
+
}
|
|
582
|
+
|
|
558
583
|
// ─── Gitignore ────────────────────────────────────────────────────────────
|
|
559
584
|
|
|
560
585
|
/**
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFile, writeFile, stat, utimes } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { loadConfig, loadTransactionKeyPreset } from './config.js';
|
|
4
|
+
import { log } from './logger.js';
|
|
5
|
+
|
|
6
|
+
const appJsonPath = () => join(process.cwd(), 'app.json');
|
|
7
|
+
|
|
8
|
+
/** Read _domain from app.json. Returns null if file missing or field absent. */
|
|
9
|
+
export async function readAppJsonDomain() {
|
|
10
|
+
try {
|
|
11
|
+
const obj = JSON.parse(await readFile(appJsonPath(), 'utf8'));
|
|
12
|
+
return obj._domain || null;
|
|
13
|
+
} catch { return null; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write _domain to app.json, preserving mtime so git doesn't see a spurious change.
|
|
18
|
+
* No-op if app.json does not exist.
|
|
19
|
+
*/
|
|
20
|
+
export async function writeAppJsonDomain(domain) {
|
|
21
|
+
const path = appJsonPath();
|
|
22
|
+
let originalStat;
|
|
23
|
+
try { originalStat = await stat(path); } catch { return; }
|
|
24
|
+
const obj = JSON.parse(await readFile(path, 'utf8'));
|
|
25
|
+
obj._domain = domain;
|
|
26
|
+
await writeFile(path, JSON.stringify(obj, null, 2) + '\n');
|
|
27
|
+
await utimes(path, originalStat.atime, originalStat.mtime); // restore mtime
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check whether newDomain differs from the project reference domain.
|
|
32
|
+
* Reference = app.json._domain (authoritative) → fallback: config.json.domain.
|
|
33
|
+
* If reference is absent entirely, returns { changed: false } (no warning).
|
|
34
|
+
*
|
|
35
|
+
* Returns { changed: false } or { changed: true, proceed: bool }.
|
|
36
|
+
* Throws in non-interactive mode when TransactionKeyPreset=RowID and domain changes.
|
|
37
|
+
*/
|
|
38
|
+
export async function checkDomainChange(newDomain, options = {}) {
|
|
39
|
+
let referenceDomain = await readAppJsonDomain();
|
|
40
|
+
if (!referenceDomain) {
|
|
41
|
+
const config = await loadConfig();
|
|
42
|
+
referenceDomain = config.domain || null;
|
|
43
|
+
}
|
|
44
|
+
if (!referenceDomain) return { changed: false };
|
|
45
|
+
|
|
46
|
+
const norm = d => d ? String(d).replace(/^https?:\/\//, '').replace(/\/$/, '').toLowerCase() : null;
|
|
47
|
+
if (norm(newDomain) === norm(referenceDomain)) return { changed: false };
|
|
48
|
+
|
|
49
|
+
const preset = await loadTransactionKeyPreset();
|
|
50
|
+
const isRowID = preset === 'RowID';
|
|
51
|
+
|
|
52
|
+
if (options.yes || !process.stdin.isTTY) {
|
|
53
|
+
if (isRowID) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Domain change detected: "${referenceDomain}" → "${newDomain}"\n` +
|
|
56
|
+
`Cannot proceed in non-interactive mode with TransactionKeyPreset=RowID. ` +
|
|
57
|
+
`Numeric IDs are not unique across domains — submitting to the wrong domain may overwrite or corrupt records.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
log.warn(`Domain change detected: "${referenceDomain}" → "${newDomain}". Proceeding (TransactionKeyPreset=RowUID).`);
|
|
61
|
+
return { changed: true, proceed: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const inquirer = (await import('inquirer')).default;
|
|
65
|
+
log.plain('');
|
|
66
|
+
|
|
67
|
+
if (isRowID) {
|
|
68
|
+
log.error('⛔ CRITICAL: Domain change detected');
|
|
69
|
+
log.warn(` Reference domain : ${referenceDomain}`);
|
|
70
|
+
log.warn(` New domain : ${newDomain}`);
|
|
71
|
+
log.plain('');
|
|
72
|
+
log.warn(' TransactionKeyPreset=RowID is active. Numeric IDs are NOT unique across');
|
|
73
|
+
log.warn(' domains. Pushing to the wrong domain may overwrite or corrupt records in');
|
|
74
|
+
log.warn(' a different app.');
|
|
75
|
+
log.plain('');
|
|
76
|
+
const { confirmText } = await inquirer.prompt([{
|
|
77
|
+
type: 'input',
|
|
78
|
+
name: 'confirmText',
|
|
79
|
+
message: `Type "yes, change domain" to confirm switching to "${newDomain}":`,
|
|
80
|
+
}]);
|
|
81
|
+
return { changed: true, proceed: confirmText.trim() === 'yes, change domain' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log.warn('⚠ Domain change detected');
|
|
85
|
+
log.dim(` Reference domain : ${referenceDomain}`);
|
|
86
|
+
log.dim(` New domain : ${newDomain}`);
|
|
87
|
+
log.plain('');
|
|
88
|
+
const { confirm } = await inquirer.prompt([{
|
|
89
|
+
type: 'confirm',
|
|
90
|
+
name: 'confirm',
|
|
91
|
+
message: `Switch domain from "${referenceDomain}" to "${newDomain}"?`,
|
|
92
|
+
default: false,
|
|
93
|
+
}]);
|
|
94
|
+
return { changed: true, proceed: confirm };
|
|
95
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mkdir, stat, writeFile, access } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { DEFAULT_PROJECT_DIRS } from './structure.js';
|
|
4
|
+
import { log } from './logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scaffold the standard DBO project directory structure in cwd.
|
|
8
|
+
* Creates missing directories, skips existing ones, warns on name conflicts.
|
|
9
|
+
* Also creates app.json with {} if absent.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} [cwd=process.cwd()]
|
|
12
|
+
* @returns {Promise<{ created: string[], skipped: string[], warned: string[] }>}
|
|
13
|
+
*/
|
|
14
|
+
export async function scaffoldProjectDirs(cwd = process.cwd()) {
|
|
15
|
+
const created = [];
|
|
16
|
+
const skipped = [];
|
|
17
|
+
const warned = [];
|
|
18
|
+
|
|
19
|
+
for (const dir of DEFAULT_PROJECT_DIRS) {
|
|
20
|
+
const target = join(cwd, dir);
|
|
21
|
+
try {
|
|
22
|
+
const s = await stat(target);
|
|
23
|
+
if (s.isDirectory()) {
|
|
24
|
+
skipped.push(dir);
|
|
25
|
+
} else {
|
|
26
|
+
log.warn(` Skipping "${dir}" — a file with that name already exists`);
|
|
27
|
+
warned.push(dir);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Does not exist — create it
|
|
31
|
+
await mkdir(target, { recursive: true });
|
|
32
|
+
created.push(dir);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create app.json if absent
|
|
37
|
+
const appJsonPath = join(cwd, 'app.json');
|
|
38
|
+
try {
|
|
39
|
+
await access(appJsonPath);
|
|
40
|
+
} catch {
|
|
41
|
+
await writeFile(appJsonPath, '{}\n');
|
|
42
|
+
created.push('app.json');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { created, skipped, warned };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Print the scaffold summary to the console.
|
|
50
|
+
* @param {{ created: string[], skipped: string[], warned: string[] }} result
|
|
51
|
+
*/
|
|
52
|
+
export function logScaffoldResult({ created, skipped, warned }) {
|
|
53
|
+
if (created.length) {
|
|
54
|
+
log.success('Scaffolded directories:');
|
|
55
|
+
for (const d of created) log.dim(` + ${d}`);
|
|
56
|
+
}
|
|
57
|
+
if (skipped.length) {
|
|
58
|
+
log.dim('Skipped (already exist):');
|
|
59
|
+
for (const d of skipped) log.dim(` · ${d}`);
|
|
60
|
+
}
|
|
61
|
+
// warned items already printed inline during scaffold
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { loadTransactionKeyPreset, saveTransactionKeyPreset } from './config.js';
|
|
2
|
+
import { log } from './logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the active transaction key preset.
|
|
6
|
+
* Priority: --row-key flag > config > lazy prompt > default RowUID.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options - Command options (may have .rowKey)
|
|
9
|
+
* @returns {Promise<'RowUID'|'RowID'>}
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveTransactionKey(options = {}) {
|
|
12
|
+
// 1. --row-key flag takes precedence (no config write)
|
|
13
|
+
if (options.rowKey) return options.rowKey;
|
|
14
|
+
|
|
15
|
+
// 2. Check config
|
|
16
|
+
const stored = await loadTransactionKeyPreset();
|
|
17
|
+
if (stored) return stored;
|
|
18
|
+
|
|
19
|
+
// 3. Non-interactive or -y: default to RowUID, save to config
|
|
20
|
+
if (options.yes || options.nonInteractive || !process.stdin.isTTY) {
|
|
21
|
+
await saveTransactionKeyPreset('RowUID');
|
|
22
|
+
return 'RowUID';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 4. Lazy prompt (first submission without preset configured)
|
|
26
|
+
log.plain('');
|
|
27
|
+
log.info('TransactionKeyPreset is not configured.');
|
|
28
|
+
log.dim(' RowUID — stable across domains (recommended)');
|
|
29
|
+
log.dim(' RowID — uses numeric IDs (may differ across domains)');
|
|
30
|
+
log.plain('');
|
|
31
|
+
|
|
32
|
+
const inquirer = (await import('inquirer')).default;
|
|
33
|
+
const { preset } = await inquirer.prompt([{
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'preset',
|
|
36
|
+
message: 'Which row key should the CLI use when building input expressions?',
|
|
37
|
+
choices: [
|
|
38
|
+
{ name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
|
|
39
|
+
{ name: 'RowID (numeric IDs)', value: 'RowID' },
|
|
40
|
+
],
|
|
41
|
+
}]);
|
|
42
|
+
|
|
43
|
+
await saveTransactionKeyPreset(preset);
|
|
44
|
+
log.dim(` Saved TransactionKeyPreset: ${preset} to .dbo/config.json`);
|
|
45
|
+
return preset;
|
|
46
|
+
}
|