@dboio/cli 0.19.7 → 0.20.3
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 +18 -12
- package/bin/dbo.js +5 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +39 -12
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +18 -12
- package/plugins/claude/dbo/docs/dual-platform-maintenance.md +135 -0
- package/plugins/claude/dbo/skills/cookbook/SKILL.md +13 -3
- package/plugins/claude/dbo/skills/white-paper/SKILL.md +49 -8
- package/plugins/claude/dbo/skills/white-paper/references/api-reference.md +1 -1
- package/plugins/claude/track/.claude-plugin/plugin.json +1 -1
- package/src/commands/adopt.js +22 -19
- package/src/commands/clone.js +412 -57
- package/src/commands/init.js +2 -2
- package/src/commands/input.js +2 -2
- package/src/commands/login.js +3 -3
- package/src/commands/push.js +142 -43
- package/src/commands/status.js +15 -7
- package/src/lib/config.js +117 -11
- package/src/lib/dependencies.js +11 -9
- package/src/lib/filenames.js +54 -66
- package/src/lib/ignore.js +3 -0
- package/src/lib/input-parser.js +2 -6
- package/src/lib/insert.js +34 -49
- package/src/lib/structure.js +23 -8
- package/src/lib/ticketing.js +66 -9
- package/src/lib/toe-stepping.js +103 -3
- package/src/migrations/008-metadata-uid-in-suffix.js +4 -2
- package/src/migrations/009-fix-media-collision-metadata-names.js +9 -3
- package/src/migrations/013-remove-uid-from-meta-filenames.js +117 -0
- package/src/migrations/014-entity-dir-to-data-source.js +68 -0
|
@@ -49,10 +49,9 @@ project-root/
|
|
|
49
49
|
├── lib/ # Server-mapped files (mirrors DBO table data)
|
|
50
50
|
│ ├── bins/ # Bin entities and their children (see Bin Mapping below)
|
|
51
51
|
│ ├── extension/ # Extension records, sub-foldered by Descriptor
|
|
52
|
-
│ ├──
|
|
52
|
+
│ ├── data_source/ # Data source configs + entity (table) definitions
|
|
53
53
|
│ ├── site/ # Site definitions
|
|
54
54
|
│ ├── security/ # Security rule records
|
|
55
|
-
│ ├── data_source/ # Data source configurations
|
|
56
55
|
│ └── app_version/ # App version metadata
|
|
57
56
|
│
|
|
58
57
|
├── docs/ # Project documentation (scripts, embeds, pages, components)
|
|
@@ -83,11 +82,11 @@ Files belonging to a `bin` entity are placed in directories matching their BinID
|
|
|
83
82
|
}
|
|
84
83
|
```
|
|
85
84
|
|
|
86
|
-
So `lib/bins/app/assets/css/colors.css` lives inside the bin whose `fullPath` is `app/assets/css`. Every file has a companion `.metadata.json` (
|
|
85
|
+
So `lib/bins/app/assets/css/colors.css` lives inside the bin whose `fullPath` is `app/assets/css`. Every file has a companion `.metadata.json` (e.g. `colors.metadata.json`) containing the server record fields, including the `UID` field.
|
|
87
86
|
|
|
88
87
|
### Non-bin entities (no BinID column)
|
|
89
88
|
|
|
90
|
-
Entities without a BinID column get their own directory under `lib/`, named after the entity: `lib/extension/`, `lib/
|
|
89
|
+
Entities without a BinID column get their own directory under `lib/`, named after the entity: `lib/extension/`, `lib/site/`, `lib/security/`, `lib/data_source/`, `lib/app_version/`. Exception: `entity` (table definitions) shares `lib/data_source/` rather than getting its own directory — both `_entity: "data_source"` and `_entity: "entity"` records live there.
|
|
91
90
|
|
|
92
91
|
### Extension special case
|
|
93
92
|
|
|
@@ -96,7 +95,7 @@ The `extension` entity has sub-folders based on the **Descriptor** column value:
|
|
|
96
95
|
- `lib/extension/Widget/` — Widget extensions
|
|
97
96
|
- `lib/extension/Descriptor Definition/` — Descriptor definitions themselves
|
|
98
97
|
|
|
99
|
-
Each extension file follows: `{Name}.{ContentColumnName}.{ext}` with a companion `{Name}.metadata
|
|
98
|
+
Each extension file follows: `{Name}.{ContentColumnName}.{ext}` with a companion `{Name}.metadata.json` (UID stored inside the JSON).
|
|
100
99
|
|
|
101
100
|
## .app/ Configuration
|
|
102
101
|
|
|
@@ -152,10 +151,10 @@ When `app_dependencies/{app_name}/` exists (e.g., `app_dependencies/operator/`),
|
|
|
152
151
|
## CLI vs REST API
|
|
153
152
|
|
|
154
153
|
### Use the DBO CLI for:
|
|
155
|
-
- **File operations**: `pull`, `push`, `
|
|
154
|
+
- **File operations**: `pull`, `push`, `adopt`, `clone`, `rm`, `diff` — for `adopt`, always pass `-e <entity>` (e.g., `-e content` for HTML/CSS/JS, `-e media` for images/binaries); entity cannot be inferred for standalone files in `lib/bins/`
|
|
156
155
|
- **Deployment**: `deploy` (shorthand or UID), content deploy
|
|
157
156
|
- **Project setup**: `init`, `login`, `status`
|
|
158
|
-
- **Batch operations**: pushing directories, scanning for
|
|
157
|
+
- **Batch operations**: pushing directories, scanning for unadopted files
|
|
159
158
|
|
|
160
159
|
The CLI handles metadata management, change detection, and file-to-record synchronization automatically.
|
|
161
160
|
|
|
@@ -182,6 +181,48 @@ RowID:del{id};entity:entityName=true # Delete
|
|
|
182
181
|
```
|
|
183
182
|
Always include `_confirm=true`. Special values: `_{now}` (datetime), `_{null}` (null).
|
|
184
183
|
|
|
184
|
+
## Creating New Assets
|
|
185
|
+
|
|
186
|
+
Two approaches — choose based on whether the file exists locally first or on the server first.
|
|
187
|
+
|
|
188
|
+
### File-first (CLI)
|
|
189
|
+
|
|
190
|
+
Use when you're creating a new file locally and want the server to create the record.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# 1. Write the file in the correct lib/ subdirectory
|
|
194
|
+
# 2. Create the metadata companion (required before push)
|
|
195
|
+
dbo adopt -e content lib/bins/app/my-file.html # HTML/CSS/JS/SQL → content
|
|
196
|
+
dbo adopt -e media lib/bins/app/assets/img/x.png # images/binaries → media
|
|
197
|
+
|
|
198
|
+
# 3. Push — inserts a new record on the server, assigns a UID, updates local metadata
|
|
199
|
+
dbo push lib/bins/app/my-file.html
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Always pass `-e <entity>` to `adopt`. Entity cannot be inferred for standalone files in `lib/bins/` because the directory maps to multiple entity types.
|
|
203
|
+
|
|
204
|
+
### Server-first (REST API)
|
|
205
|
+
|
|
206
|
+
Use when you want to create the server record first, then pull the file locally. Required fields for the insert come from three local config files:
|
|
207
|
+
|
|
208
|
+
- **`.app/<app_short_name>.metadata_schema.json`** — entity schema: which columns are required, their types and allowed values
|
|
209
|
+
- **`.app/directories.json`** — BinID for the target directory (needed for bin-based entities like `content` and `media`)
|
|
210
|
+
- **`.app/config.json`** — `AppID`, `AppUID`, and other app-level fields required on most records
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# 1. Insert the record via REST API (RowID:add1 — must use RowID:, not RowUID:)
|
|
214
|
+
curl -b .app/cookies.txt \
|
|
215
|
+
"https://{{domain}}/api/input/submit?_confirm=true" \
|
|
216
|
+
--data-urlencode 'RowID:add1;column:content.BinID=11042' \
|
|
217
|
+
--data-urlencode 'RowID:add1;column:content.AppID={{appID}}' \
|
|
218
|
+
--data-urlencode 'RowID:add1;column:content.Name=My File'
|
|
219
|
+
|
|
220
|
+
# 2. Pull to create the local file and metadata companion
|
|
221
|
+
dbo pull -e content --filter 'Name=My File'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
After pull, the file appears in `lib/bins/` at the path matching its BinID in `directories.json`, with a `.metadata.json` companion.
|
|
225
|
+
|
|
185
226
|
## docs/ Directory
|
|
186
227
|
|
|
187
228
|
Contains Markdown documentation for this project's components — scripts, embeds, pages, UI features. These are project-specific references generated or maintained alongside development. Read these to understand documented behaviors of the current app.
|
|
@@ -213,7 +254,7 @@ The `test/` directory is for developer and Claude Code test scripts. Tests targe
|
|
|
213
254
|
| Write/update data | REST API | `POST /api/input/submit` |
|
|
214
255
|
| Pull files from server | CLI | `dbo pull -e content` |
|
|
215
256
|
| Push local changes | CLI | `dbo push path/to/file` |
|
|
216
|
-
| Add new file to server | CLI | `dbo
|
|
257
|
+
| Add new file to server | CLI | `dbo adopt path/to/file -e <entity>` |
|
|
217
258
|
| Deploy by shorthand | CLI | `dbo deploy css:colors` |
|
|
218
259
|
| Deploy by UID | CLI | `dbo deploy {uid}` |
|
|
219
260
|
| Compare with server | CLI | `dbo diff` |
|
|
@@ -87,7 +87,7 @@ RowID:{ref};column:{entity}.{Column}={value}
|
|
|
87
87
|
| `{id}` or `{uid}` | Update existing record |
|
|
88
88
|
| `del{id}` | Delete record |
|
|
89
89
|
|
|
90
|
-
`RowID:` and `RowUID:` are interchangeable. Use `RowUID:` for cross-environment stability.
|
|
90
|
+
`RowID:` and `RowUID:` are interchangeable for updates and deletes. Use `RowUID:` for cross-environment stability. **Inserts (`add1`, `add2`, …) must use `RowID:` — `RowUID:` does not work for inserts.**
|
|
91
91
|
|
|
92
92
|
### Cross-referencing within a batch
|
|
93
93
|
```
|
package/src/commands/adopt.js
CHANGED
|
@@ -93,9 +93,10 @@ const ROOT_FILE_TEMPLATES = {
|
|
|
93
93
|
*/
|
|
94
94
|
async function buildBinMetadata(filePath, entity, appConfig, structure) {
|
|
95
95
|
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
96
|
-
const ext = extname(filePath).replace('.', '').toLowerCase();
|
|
97
96
|
const fileName = basename(filePath);
|
|
98
|
-
const
|
|
97
|
+
const ext = extname(filePath).replace('.', '').toLowerCase();
|
|
98
|
+
const rawBase = basename(filePath, extname(filePath));
|
|
99
|
+
const base = rawBase || fileName; // dotfiles (.dboignore): rawBase='' → use full filename
|
|
99
100
|
const fileDir = dirname(rel);
|
|
100
101
|
const bin = findBinByPath(fileDir, structure);
|
|
101
102
|
const binPath = bin?.path || '';
|
|
@@ -103,9 +104,9 @@ async function buildBinMetadata(filePath, entity, appConfig, structure) {
|
|
|
103
104
|
const metaPath = join(dirname(filePath), `${base}.metadata.json`);
|
|
104
105
|
|
|
105
106
|
if (entity === 'content') {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// For dotfiles, base === fileName (no real ext), so path = base; otherwise base.ext
|
|
108
|
+
const localName = (base === fileName) ? fileName : `${base}.${ext}`;
|
|
109
|
+
const contentPath = binPath ? `${binPath}/${localName}` : localName;
|
|
109
110
|
const meta = {
|
|
110
111
|
_entity: 'content',
|
|
111
112
|
_companionReferenceColumns: ['Content'],
|
|
@@ -159,13 +160,13 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
159
160
|
// These files live at the project root but their metadata goes in lib/bins/app/.
|
|
160
161
|
// They bypass the dboignore check and entity inference.
|
|
161
162
|
const rootFiles = await loadRootContentFiles();
|
|
162
|
-
if (rootFiles.
|
|
163
|
+
if (rootFiles.some(f => f.toLowerCase() === fileName.toLowerCase()) && relPath === fileName) {
|
|
163
164
|
const appConfig = await loadAppConfig();
|
|
164
165
|
const structure = await loadStructureFile();
|
|
165
166
|
const appBin = findBinByPath('app', structure);
|
|
166
167
|
const binsAppDir = join(process.cwd(), BINS_DIR, 'app');
|
|
167
168
|
const ext = extname(fileName).replace('.', '').toUpperCase() || 'TXT';
|
|
168
|
-
const stem =
|
|
169
|
+
const stem = fileName;
|
|
169
170
|
|
|
170
171
|
const metaFilename = `${stem}.metadata.json`;
|
|
171
172
|
const metaPath = join(binsAppDir, metaFilename);
|
|
@@ -174,22 +175,23 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
174
175
|
let existingMeta = null;
|
|
175
176
|
try { existingMeta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
176
177
|
if (existingMeta) {
|
|
177
|
-
if (existingMeta.
|
|
178
|
-
log.warn(`"${fileName}" is already on the server (
|
|
178
|
+
if (existingMeta._CreatedOn || existingMeta._LastUpdated) {
|
|
179
|
+
log.warn(`"${fileName}" is already on the server (_CreatedOn/_LastUpdated present) — skipping.`);
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
182
|
if (!options.yes) {
|
|
182
183
|
const inquirer = (await import('inquirer')).default;
|
|
183
184
|
const { overwrite } = await inquirer.prompt([{
|
|
184
185
|
type: 'confirm', name: 'overwrite',
|
|
185
|
-
message: `Metadata already exists for "${fileName}" (
|
|
186
|
+
message: `Metadata already exists for "${fileName}" (not yet on server). Overwrite?`,
|
|
186
187
|
default: false,
|
|
187
188
|
}]);
|
|
188
189
|
if (!overwrite) return;
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
const
|
|
193
|
+
const tmplKey = Object.keys(ROOT_FILE_TEMPLATES).find(k => k.toLowerCase() === fileName.toLowerCase());
|
|
194
|
+
const tmpl = (tmplKey ? ROOT_FILE_TEMPLATES[tmplKey] : null) || { Extension: ext, Public: 0, Active: 1, Title: fileName };
|
|
193
195
|
const meta = {
|
|
194
196
|
_entity: 'content',
|
|
195
197
|
_companionReferenceColumns: ['Content'],
|
|
@@ -214,8 +216,8 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
const dir = dirname(filePath);
|
|
217
|
-
const
|
|
218
|
-
const base = basename(filePath
|
|
219
|
+
const rawBase = basename(filePath, extname(filePath));
|
|
220
|
+
const base = rawBase || basename(filePath); // dotfiles: '' → full filename
|
|
219
221
|
|
|
220
222
|
// Check for existing metadata
|
|
221
223
|
const metaPath = join(dir, `${base}.metadata.json`);
|
|
@@ -225,17 +227,17 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
225
227
|
} catch { /* no file — that's fine */ }
|
|
226
228
|
|
|
227
229
|
if (existingMeta) {
|
|
228
|
-
if (existingMeta.
|
|
229
|
-
log.warn(`"${fileName}" is already on the server (
|
|
230
|
+
if (existingMeta._CreatedOn || existingMeta._LastUpdated) {
|
|
231
|
+
log.warn(`"${fileName}" is already on the server (_CreatedOn/_LastUpdated present) — skipping.`);
|
|
230
232
|
return;
|
|
231
233
|
}
|
|
232
|
-
// Metadata exists but
|
|
234
|
+
// Metadata exists but record not yet on server
|
|
233
235
|
if (!options.yes) {
|
|
234
236
|
const inquirer = (await import('inquirer')).default;
|
|
235
237
|
const { overwrite } = await inquirer.prompt([{
|
|
236
238
|
type: 'confirm',
|
|
237
239
|
name: 'overwrite',
|
|
238
|
-
message: `Metadata already exists for "${fileName}" (
|
|
240
|
+
message: `Metadata already exists for "${fileName}" (not yet on server). Overwrite?`,
|
|
239
241
|
default: false,
|
|
240
242
|
}]);
|
|
241
243
|
if (!overwrite) return;
|
|
@@ -292,7 +294,7 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
292
294
|
Name: docBase,
|
|
293
295
|
};
|
|
294
296
|
if (appConfig.AppID) docMeta.AppID = appConfig.AppID;
|
|
295
|
-
const contentCol = '
|
|
297
|
+
const contentCol = 'Text';
|
|
296
298
|
docMeta[contentCol] = `@/${relPath}`;
|
|
297
299
|
docMeta._companionReferenceColumns = [contentCol];
|
|
298
300
|
|
|
@@ -354,7 +356,8 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
354
356
|
async function runInteractiveWizard(filePath, options) {
|
|
355
357
|
const inquirer = (await import('inquirer')).default;
|
|
356
358
|
const fileName = basename(filePath);
|
|
357
|
-
const
|
|
359
|
+
const rawBase = basename(filePath, extname(filePath));
|
|
360
|
+
const base = rawBase || fileName; // dotfiles: '' → full filename
|
|
358
361
|
const metaPath = join(dirname(filePath), `${base}.metadata.json`);
|
|
359
362
|
|
|
360
363
|
log.plain('');
|