@dboio/cli 0.8.0 → 0.9.2

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 CHANGED
@@ -116,6 +116,39 @@ dbo content deploy albain3dwkofbhnd1qtd1q assets/css/colors.css
116
116
 
117
117
  ---
118
118
 
119
+ ## Project Directory Structure
120
+
121
+ When you run `dbo init --scaffold` or `dbo clone`, the following standard directories are created in your project root:
122
+
123
+ ```
124
+ my-project/
125
+ ├── .dbo/ # Config, synchronization and session info for dbo cli commands
126
+ ├── .claude/ # Claude Code plugin config (commands, specs, plans, skills)
127
+ ├── node_modules/ # Automatically created; contains all external libraries
128
+ ├── src/ # Your actual source code (sass, typescript, etc.)
129
+ ├── tests/ # Project-level tests
130
+ ├── bins/ # Assets (contents, outputs, images, HTML, CSS)
131
+ ├── trash/ # Staged soft-deleted files from dbo rm
132
+ ├── automation/ # Automation entity records
133
+ ├── app_version/ # App version entity records
134
+ ├── data_source/ # Data source entity records
135
+ ├── docs/ # Project documentation and docs entities
136
+ ├── extension/ # Extension entity records
137
+ │ ├── <DescriptorType>/ # Sub-directory per descriptor type (e.g., component/, form/)
138
+ │ └── _unsupported/ # Extensions with unrecognized descriptor types
139
+ ├── group/ # Group entity records
140
+ ├── integration/ # Integration entity records
141
+ ├── site/ # Site entity records
142
+ ├── .gitignore # Tells Git to ignore files in the repo sync
143
+ ├── .dboignore # Tells dbo cli to ignore files in commands
144
+ ├── package.json # Metadata, scripts, and dependency list
145
+ └── package-lock.json # Records exact versions of dependencies installed
146
+ ```
147
+
148
+ > **Breaking change from pre-0.9.1**: Directory names have changed from mixed-case (`Extensions/`, `App Versions/`, `bins/`) to lowercase snake_case (`extension/`, `app_version/`, `bins/`). If you have an existing project, rename your directories manually before running `dbo clone` again.
149
+
150
+ ---
151
+
119
152
  ## Configuration
120
153
 
121
154
  All configuration is **directory-scoped**. Each project folder maintains its own `.dbo/` directory with its own domain and session. Switch environments by switching directories:
@@ -135,6 +168,7 @@ All configuration is **directory-scoped**. Each project folder maintains its own
135
168
  | `credentials.json` | Username, user ID, UID, name, email (no password) | Gitignored (per-user) |
136
169
  | `cookies.txt` | Session cookie (Netscape format) | Gitignored (per-user) |
137
170
  | `structure.json` | Bin directory mapping (created by `dbo clone`) | Committable (shared) |
171
+ | `metadata_templates.json` | Column templates per entity/descriptor (auto-generated by `dbo clone`) | Committable (shared) |
138
172
 
139
173
  `dbo init` automatically adds `.dbo/credentials.json`, `.dbo/cookies.txt`, `.dbo/config.local.json`, and `.dbo/ticketing.local.json` to `.gitignore` (creates the file if it doesn't exist).
140
174
 
@@ -185,8 +219,26 @@ A gitignore-style file in the project root that controls which files and directo
185
219
 
186
220
  **Used by:**
187
221
  - `dbo init` — scaffold empty-check (determines if directory is "effectively empty")
188
- - `dbo add .` — directory scanning (which files/dirs to skip)
189
- - `dbo push` — metadata file discovery (which dirs to skip)
222
+ - `dbo add .` — directory scanning (which files/dirs to skip); also checked in single-file mode (`dbo add file.html`)
223
+ - `dbo push` — metadata file discovery: skips matching `.metadata.json` / `_output~*.json` files AND any record whose companion content file (`@reference`) matches an ignore pattern
224
+
225
+ **Bypass:** Use `dbo input -d '...'` to submit expressions for a file that would otherwise be ignored — `dbo input` never does file discovery so `.dboignore` does not apply.
226
+
227
+ **Pattern examples:**
228
+
229
+ ```gitignore
230
+ # Ignore all SQL companion files (output records with CustomSQL)
231
+ **/*.CustomSQL.sql
232
+
233
+ # Ignore a specific record (by metadata file path)
234
+ bins/app/my-draft-page.metadata.json
235
+
236
+ # Ignore an entire directory
237
+ bins/staging/
238
+
239
+ # Ignore all content in bins/ (still push entity-dir records like extension/)
240
+ bins/
241
+ ```
190
242
 
191
243
  **Syntax:** Same as `.gitignore` — glob patterns, `#` comments, blank lines, negation with `!`, directory-only patterns with trailing `/`.
192
244
 
@@ -250,12 +302,13 @@ dbo init --scaffold --yes # scaffold dirs non-intera
250
302
  | `--domain <host>` | DBO instance domain |
251
303
  | `--username <user>` | DBO username (stored for login default) |
252
304
  | `--force` | Overwrite existing configuration. Triggers a domain-change confirmation prompt when the new domain differs from the project reference domain |
253
- | `--app <shortName>` | App short name (triggers clone after init) |
305
+ | `--app <shortName>` | App short name (triggers clone after init). Prompts for password and authenticates automatically before fetching app data from the server |
254
306
  | `--clone` | Clone the app after initialization |
255
307
  | `-g, --global` | Install Claude commands globally (`~/.claude/commands/`) |
256
308
  | `--local` | Install Claude commands to project (`.claude/commands/`) |
257
- | `--scaffold` | Pre-create standard project directories (`App Versions`, `Automations`, `Bins`, `Data Sources`, `Documentation`, `Extensions`, `Groups`, `Integrations`, `Sites`) |
309
+ | `--scaffold` | Pre-create standard project directories (`app_version`, `automation`, `bins`, `data_source`, `docs`, `extension`, `group`, `integration`, `site`, `src`, `tests`, `trash`) |
258
310
  | `--dboignore` | Create `.dboignore` with default patterns (use with `--force` to overwrite existing) |
311
+ | `--media-placement <placement>` | Set media placement when cloning: `fullpath` or `binpath` (default: `bin`) |
259
312
  | `-y, --yes` | Skip all interactive prompts (legacy migration, Claude Code setup) |
260
313
  | `--non-interactive` | Alias for `--yes` |
261
314
 
@@ -291,7 +344,8 @@ dbo clone -e extension --descriptor-types false # Clone extensions flat (no de
291
344
  | `--app <name>` | App short name to fetch from server |
292
345
  | `-e, --entity <type>` | Only clone a specific entity type (e.g. `output`, `content`, `media`, `extension`) |
293
346
  | `--documentation-only` | When used with `-e extension`, clone only documentation extensions |
294
- | `--descriptor-types <bool>` | Sort extensions into descriptor sub-directories (default: `true`). Set to `false` to use flat `Extensions/` layout |
347
+ | `--descriptor-types <bool>` | Sort extensions into descriptor sub-directories (default: `true`). Set to `false` to use flat `extension/` layout |
348
+ | `--media-placement <placement>` | Set media placement: `fullpath` or `binpath` (default: `bin`). Skips interactive media placement prompt |
295
349
  | `--domain <host>` | Override domain. Triggers a domain-change confirmation prompt when it differs from the project reference domain |
296
350
  | `--force` | Skip source mismatch confirmation and change detection; re-processes all files |
297
351
  | `-y, --yes` | Auto-accept all prompts (also skips source mismatch confirmation) |
@@ -305,14 +359,14 @@ The project's reference domain is stored in `app.json._domain` (committed to git
305
359
 
306
360
  #### What clone does
307
361
 
308
- 1. **Loads app JSON** — from a local file, server API, or interactive prompt
362
+ 1. **Loads app JSON** — from a local file, server API, or interactive prompt. A spinner shows progress while fetching from the server (responses can be slow as the JSON is assembled on demand)
309
363
  2. **Updates `.dbo/config.json`** — saves `AppID`, `AppUID`, `AppName`, `AppShortName`, `AppModifyKey` (if the app is locked), and `cloneSource` (the source used for this clone)
310
364
  3. **Updates `package.json`** — populates `name`, `productName`, `description`, `homepage`, and `deploy` script
311
365
  4. **Creates directories** — processes `children.bin` to build the directory hierarchy based on `ParentBinID` relationships
312
366
  5. **Saves `.dbo/structure.json`** — maps BinIDs to directory paths for file placement
313
- 6. **Writes content files** — decodes base64 content, creates `*.metadata.json` + content files in the correct bin directory
367
+ 6. **Writes content files** — decodes base64 content, creates `*.metadata.json` + content files in the correct bin directory. When a record's `Extension` field is empty, prompts you to choose from `css`, `js`, `html`, `xml`, `txt`, `md`, `cs`, `json`, `sql` (or skip to keep no extension). The chosen extension is saved back into the `metadata.json` `Extension` field
314
368
  7. **Downloads media files** — fetches binary files (images, CSS, fonts) from the server via `/api/media/{uid}` and saves with metadata
315
- 8. **Processes entity-dir records** — entities matching project directories (`extension`, `app_version`, `data_source`, `site`, `group`, `integration`, `automation`) are saved as `.metadata.json` files in their corresponding directory (e.g., `Extensions/`, `Data Sources/`)
369
+ 8. **Processes entity-dir records** — entities matching project directories (`extension`, `app_version`, `data_source`, `site`, `group`, `integration`, `automation`) are saved as `.metadata.json` files in their corresponding directory (e.g., `extension/`, `data_source/`)
316
370
  9. **Processes other entities** — remaining entities with a `BinID` are placed in the corresponding bin directory
317
371
  10. **Saves `app.json`** — clone of the original JSON with processed entries replaced by `@path/to/*.metadata.json` references
318
372
 
@@ -349,7 +403,7 @@ When cloning an app that was already cloned locally, the CLI detects existing fi
349
403
  When multiple records would create files at the same path (e.g., a `content` record and a `media` record both named `colors.css`), the CLI detects the collision before writing any files and prompts you to choose which record to keep:
350
404
 
351
405
  ```
352
- ⚠ Collision: 2 records want to create "Bins/app/colors.css"
406
+ ⚠ Collision: 2 records want to create "bins/app/colors.css"
353
407
  ? Which record should create this file?
354
408
  ❯ [content] colors (UID: abc123)
355
409
  [media] colors.css (UID: def456)
@@ -387,13 +441,13 @@ Entity types that correspond to project directories (`extension`, `app_version`,
387
441
 
388
442
  | Entity Key | Directory |
389
443
  |------------|-----------|
390
- | `extension` | `Extensions/<Descriptor>/` (see below) |
391
- | `app_version` | `App Versions/` |
392
- | `data_source` | `Data Sources/` |
393
- | `site` | `Sites/` |
394
- | `group` | `Groups/` |
395
- | `integration` | `Integrations/` |
396
- | `automation` | `Automations/` |
444
+ | `extension` | `extension/<Descriptor>/` (see below) |
445
+ | `app_version` | `app_version/` |
446
+ | `data_source` | `data_source/` |
447
+ | `site` | `site/` |
448
+ | `group` | `group/` |
449
+ | `integration` | `integration/` |
450
+ | `automation` | `automation/` |
397
451
 
398
452
  For each entity type, the CLI prompts to choose which column becomes the filename (defaults to `Name`, fallback `UID`). The choice is saved per entity type in `config.json` (e.g., `ExtensionFilenameCol`).
399
453
 
@@ -403,13 +457,13 @@ Use `-y` to skip prompts (uses `Name` column, no content extraction).
403
457
 
404
458
  #### Extension descriptor sub-directories
405
459
 
406
- Extension records are organized into sub-directories under `Extensions/` based on their `Descriptor` column value. A special extension type called `descriptor_definition` (itself an extension with `Descriptor === "descriptor_definition"`) provides the mapping from descriptor key (`String1`) to display/directory name (`Name`).
460
+ Extension records are organized into sub-directories under `extension/` based on their `Descriptor` column value. A special extension type called `descriptor_definition` (itself an extension with `Descriptor === "descriptor_definition"`) provides the mapping from descriptor key (`String1`) to display/directory name (`Name`).
407
461
 
408
462
  During clone, the CLI:
409
463
 
410
464
  1. **Pre-scans** all extension records for `descriptor_definition` entries and builds a `String1 → Name` mapping
411
- 2. **Creates sub-directories** under `Extensions/` for each mapped descriptor (e.g., `Extensions/Documentation/`, `Extensions/Includes/`)
412
- 3. **Always creates** `Extensions/Unsupported/` — extensions with an unmapped or null `Descriptor` are placed here
465
+ 2. **Creates sub-directories** under `extension/` for each mapped descriptor (e.g., `extension/Documentation/`, `extension/Includes/`)
466
+ 3. **Always creates** `extension/_unsupported/` — extensions with an unmapped or null `Descriptor` are placed here
413
467
  4. **Prompts per descriptor** — filename column and content extraction prompts fire once per unique `Descriptor` value (not once for all extensions)
414
468
  5. **Persists the mapping** in `.dbo/structure.json` under `descriptorMapping`
415
469
 
@@ -423,10 +477,10 @@ During clone, the CLI:
423
477
 
424
478
  **Documentation alternate placement**: When extracting MD content from `documentation` descriptor extensions, the CLI offers a choice:
425
479
 
426
- - **Root placement** (`/Documentation/<filename>.md`) — recommended for easy access; creates `@/Documentation/<filename>.md` references in metadata
427
- - **Inline placement** (`Extensions/Documentation/<filename>.md`) — keeps files alongside metadata
480
+ - **Root placement** (`/docs/<filename>.md`) — recommended for easy access; creates `@/docs/<filename>.md` references in metadata
481
+ - **Inline placement** (`extension/Documentation/<filename>.md`) — keeps files alongside metadata
428
482
 
429
- Root-placed files use absolute-from-root `@/` references (e.g., `@/Documentation/my-doc.md`) so `dbo push` can locate them regardless of where the metadata lives.
483
+ Root-placed files use absolute-from-root `@/` references (e.g., `@/docs/my-doc.md`) so `dbo push` can locate them regardless of where the metadata lives.
430
484
 
431
485
  **Entity filter support**:
432
486
 
@@ -437,7 +491,7 @@ dbo clone -e extension # Clone all extensions (all
437
491
  dbo clone -e extension --descriptor-types false # Flat layout (no descriptor sorting)
438
492
  ```
439
493
 
440
- **`dbo add` auto-inference**: Running `dbo add Documentation/my-doc.md` when `ExtensionDocumentationMDPlacement` is `"root"` auto-creates a companion `.metadata.json` in `Extensions/Documentation/` with the correct `@/` reference.
494
+ **`dbo add` auto-inference**: Running `dbo add docs/my-doc.md` when `ExtensionDocumentationMDPlacement` is `"root"` auto-creates a companion `.metadata.json` in `extension/documentation/` with the correct `@/` reference.
441
495
 
442
496
  #### Output structure
443
497
 
@@ -449,7 +503,7 @@ project/
449
503
  .gitignore # credentials.json + cookies.txt added
450
504
  package.json # Updated with app info + deploy script
451
505
  app.json # Clone of original with @references
452
- Bins/ # ← root for all bin-placed files
506
+ bins/ # ← root for all bin-placed files
453
507
  app/ # ← directory from children.bin
454
508
  thomas-scratch.md
455
509
  thomas-scratch.metadata.json
@@ -457,67 +511,93 @@ project/
457
511
  CurrentTicketID.metadata.json
458
512
  ticket_test/ # ← another bin directory
459
513
  ...
460
- Extensions/ # ← extension records organized by descriptor
514
+ extension/ # ← extension records organized by descriptor
461
515
  Documentation/ # ← descriptor sub-directory (from descriptor_definition)
462
516
  MyDoc.uid1.metadata.json
463
517
  MyDoc.uid1.String10.md # ← extracted content column
464
518
  Includes/
465
519
  Header.uid2.metadata.json
466
- Unsupported/ # ← always created (unmapped/null descriptors)
467
- Documentation/ # ← root-placed documentation MD files (optional)
520
+ _unsupported/ # ← always created (unmapped/null descriptors)
521
+ docs/ # ← root-placed documentation MD files (optional)
468
522
  MyDoc.md
469
- Data Sources/
523
+ data_source/
470
524
  MySQL-Primary.metadata.json
471
- Sites/
525
+ site/
472
526
  MainSite.metadata.json
473
527
  media/operator/app/... # ← FullPath placement (when MediaPlacement=fullpath)
474
528
  ```
475
529
 
476
- #### Understanding the `Bins/` directory structure
530
+ #### Understanding the `bins/` directory structure
477
531
 
478
- The `Bins/` directory is the default location for all bin-placed content files during clone. It organizes files according to your app's bin hierarchy from DBO.io.
532
+ The `bins/` directory is the default location for all bin-placed content files during clone. It organizes files according to your app's bin hierarchy from DBO.io.
479
533
 
480
534
  **Directory Organization:**
481
535
 
482
- - **`Bins/`** — Root directory for all bin-placed files (local organizational directory only)
483
- - **`Bins/app/`** — Special subdirectory for the main app bin (typically the default bin)
484
- - **`Bins/custom_name/`** — Custom bin directories (e.g., `tpl/`, `ticket_test/`, etc.)
536
+ - **`bins/`** — Root directory for all bin-placed files (local organizational directory only)
537
+ - **`bins/app/`** — Special subdirectory for the main app bin (typically the default bin)
538
+ - **`bins/custom_name/`** — Custom bin directories (e.g., `tpl/`, `ticket_test/`, etc.)
485
539
 
486
- **Important: The `Bins/app/` special case**
540
+ **Important: The `bins/app/` special case**
487
541
 
488
- The `app/` subdirectory under `Bins/` is treated specially:
542
+ The `app/` subdirectory under `bins/` is treated specially:
489
543
 
490
- 1. **It's organizational only** — The `Bins/app/` prefix exists only for local file organization and is **not part of the server-side path**.
544
+ 1. **It's organizational only** — The `bins/app/` prefix exists only for local file organization and is **not part of the server-side path**.
491
545
 
492
- 2. **Path normalization** — When comparing paths (during `dbo push`), the CLI automatically strips both `Bins/` and `app/` from paths:
546
+ 2. **Path normalization** — When comparing paths (during `dbo push`), the CLI automatically strips both `bins/` and `app/` from paths:
493
547
  ```
494
- Local file: Bins/app/assets/css/operator.css
548
+ Local file: bins/app/assets/css/operator.css
495
549
  Server Path: assets/css/operator.css
496
550
  → These are considered the same path ✓
497
551
  ```
498
552
 
499
- 3. **Custom bins are preserved** — Other subdirectories like `Bins/tpl/` or `Bins/ticket_test/` represent actual bin hierarchies and their names are meaningful:
553
+ 3. **Custom bins are preserved** — Other subdirectories like `bins/tpl/` or `bins/ticket_test/` represent actual bin hierarchies and their names are meaningful:
500
554
  ```
501
- Local file: Bins/tpl/header.html
555
+ Local file: bins/tpl/header.html
502
556
  Server Path: tpl/header.html
503
557
  → The 'tpl/' directory is preserved ✓
504
558
  ```
505
559
 
506
560
  **Why this matters:**
507
561
 
508
- - When you `dbo push` files from `Bins/app/`, the CLI knows these paths should match the root-level paths in your metadata
509
- - If your metadata `Path` column contains `assets/css/colors.css`, it will correctly match files in `Bins/app/assets/css/colors.css`
510
- - Custom bin directories like `Bins/tpl/` serve from the `tpl/` directive and maintain their path structure
562
+ - When you `dbo push` files from `bins/app/`, the CLI knows these paths should match the root-level paths in your metadata
563
+ - If your metadata `Path` column contains `assets/css/colors.css`, it will correctly match files in `bins/app/assets/css/colors.css`
564
+ - Custom bin directories like `bins/tpl/` serve from the `tpl/` directive and maintain their path structure
511
565
 
512
566
  **Leading slash handling:**
513
567
 
514
568
  The CLI handles leading `/` in metadata paths flexibly:
515
- - `Path: assets/css/file.css` matches `Bins/app/assets/css/file.css` ✓
516
- - `Path: /assets/css/file.css` also matches `Bins/app/assets/css/file.css` ✓
517
- - `Path: /assets/css/file.css` matches `Bins/assets/css/file.css` ✓
569
+ - `Path: assets/css/file.css` matches `bins/app/assets/css/file.css` ✓
570
+ - `Path: /assets/css/file.css` also matches `bins/app/assets/css/file.css` ✓
571
+ - `Path: /assets/css/file.css` matches `bins/assets/css/file.css` ✓
518
572
 
519
573
  This ensures compatibility with various path formats from the server while maintaining correct local file organization.
520
574
 
575
+ #### Metadata Templates
576
+
577
+ During `dbo clone`, the CLI auto-generates `.dbo/metadata_templates.json` — a file that records which columns each entity/descriptor uses. This file is seeded from the first cloned record of each type and can be manually edited afterwards.
578
+
579
+ **File format:**
580
+
581
+ ```json
582
+ {
583
+ "site": ["AppID", "Name", "ShortName", "Active"],
584
+ "extension": {
585
+ "documentation": ["AppID", "Name", "Descriptor=documentation", "String10=@reference"],
586
+ "control": ["AppID", "Name", "ShortName", "Active"]
587
+ }
588
+ }
589
+ ```
590
+
591
+ **Column syntax:**
592
+
593
+ | Syntax | Meaning |
594
+ |--------|---------|
595
+ | `Name` | Plain column — populated from filename or left empty |
596
+ | `Descriptor=documentation` | Literal value — always set to `documentation` |
597
+ | `String10=@reference` | Content reference — links to the file being added |
598
+
599
+ `dbo add` uses these templates to auto-detect the entity type from a file's directory placement and generate metadata without prompting. For example, `dbo add extension/include/nav.html` resolves to entity `extension` / descriptor `include` and applies the matching template.
600
+
521
601
  ---
522
602
 
523
603
  ### `dbo login`
@@ -729,8 +809,20 @@ The save-to-disk feature fetches data and persists records as local files. It us
729
809
  4. **Extension** — use an entity column or specify manually
730
810
 
731
811
  Each record produces:
732
- - A content file: `[filename].[extension]`
733
- - A metadata file: `[filename].metadata.json` (all column values)
812
+ - A content file: `[filename]~[uid].[extension]` (e.g., `colors~abc123.css`)
813
+ - A metadata file: `[filename]~[uid].metadata.json` (all column values)
814
+
815
+ ### Tilde UID Convention
816
+
817
+ Every local file whose server record has a known UID embeds that UID in the filename, separated by a tilde (`~`):
818
+
819
+ - Content: `<basename>~<uid>.<ext>` / `<basename>~<uid>.metadata.json`
820
+ - Media: `<basename>~<uid>.<ext>` / `<basename>~<uid>.<ext>.metadata.json`
821
+ - Entity-dir: `<name>~<uid>.metadata.json`
822
+
823
+ Exception: if the chosen filename column value *is* the UID itself, the tilde suffix is omitted: `<uid>.<ext>`.
824
+
825
+ When pushing, the `~<uid>` portion is automatically stripped from filenames sent to the server (e.g., `logo~def456.png` is uploaded as `logo.png`).
734
826
 
735
827
  If the path column contains a filename (e.g., `assets/js/main.js`), the CLI detects it and asks if you want to use it.
736
828
 
@@ -1015,6 +1107,8 @@ After accepting changes, file modification times are synced to the server's `_La
1015
1107
 
1016
1108
  Push local files back to DBO.io using metadata from a previous pull. This is the counterpart to `dbo content pull` and `dbo output --save`.
1017
1109
 
1110
+ Files and records matching `.dboignore` patterns are skipped — both by metadata file path (e.g. `*.metadata.json`) and by companion content file path (e.g. a record whose `@Content` points to an ignored `.sql` file). To push an ignored file directly, use `dbo input -d '...'` with an explicit expression.
1111
+
1018
1112
  #### Round-trip workflow
1019
1113
 
1020
1114
  ```bash
@@ -1113,29 +1207,29 @@ Remove a file or directory locally and stage server deletions for the next `dbo
1113
1207
 
1114
1208
  ```bash
1115
1209
  # Remove a content file (finds companion .metadata.json automatically)
1116
- dbo rm Bins/app/some-file.txt
1210
+ dbo rm bins/app/some-file.txt
1117
1211
 
1118
1212
  # Remove by metadata file directly
1119
- dbo rm Bins/app/some-file.metadata.json
1213
+ dbo rm bins/app/some-file.metadata.json
1120
1214
 
1121
1215
  # Skip confirmation prompt
1122
- dbo rm -f Bins/app/some-file.txt
1216
+ dbo rm -f bins/app/some-file.txt
1123
1217
 
1124
1218
  # Stage server deletion without deleting local files
1125
- dbo rm --keep-local Bins/app/some-file.txt
1219
+ dbo rm --keep-local bins/app/some-file.txt
1126
1220
  ```
1127
1221
 
1128
1222
  #### Directory
1129
1223
 
1130
1224
  ```bash
1131
1225
  # Remove a directory and all its contents (prompts for approach)
1132
- dbo rm Bins/app/ui/
1226
+ dbo rm bins/app/ui/
1133
1227
 
1134
1228
  # Remove directory without prompts
1135
- dbo rm -f Bins/app/ui/
1229
+ dbo rm -f bins/app/ui/
1136
1230
 
1137
1231
  # Stage deletions without removing local files/directories
1138
- dbo rm --keep-local Bins/app/ui/
1232
+ dbo rm --keep-local bins/app/ui/
1139
1233
  ```
1140
1234
 
1141
1235
  When removing a directory, the CLI:
@@ -1150,6 +1244,7 @@ When removing a directory, the CLI:
1150
1244
  | `<path>` | File, `.metadata.json`, or directory to remove |
1151
1245
  | `-f, --force` | Skip all confirmation prompts |
1152
1246
  | `--keep-local` | Only stage server deletions, keep local files/directories |
1247
+ | `--hard` | Immediately delete local files (no `Trash/` move; legacy behavior) |
1153
1248
 
1154
1249
  #### What rm does (single file)
1155
1250
 
@@ -1158,7 +1253,10 @@ When removing a directory, the CLI:
1158
1253
  3. **Prompts for confirmation** — "Do you really want to remove this file and all of its nodes?"
1159
1254
  4. **Stages deletion** — adds a delete entry to `.dbo/synchronize.json`
1160
1255
  5. **Updates app.json** — removes the `@path/to/file.metadata.json` reference from children arrays
1161
- 6. **Deletes local files** — removes the metadata file and all referenced content/media files (unless `--keep-local`)
1256
+ 6. **Soft-deletes local files** — renames files with `__WILL_DELETE__` prefix (unless `--keep-local` or `--hard`)
1257
+ 7. **After `dbo push`** — successfully deleted records have their `__WILL_DELETE__` files moved to `Trash/`
1258
+
1259
+ Use `--hard` to immediately delete files without the `Trash/` safety net.
1162
1260
 
1163
1261
  #### synchronize.json
1164
1262
 
@@ -1188,6 +1286,8 @@ The next `dbo push` processes all pending deletions before pushing file changes.
1188
1286
 
1189
1287
  Add a new file to DBO.io by creating a server record. Similar to `git add`, this registers a local file with the server.
1190
1288
 
1289
+ Files matching `.dboignore` patterns are skipped — both in directory-scan mode (`dbo add .`) and single-file mode (`dbo add file.html`). Use `dbo input` to create a record for an ignored file directly.
1290
+
1191
1291
  #### Single file
1192
1292
 
1193
1293
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dboio/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.2",
4
4
  "description": "CLI for the DBO.io framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,15 @@
1
1
  import { Command } from 'commander';
2
- import { readFile, readdir, stat, writeFile, mkdir } from 'fs/promises';
2
+ import { readFile, readdir, stat, writeFile, mkdir, rename } from 'fs/promises';
3
3
  import { join, dirname, basename, extname, relative } from 'path';
4
4
  import { DboClient } from '../lib/client.js';
5
5
  import { buildInputBody, checkSubmitErrors } from '../lib/input-parser.js';
6
6
  import { formatResponse, formatError } from '../lib/formatter.js';
7
7
  import { log } from '../lib/logger.js';
8
8
  import { shouldSkipColumn } from '../lib/columns.js';
9
- import { loadAppConfig, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference } from '../lib/config.js';
9
+ import { loadAppConfig, loadAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference, loadConfig } from '../lib/config.js';
10
+ import { resolveDirective, resolveTemplateCols, assembleMetadata, promptReferenceColumn, setTemplateCols, saveMetadataTemplates, loadMetadataTemplates } from '../lib/metadata-templates.js';
11
+ import { buildUidFilename, hasUidInFilename } from '../lib/filenames.js';
12
+ import { setFileTimestamps } from '../lib/timestamps.js';
10
13
  import { checkStoredTicket, clearGlobalTicket } from '../lib/ticketing.js';
11
14
  import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
12
15
  import { loadIgnore } from '../lib/ignore.js';
@@ -67,12 +70,20 @@ async function detectDocumentationFile(filePath) {
67
70
  if (placement !== 'root') return null;
68
71
 
69
72
  const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
70
- if (!rel.startsWith('Documentation/')) return null;
73
+ if (!rel.startsWith('docs/')) return null;
71
74
 
72
75
  return { entity: 'extension', descriptor: 'documentation' };
73
76
  }
74
77
 
75
78
  async function addSingleFile(filePath, client, options, batchDefaults) {
79
+ // Check .dboignore before doing any processing
80
+ const ig = await loadIgnore();
81
+ const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
82
+ if (ig.ignores(relPath)) {
83
+ log.dim(`Skipped (dboignored): ${relPath}`);
84
+ return null;
85
+ }
86
+
76
87
  const dir = dirname(filePath);
77
88
  const ext = extname(filePath);
78
89
  const base = basename(filePath, ext);
@@ -102,12 +113,58 @@ async function addSingleFile(filePath, client, options, batchDefaults) {
102
113
  return await submitAdd(meta, metaPath, filePath, client, options);
103
114
  }
104
115
 
105
- // Step 3b: Auto-infer entity/descriptor for Documentation/ files when root placement is configured
116
+ // Step 3b: Directive / template path
117
+ const directive = resolveDirective(filePath);
118
+ if (directive) {
119
+ const { entity, descriptor } = directive;
120
+ const appConfig = await loadAppConfig();
121
+ const appJson = await loadAppJsonBaseline().catch(() => null);
122
+ const resolved = await resolveTemplateCols(entity, descriptor, appConfig, appJson);
123
+
124
+ if (resolved !== null) {
125
+ let { cols, templates } = resolved;
126
+
127
+ // Check if @reference col is known
128
+ const hasRefMarker = cols.some(c => c.includes('=@reference'));
129
+ if (!hasRefMarker) {
130
+ if (options.yes) {
131
+ console.warn(`Warning: No @reference column in template for ${entity}${descriptor ? '.' + descriptor : ''} — skipping content column. Update .dbo/metadata_templates.json to add Key=@reference.`);
132
+ } else {
133
+ const chosen = await promptReferenceColumn(cols, entity, descriptor);
134
+ if (chosen) {
135
+ cols = cols.map(c => c === chosen ? `${chosen}=@reference` : c);
136
+ templates = templates ?? await loadMetadataTemplates() ?? {};
137
+ setTemplateCols(templates, entity, descriptor, cols);
138
+ await saveMetadataTemplates(templates);
139
+ }
140
+ }
141
+ }
142
+
143
+ const { meta } = assembleMetadata(cols, filePath, entity, descriptor, appConfig);
144
+
145
+ // Determine metadata path
146
+ let templateMetaPath;
147
+ const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
148
+ if (rel.startsWith('docs/')) {
149
+ // docs/ files: companion metadata goes to extension/documentation/
150
+ const docDir = join(process.cwd(), 'extension', 'documentation');
151
+ await mkdir(docDir, { recursive: true });
152
+ templateMetaPath = join(docDir, `${basename(filePath, extname(filePath))}.metadata.json`);
153
+ } else {
154
+ templateMetaPath = join(dirname(filePath), `${basename(filePath, extname(filePath))}.metadata.json`);
155
+ }
156
+
157
+ await writeFile(templateMetaPath, JSON.stringify(meta, null, 2) + '\n');
158
+ return await submitAdd(meta, templateMetaPath, filePath, client, options);
159
+ }
160
+ }
161
+
162
+ // Step 3c: Auto-infer entity/descriptor for Documentation/ files when root placement is configured
106
163
  const docInfo = await detectDocumentationFile(filePath);
107
164
  if (docInfo) {
108
165
  const filenameCol = await loadDescriptorFilenamePreference('documentation') || 'Name';
109
166
  const docBase = basename(filePath, extname(filePath));
110
- const companionDir = join(process.cwd(), 'Extensions', 'Documentation');
167
+ const companionDir = join(process.cwd(), 'extension', 'documentation');
111
168
  await mkdir(companionDir, { recursive: true });
112
169
 
113
170
  const docMeta = {
@@ -329,20 +386,75 @@ async function submitAdd(meta, metaPath, filePath, client, options) {
329
386
  result = await client.postUrlEncoded('/api/input/submit', body);
330
387
  }
331
388
 
332
- formatResponse(result, { json: options.json, jq: options.jq });
389
+ formatResponse(result, { json: options.json, jq: options.jq, verbose: options.verbose });
333
390
 
334
391
  if (!result.successful) {
335
392
  throw new Error('Add failed');
336
393
  }
337
394
 
338
- // Extract UID from response and update metadata
395
+ // Extract UID from response and rename files to ~uid convention
339
396
  const addResults = result.payload?.Results?.Add || result.data?.Payload?.Results?.Add || [];
340
397
  if (addResults.length > 0) {
341
398
  const returnedUID = addResults[0].UID;
399
+ const returnedLastUpdated = addResults[0]._LastUpdated;
400
+
342
401
  if (returnedUID) {
343
- meta.UID = returnedUID;
344
- await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
345
- log.success(`UID assigned: ${returnedUID}`);
402
+ // Check if UID is already embedded in the filename (idempotency guard)
403
+ const currentMetaBase = basename(metaPath);
404
+ if (hasUidInFilename(currentMetaBase, returnedUID)) {
405
+ meta.UID = returnedUID;
406
+ await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
407
+ log.success(`UID already in filename: ${returnedUID}`);
408
+ } else {
409
+ // Compute new names
410
+ const metaDir = dirname(metaPath);
411
+ const currentExt = extname(filePath).substring(1);
412
+ const currentBase = basename(filePath, extname(filePath));
413
+ const newBase = buildUidFilename(currentBase, returnedUID);
414
+ const newFilename = currentExt ? `${newBase}.${currentExt}` : newBase;
415
+ const newFilePath = join(metaDir, newFilename);
416
+ const newMetaPath = join(metaDir, `${newBase}.metadata.json`);
417
+
418
+ // Rename content file
419
+ try {
420
+ await rename(filePath, newFilePath);
421
+ log.success(`Renamed: ${basename(filePath)} → ${newFilename}`);
422
+ } catch (err) {
423
+ log.warn(` Could not rename content file: ${err.message}`);
424
+ }
425
+
426
+ // Update @reference in metadata
427
+ meta.UID = returnedUID;
428
+ for (const col of (meta._contentColumns || [])) {
429
+ const ref = meta[col];
430
+ if (ref && String(ref).startsWith('@')) {
431
+ const oldRefFile = String(ref).substring(1);
432
+ if (oldRefFile === basename(filePath)) {
433
+ meta[col] = `@${newFilename}`;
434
+ }
435
+ }
436
+ }
437
+
438
+ // Write and rename metadata file
439
+ await writeFile(newMetaPath, JSON.stringify(meta, null, 2) + '\n');
440
+ if (metaPath !== newMetaPath) {
441
+ try { await rename(metaPath, newMetaPath); } catch { /* already written */ }
442
+ }
443
+ log.dim(` Renamed metadata: ${basename(metaPath)} → ${basename(newMetaPath)}`);
444
+
445
+ // Restore timestamps from server _LastUpdated
446
+ const config = await loadConfig();
447
+ const serverTz = config.ServerTimezone;
448
+ if (serverTz && returnedLastUpdated) {
449
+ try {
450
+ await setFileTimestamps(newFilePath, returnedLastUpdated, returnedLastUpdated, serverTz);
451
+ await setFileTimestamps(newMetaPath, returnedLastUpdated, returnedLastUpdated, serverTz);
452
+ } catch { /* non-critical */ }
453
+ }
454
+
455
+ log.success(`UID assigned: ${returnedUID}`);
456
+ log.dim(` Files renamed to ~${returnedUID} convention`);
457
+ }
346
458
  log.dim(` Run "dbo pull -e ${entity} ${returnedUID}" to populate all columns`);
347
459
  }
348
460
  }