@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.
@@ -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
- │ ├── entity/ # Entity metadata files
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` (or `.metadata~{uid}.json`) containing the server record fields.
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/entity/`, `lib/site/`, `lib/security/`, `lib/data_source/`, `lib/app_version/`.
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~{uid}.json`.
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`, `add`, `clone`, `rm`, `diff`
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 un-added files
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 add path/to/file` |
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
  ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "track",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Changelog and Track API logging for Claude Code — automatically logs changes to changelog.md and the remote Track task_log on every session",
5
5
  "author": {
6
6
  "name": "DBO.io"
@@ -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 base = basename(filePath, extname(filePath));
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
- const contentPath = binPath
107
- ? `${binPath}/${base}.${ext}`
108
- : `${base}.${ext}`;
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.includes(fileName) && relPath === fileName) {
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 = basename(fileName, extname(fileName));
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.UID || existingMeta._CreatedOn) {
178
- log.warn(`"${fileName}" is already on the server (has UID/_CreatedOn) — skipping.`);
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}" (no UID). Overwrite?`,
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 tmpl = ROOT_FILE_TEMPLATES[fileName] || { Extension: ext, Public: 0, Active: 1, Title: fileName };
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 ext = extname(filePath);
218
- const base = basename(filePath, ext);
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.UID || existingMeta._CreatedOn) {
229
- log.warn(`"${fileName}" is already on the server (has UID/_CreatedOn) — skipping.`);
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 no server record
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}" (no UID). Overwrite?`,
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 = 'String10';
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 base = basename(filePath, extname(filePath));
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('');