@dboio/cli 0.7.2 → 0.8.0

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
@@ -98,6 +98,9 @@ dbo --version
98
98
  # 1. Initialize configuration for the current directory
99
99
  dbo init --domain my-domain.com
100
100
 
101
+ # 1b. Combined with cloning an app to local project bin
102
+ dbo init --domain my-domain.com --app myapp --clone
103
+
101
104
  # 2. Authenticate
102
105
  dbo login
103
106
 
@@ -144,6 +147,7 @@ All configuration is **directory-scoped**. Each project folder maintains its own
144
147
  "AppUID": "abc123",
145
148
  "AppName": "My App",
146
149
  "AppShortName": "myapp",
150
+ "cloneSource": "default",
147
151
  "ContentPlacement": "bin",
148
152
  "MediaPlacement": "fullpath"
149
153
  }
@@ -159,6 +163,7 @@ All configuration is **directory-scoped**. Each project folder maintains its own
159
163
  | `AppModifyKey` | string | ModifyKey for locked/production apps (set by `dbo clone`, used for submission guards) |
160
164
  | `TransactionKeyPreset` | `RowUID` \| `RowID` | Row key type for auto-assembled expressions (set during `dbo init`/`dbo clone`, default `RowUID`) |
161
165
  | `TicketSuggestionOutput` | string | Output UID for fetching ticket suggestions during error recovery (set during `dbo init`, default `ojaie9t3o0kfvliahnuuda`) |
166
+ | `cloneSource` | `"default"` \| file path \| URL | Where the last `dbo clone` fetched app JSON from. `"default"` = server fetch via `AppShortName`; any other value = the explicit local file path or URL used. Set automatically after each successful clone. |
162
167
  | `ContentPlacement` | `bin` \| `path` \| `ask` | Where to place content files during clone |
163
168
  | `MediaPlacement` | `bin` \| `fullpath` \| `ask` | Where to place media files during clone |
164
169
  | `<Entity>FilenameCol` | column name | Filename column for entity-dir records (e.g., `ExtensionFilenameCol`) |
@@ -174,6 +179,41 @@ These are set interactively on first clone and saved for future use. Pre-set the
174
179
 
175
180
  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.
176
181
 
182
+ ### `.dboignore`
183
+
184
+ A gitignore-style file in the project root that controls which files and directories are excluded from CLI operations. Created automatically by `dbo init` with sensible defaults.
185
+
186
+ **Used by:**
187
+ - `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)
190
+
191
+ **Syntax:** Same as `.gitignore` — glob patterns, `#` comments, blank lines, negation with `!`, directory-only patterns with trailing `/`.
192
+
193
+ **Default patterns:**
194
+
195
+ ```gitignore
196
+ .dbo/ # DBO internal
197
+ *.dboio.json # Export files
198
+ app.json # Clone output
199
+ .app.json # Clone baseline
200
+ dbo.deploy.json # Deployment manifest
201
+ .git/ # Version control
202
+ .gitignore
203
+ node_modules/ # Node
204
+ package.json
205
+ package-lock.json
206
+ .claude/ # AI / tooling
207
+ .mcp.json
208
+ .idea/ # Editor / IDE
209
+ .vscode/
210
+ *.codekit3
211
+ README.md # Repo scaffolding
212
+ SETUP.md
213
+ ```
214
+
215
+ Edit `.dboignore` to add or remove patterns. Committed to git and shared across users.
216
+
177
217
  ### Legacy migration
178
218
 
179
219
  If your project uses the older `.domain`, `.username`, `.password`, `.cookies` files, `dbo init` will detect them and offer to migrate automatically.
@@ -215,6 +255,7 @@ dbo init --scaffold --yes # scaffold dirs non-intera
215
255
  | `-g, --global` | Install Claude commands globally (`~/.claude/commands/`) |
216
256
  | `--local` | Install Claude commands to project (`.claude/commands/`) |
217
257
  | `--scaffold` | Pre-create standard project directories (`App Versions`, `Automations`, `Bins`, `Data Sources`, `Documentation`, `Extensions`, `Groups`, `Integrations`, `Sites`) |
258
+ | `--dboignore` | Create `.dboignore` with default patterns (use with `--force` to overwrite existing) |
218
259
  | `-y, --yes` | Skip all interactive prompts (legacy migration, Claude Code setup) |
219
260
  | `--non-interactive` | Alias for `--yes` |
220
261
 
@@ -236,14 +277,24 @@ dbo clone --app myapp
236
277
 
237
278
  # Combined with init
238
279
  dbo init --domain my-domain.com --app myapp --clone
280
+
281
+ # Extension descriptor sub-directory handling
282
+ dbo clone -e extension # Clone all extensions (sorted by descriptor type)
283
+ dbo clone -e extension --documentation-only # Clone only documentation extensions
284
+ dbo clone -e extension --documentation-only --force # Reset documentation preferences and re-clone
285
+ dbo clone -e extension --descriptor-types false # Clone extensions flat (no descriptor sorting)
239
286
  ```
240
287
 
241
288
  | Flag | Description |
242
289
  |------|-------------|
243
- | `<source>` | Local JSON file path (optional positional argument) |
290
+ | `<source>` | Local file path or URL to load app JSON from (optional positional argument) |
244
291
  | `--app <name>` | App short name to fetch from server |
292
+ | `-e, --entity <type>` | Only clone a specific entity type (e.g. `output`, `content`, `media`, `extension`) |
293
+ | `--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 |
245
295
  | `--domain <host>` | Override domain. Triggers a domain-change confirmation prompt when it differs from the project reference domain |
246
- | `-y, --yes` | Auto-accept all prompts |
296
+ | `--force` | Skip source mismatch confirmation and change detection; re-processes all files |
297
+ | `-y, --yes` | Auto-accept all prompts (also skips source mismatch confirmation) |
247
298
  | `-v, --verbose` | Show HTTP request details |
248
299
 
249
300
  #### Domain change detection
@@ -255,7 +306,7 @@ The project's reference domain is stored in `app.json._domain` (committed to git
255
306
  #### What clone does
256
307
 
257
308
  1. **Loads app JSON** — from a local file, server API, or interactive prompt
258
- 2. **Updates `.dbo/config.json`** — saves `AppID`, `AppUID`, `AppName`, `AppShortName`, and `AppModifyKey` (if the app is locked)
309
+ 2. **Updates `.dbo/config.json`** — saves `AppID`, `AppUID`, `AppName`, `AppShortName`, `AppModifyKey` (if the app is locked), and `cloneSource` (the source used for this clone)
259
310
  3. **Updates `package.json`** — populates `name`, `productName`, `description`, `homepage`, and `deploy` script
260
311
  4. **Creates directories** — processes `children.bin` to build the directory hierarchy based on `ParentBinID` relationships
261
312
  5. **Saves `.dbo/structure.json`** — maps BinIDs to directory paths for file placement
@@ -265,6 +316,30 @@ The project's reference domain is stored in `app.json._domain` (committed to git
265
316
  9. **Processes other entities** — remaining entities with a `BinID` are placed in the corresponding bin directory
266
317
  10. **Saves `app.json`** — clone of the original JSON with processed entries replaced by `@path/to/*.metadata.json` references
267
318
 
319
+ #### Clone source tracking
320
+
321
+ After every successful clone, the source is persisted as `cloneSource` in `.dbo/config.json`:
322
+
323
+ - `"default"` — app JSON was fetched from the server using `AppShortName` (the normal flow)
324
+ - A file path or URL — set when an explicit `<source>` argument was provided (e.g. `dbo clone /path/to/export.json`)
325
+
326
+ On subsequent clones, if you provide an explicit source that **differs** from the stored `cloneSource`, the CLI warns and asks for confirmation before proceeding:
327
+
328
+ ```
329
+ ⚠ This project was previously cloned from: default
330
+ Requested source: /path/to/export.json
331
+ ? Clone from the new source anyway? This will update the stored clone source. (y/N)
332
+ ```
333
+
334
+ Use `--force` or `-y` to skip the prompt and override without being asked.
335
+
336
+ If the initial clone attempt fails (network error, file not found, empty response), the CLI prompts for a fallback source to retry with instead of aborting:
337
+
338
+ ```
339
+ ⚠ Source did not return expected results: No app found with ShortName "myapp"
340
+ ? Enter another local file path or URL to retry (or leave empty to abort):
341
+ ```
342
+
268
343
  #### Change detection on re-clone
269
344
 
270
345
  When cloning an app that was already cloned locally, the CLI detects existing files and compares modification times against the server's `_LastUpdated`. You'll be prompted to overwrite, compare differences, or skip — same as `dbo pull`. Use `-y` to auto-accept all changes.
@@ -312,7 +387,7 @@ Entity types that correspond to project directories (`extension`, `app_version`,
312
387
 
313
388
  | Entity Key | Directory |
314
389
  |------------|-----------|
315
- | `extension` | `Extensions/` |
390
+ | `extension` | `Extensions/<Descriptor>/` (see below) |
316
391
  | `app_version` | `App Versions/` |
317
392
  | `data_source` | `Data Sources/` |
318
393
  | `site` | `Sites/` |
@@ -326,6 +401,44 @@ If any columns contain base64-encoded content, the CLI prompts to extract them a
326
401
 
327
402
  Use `-y` to skip prompts (uses `Name` column, no content extraction).
328
403
 
404
+ #### Extension descriptor sub-directories
405
+
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`).
407
+
408
+ During clone, the CLI:
409
+
410
+ 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
413
+ 4. **Prompts per descriptor** — filename column and content extraction prompts fire once per unique `Descriptor` value (not once for all extensions)
414
+ 5. **Persists the mapping** in `.dbo/structure.json` under `descriptorMapping`
415
+
416
+ **Config keys** (saved per descriptor in `config.json`):
417
+
418
+ | Key | Example | Purpose |
419
+ |-----|---------|---------|
420
+ | `Extension_<descriptor>_FilenameCol` | `Extension_documentation_FilenameCol` | Filename column for that descriptor type |
421
+ | `Extension_<descriptor>_ContentExtractions` | `Extension_documentation_ContentExtractions` | Content extraction preferences per column |
422
+ | `ExtensionDocumentationMDPlacement` | `"inline"` or `"root"` | Where to place documentation MD files |
423
+
424
+ **Documentation alternate placement**: When extracting MD content from `documentation` descriptor extensions, the CLI offers a choice:
425
+
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
428
+
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.
430
+
431
+ **Entity filter support**:
432
+
433
+ ```bash
434
+ dbo clone -e extension --documentation-only # Clone only documentation extensions
435
+ dbo clone -e extension --documentation-only --force # Reset documentation preferences and re-clone
436
+ dbo clone -e extension # Clone all extensions (all descriptor types)
437
+ dbo clone -e extension --descriptor-types false # Flat layout (no descriptor sorting)
438
+ ```
439
+
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.
441
+
329
442
  #### Output structure
330
443
 
331
444
  ```
@@ -344,9 +457,15 @@ project/
344
457
  CurrentTicketID.metadata.json
345
458
  ticket_test/ # ← another bin directory
346
459
  ...
347
- Extensions/ # ← entity-dir records
348
- MyExtension.metadata.json
349
- MyExtension.CustomCSS.css # ← extracted content column
460
+ Extensions/ # ← extension records organized by descriptor
461
+ Documentation/ # ← descriptor sub-directory (from descriptor_definition)
462
+ MyDoc.uid1.metadata.json
463
+ MyDoc.uid1.String10.md # ← extracted content column
464
+ Includes/
465
+ Header.uid2.metadata.json
466
+ Unsupported/ # ← always created (unmapped/null descriptors)
467
+ Documentation/ # ← root-placed documentation MD files (optional)
468
+ MyDoc.md
350
469
  Data Sources/
351
470
  MySQL-Primary.metadata.json
352
471
  Sites/
@@ -1357,7 +1476,8 @@ Smart behavior:
1357
1476
  - Compares file hashes — unchanged files are skipped
1358
1477
  - Adds project-scoped plugins to `.gitignore` (source of truth is `plugins/claude/` at repo root)
1359
1478
  - Per-plugin scope (project or global) is stored in `.dbo/config.local.json` and remembered for future upgrades
1360
- - On first install of a plugin without a stored preference, prompts for scope
1479
+ - On re-install (e.g. after `npm install` or `npm link`), scope is inferred automatically from the global plugin registry (`~/.claude/plugins/installed_plugins.json`) or existing project installation — no prompt is shown
1480
+ - On first install with no prior preference and no existing installation, prompts for scope
1361
1481
  - `--global` / `--local` flags override stored preferences and update them
1362
1482
 
1363
1483
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dboio/cli",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "CLI for the DBO.io framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,8 +21,9 @@
21
21
  "test": "node --test src/**/*.test.js tests/**/*.test.js"
22
22
  },
23
23
  "dependencies": {
24
- "commander": "^12.1.0",
25
24
  "chalk": "^5.3.0",
25
+ "commander": "^12.1.0",
26
+ "ignore": "^7.0.5",
26
27
  "inquirer": "^9.3.0"
27
28
  },
28
29
  "keywords": [
@@ -1,17 +1,15 @@
1
1
  import { Command } from 'commander';
2
- import { readFile, readdir, stat, writeFile } from 'fs/promises';
2
+ import { readFile, readdir, stat, writeFile, mkdir } 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 } from '../lib/config.js';
9
+ import { loadAppConfig, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference } from '../lib/config.js';
10
10
  import { checkStoredTicket, clearGlobalTicket } from '../lib/ticketing.js';
11
11
  import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
12
-
13
- // Directories and patterns to skip when scanning with `dbo add .`
14
- const IGNORE_DIRS = new Set(['.dbo', '.git', 'node_modules', '.svn', '.hg']);
12
+ import { loadIgnore } from '../lib/ignore.js';
15
13
 
16
14
  export const addCommand = new Command('add')
17
15
  .description('Add a new file to DBO.io (creates record on server)')
@@ -57,6 +55,23 @@ export const addCommand = new Command('add')
57
55
  * Add a single file to DBO.io.
58
56
  * Returns the defaults used (entity, contentColumn) for batch reuse.
59
57
  */
58
+ /**
59
+ * Detect whether a file lives in the project-root Documentation/ directory
60
+ * AND the ExtensionDocumentationMDPlacement preference is 'root'.
61
+ * Returns detection info object or null if not applicable.
62
+ *
63
+ * @param {string} filePath - Path to the file being added
64
+ */
65
+ async function detectDocumentationFile(filePath) {
66
+ const placement = await loadExtensionDocumentationMDPlacement();
67
+ if (placement !== 'root') return null;
68
+
69
+ const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
70
+ if (!rel.startsWith('Documentation/')) return null;
71
+
72
+ return { entity: 'extension', descriptor: 'documentation' };
73
+ }
74
+
60
75
  async function addSingleFile(filePath, client, options, batchDefaults) {
61
76
  const dir = dirname(filePath);
62
77
  const ext = extname(filePath);
@@ -87,6 +102,33 @@ async function addSingleFile(filePath, client, options, batchDefaults) {
87
102
  return await submitAdd(meta, metaPath, filePath, client, options);
88
103
  }
89
104
 
105
+ // Step 3b: Auto-infer entity/descriptor for Documentation/ files when root placement is configured
106
+ const docInfo = await detectDocumentationFile(filePath);
107
+ if (docInfo) {
108
+ const filenameCol = await loadDescriptorFilenamePreference('documentation') || 'Name';
109
+ const docBase = basename(filePath, extname(filePath));
110
+ const companionDir = join(process.cwd(), 'Extensions', 'Documentation');
111
+ await mkdir(companionDir, { recursive: true });
112
+
113
+ const docMeta = {
114
+ _entity: 'extension',
115
+ Descriptor: 'documentation',
116
+ [filenameCol]: docBase,
117
+ Name: docBase,
118
+ };
119
+
120
+ // Find the content column name from saved preferences
121
+ const contentCol = filenameCol === 'Name' ? 'String10' : 'String10';
122
+ const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
123
+ docMeta[contentCol] = `@/${relPath}`;
124
+ docMeta._contentColumns = [contentCol];
125
+
126
+ const docMetaFile = join(companionDir, `${docBase}.metadata.json`);
127
+ await writeFile(docMetaFile, JSON.stringify(docMeta, null, 2) + '\n');
128
+ log.success(`Created companion metadata at ${docMetaFile}`);
129
+ return await submitAdd(docMeta, docMetaFile, filePath, client, options);
130
+ }
131
+
90
132
  // Step 4: No usable metadata — interactive wizard
91
133
  const inquirer = (await import('inquirer')).default;
92
134
 
@@ -379,23 +421,26 @@ async function addDirectory(dirPath, client, options) {
379
421
  * - It has no companion .metadata.json, OR
380
422
  * - Its .metadata.json exists but has no _CreatedOn (never been on server)
381
423
  */
382
- async function findUnaddedFiles(dir) {
424
+ async function findUnaddedFiles(dir, ig) {
425
+ if (!ig) ig = await loadIgnore();
426
+
383
427
  const results = [];
384
428
  const entries = await readdir(dir, { withFileTypes: true });
385
429
 
386
430
  for (const entry of entries) {
387
431
  const fullPath = join(dir, entry.name);
432
+ const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
388
433
 
389
434
  if (entry.isDirectory()) {
390
- if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
391
- results.push(...await findUnaddedFiles(fullPath));
435
+ if (ig.ignores(relPath + '/') || ig.ignores(relPath)) continue;
436
+ results.push(...await findUnaddedFiles(fullPath, ig));
392
437
  continue;
393
438
  }
394
439
 
395
- // Skip metadata files themselves
440
+ // Skip metadata files themselves (structural, not user-configurable)
396
441
  if (entry.name.endsWith('.metadata.json')) continue;
397
- // Skip dotfiles
398
- if (entry.name.startsWith('.')) continue;
442
+ // Skip ignored files
443
+ if (ig.ignores(relPath)) continue;
399
444
 
400
445
  // Check for companion metadata
401
446
  const ext = extname(entry.name);