@dboio/cli 0.19.2 → 0.19.7
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 +54 -7
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +5 -0
- package/plugins/claude/dbo/commands/dbo.md +1 -1
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +54 -7
- package/plugins/claude/dbo/skills/cookbook/SKILL.md +152 -0
- package/plugins/claude/track/commands/install.md +1 -1
- package/src/commands/adopt.js +67 -13
- package/src/commands/clone.js +245 -47
- package/src/commands/push.js +133 -44
- package/src/lib/config.js +47 -0
package/README.md
CHANGED
|
@@ -157,7 +157,9 @@ my-project/
|
|
|
157
157
|
├── test/ # Project-level tests
|
|
158
158
|
├── trash/ # Staged soft-deleted files from dbo rm
|
|
159
159
|
├── docs/ # Project documentation and docs entities
|
|
160
|
-
├── manifest.json # PWA web app manifest (
|
|
160
|
+
├── manifest.json # PWA web app manifest (root content file — server-tracked)
|
|
161
|
+
├── CLAUDE.md # Claude Code instructions (root content file — server-tracked)
|
|
162
|
+
├── README.md # Project readme (root content file — server-tracked)
|
|
161
163
|
├── .gitignore # Tells Git to ignore files in the repo sync
|
|
162
164
|
├── .dboignore # Tells dbo cli to ignore files in commands
|
|
163
165
|
├── package.json # Metadata, scripts, and dependency list
|
|
@@ -244,6 +246,7 @@ Use `dbo status` to see how many pending migrations exist.
|
|
|
244
246
|
| `TicketSuggestionOutput` | string | Output UID for ticket suggestions (auto-set during init, default `ojaie9t3o0kfvliahnuuda`) |
|
|
245
247
|
| `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. |
|
|
246
248
|
| `ContentPlacement` | `bin` \| `path` | Where to place content files during clone (default: `bin`) |
|
|
249
|
+
| `UserMedia` | `true` \| `false` | Whether to download user media (`/media/<app>/user/...`) during clone. Set on first clone via prompt (or `false` in non-interactive mode). |
|
|
247
250
|
| `<Entity>FilenameCol` | column name | Filename column for entity-dir records (e.g., `ExtensionFilenameCol`) |
|
|
248
251
|
| `dependencies` | string[] | Array of app short-names to auto-clone as read-only local checkouts. Default: `["_system"]` |
|
|
249
252
|
| `dependencyLastUpdated` | object | Map of dependency short-name to last-cloned `_LastUpdated` ISO string |
|
|
@@ -286,20 +289,46 @@ Clone of the original app JSON from the server with entity entries replaced by `
|
|
|
286
289
|
|
|
287
290
|
The `_domain` field in the app metadata file 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.
|
|
288
291
|
|
|
289
|
-
#### `manifest.json`
|
|
292
|
+
#### Root content files (`manifest.json`, `CLAUDE.md`, `README.md`)
|
|
290
293
|
|
|
291
|
-
|
|
294
|
+
Certain files live at the **project root** (not inside `lib/`) but are still tracked and synced to the server as `content` entities. Their metadata companions live in `lib/bins/app/` with a root-relative `@/` reference (`Content: "@/manifest.json"`).
|
|
295
|
+
|
|
296
|
+
| File | Server entity | Public | Description |
|
|
297
|
+
|------|--------------|--------|-------------|
|
|
298
|
+
| `manifest.json` | content | Yes | [PWA web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) |
|
|
299
|
+
| `CLAUDE.md` | content | No | Claude Code project instructions |
|
|
300
|
+
| `README.md` | content | Yes | Project readme |
|
|
301
|
+
|
|
302
|
+
**`dbo clone`** writes all three to the project root. If the server has no record for a file, a stub is generated automatically (`manifest.json` from app metadata, `CLAUDE.md` and `README.md` from app name/description).
|
|
303
|
+
|
|
304
|
+
**`dbo push`** auto-creates the companion metadata in `lib/bins/app/` for any listed root file that exists locally but has no tracked metadata yet — no manual `dbo adopt` needed before the first push.
|
|
305
|
+
|
|
306
|
+
**`dbo adopt <filename>`** called from the project root also works for any root content file: it bypasses the `.dboignore` check and entity inference, and writes the metadata directly to `lib/bins/app/`.
|
|
307
|
+
|
|
308
|
+
**`manifest.json`** generated fields (when produced as a stub):
|
|
292
309
|
|
|
293
310
|
| Field | Source |
|
|
294
311
|
|-------|--------|
|
|
295
|
-
| `name` |
|
|
312
|
+
| `name` | `AppName` |
|
|
296
313
|
| `short_name` | `AppShortName` |
|
|
297
314
|
| `description` | `App.Description` |
|
|
298
315
|
| `start_url` / `scope` | `/app/<ShortName>/ui/` |
|
|
299
|
-
| `background_color` | Extracted from the `widget` extension matching
|
|
316
|
+
| `background_color` | Extracted from the `widget` extension matching `ShortName` (field `String4`), defaults to `#ffffff` |
|
|
300
317
|
| `theme_color` | `#000000` |
|
|
301
318
|
|
|
302
|
-
|
|
319
|
+
#### `rootContentFiles` config
|
|
320
|
+
|
|
321
|
+
The list of root content files is stored in `.app/config.json` under `rootContentFiles`:
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"rootContentFiles": ["CLAUDE.md", "README.md", "manifest.json"]
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- **Absent**: defaults are written on the first `dbo push` — `["CLAUDE.md", "README.md", "manifest.json"]`
|
|
330
|
+
- **`[]`, `false`, or `null`**: feature disabled — no root content files are auto-managed
|
|
331
|
+
- **Custom list**: any filename can be added; metadata is derived from the file extension
|
|
303
332
|
|
|
304
333
|
### `.dboignore`
|
|
305
334
|
|
|
@@ -460,7 +489,7 @@ The project's reference domain is stored in `.app/<shortName>.metadata.json._dom
|
|
|
460
489
|
6. **Creates directories** — processes `children.bin` to build the directory hierarchy based on `ParentBinID` relationships
|
|
461
490
|
7. **Saves `.app/directories.json`** — maps BinIDs to directory paths for file placement
|
|
462
491
|
8. **Writes content files** — decodes base64 content, creates `*.metadata.json` + content files in the correct bin directory. Filename columns and companion file extraction preferences are auto-applied with sensible defaults on first clone (use `--configure` to re-prompt)
|
|
463
|
-
9. **Downloads media files** —
|
|
492
|
+
9. **Downloads media files** — classifies records into three scopes before downloading: `app` (`/media/<app>/app/...` or no FullPath — always downloaded), `user` (`/media/<app>/user/...` — opt-in, preference saved as `UserMedia` in `config.json`), and `foreign` (`/media/<other_app>/...` — silently skipped with stale metadata written). Fetches using a fallback chain: `FullPath` directly (`/media/{app}/{path}`) → `/dir/` route → `/api/media/{uid}`. 404 errors create stale metadata to prevent re-prompting. Errors are logged to `.app/errors.log`
|
|
464
493
|
10. **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/`)
|
|
465
494
|
11. **Processes other entities** — remaining entities with a `BinID` are placed in the corresponding bin directory
|
|
466
495
|
12. **Saves `.app/<shortName>.metadata.json`** — clone of the original JSON with processed entries replaced by `@path/to/*.metadata.json` references
|
|
@@ -526,6 +555,24 @@ This helps keep your app clean by removing database records for media files that
|
|
|
526
555
|
|
|
527
556
|
In non-interactive mode (`-y`), stale cleanup is skipped (conservative default).
|
|
528
557
|
|
|
558
|
+
#### Media scope filtering
|
|
559
|
+
|
|
560
|
+
During `dbo clone`, media records are classified by their `FullPath` relative to the app short name:
|
|
561
|
+
|
|
562
|
+
| Scope | Path pattern | Behavior |
|
|
563
|
+
|-------|-------------|----------|
|
|
564
|
+
| `app` | `/media/<app>/app/...` or no `FullPath` | Always downloaded |
|
|
565
|
+
| `user` | `/media/<app>/user/...` | Downloaded only if `UserMedia=true` in `.app/config.json` |
|
|
566
|
+
| `foreign` | `/media/<other_app>/...` | Never downloaded — stale metadata written to prevent re-prompts |
|
|
567
|
+
|
|
568
|
+
On the first clone with user media present, you'll be prompted once:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
App has 14 user media file(s) (e.g. "avatar.jpg"). Download user media? (saves preference)
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The answer is saved as `UserMedia` in `.app/config.json` and reused on subsequent clones. In non-interactive mode (`-y`), user media defaults to skipped (`UserMedia=false`).
|
|
575
|
+
|
|
529
576
|
#### Path resolution
|
|
530
577
|
|
|
531
578
|
When a content record has both `Path` and `BinID`, the CLI prompts:
|
package/package.json
CHANGED
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"path": "skills/white-paper/SKILL.md",
|
|
16
16
|
"name": "white-paper",
|
|
17
17
|
"description": "DBO project context and architecture guide — project structure, file-to-server mapping, dependencies, CLI vs API boundaries"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "skills/cookbook/SKILL.md",
|
|
21
|
+
"name": "cookbook",
|
|
22
|
+
"description": "Operational DOs and DON'Ts for working correctly in any DBO project — file operations, build recipes, metadata handling, gotchas"
|
|
18
23
|
}
|
|
19
24
|
]
|
|
20
25
|
}
|
|
@@ -43,7 +43,7 @@ dbo $ARGUMENTS
|
|
|
43
43
|
| `init` | Initialize `.app/` configuration |
|
|
44
44
|
| `login` / `logout` | Authenticate / clear session |
|
|
45
45
|
| `status` | Show config, domain, session info |
|
|
46
|
-
| `clone` | Clone an app to local project structure |
|
|
46
|
+
| `clone` | Clone an app to local project structure (media filtered by scope: app/user/foreign) |
|
|
47
47
|
| `pull` | Pull records to local files |
|
|
48
48
|
| `push` | Push local changes to server |
|
|
49
49
|
| `add` | Add a new file to DBO.io |
|
|
@@ -157,7 +157,9 @@ my-project/
|
|
|
157
157
|
├── test/ # Project-level tests
|
|
158
158
|
├── trash/ # Staged soft-deleted files from dbo rm
|
|
159
159
|
├── docs/ # Project documentation and docs entities
|
|
160
|
-
├── manifest.json # PWA web app manifest (
|
|
160
|
+
├── manifest.json # PWA web app manifest (root content file — server-tracked)
|
|
161
|
+
├── CLAUDE.md # Claude Code instructions (root content file — server-tracked)
|
|
162
|
+
├── README.md # Project readme (root content file — server-tracked)
|
|
161
163
|
├── .gitignore # Tells Git to ignore files in the repo sync
|
|
162
164
|
├── .dboignore # Tells dbo cli to ignore files in commands
|
|
163
165
|
├── package.json # Metadata, scripts, and dependency list
|
|
@@ -244,6 +246,7 @@ Use `dbo status` to see how many pending migrations exist.
|
|
|
244
246
|
| `TicketSuggestionOutput` | string | Output UID for ticket suggestions (auto-set during init, default `ojaie9t3o0kfvliahnuuda`) |
|
|
245
247
|
| `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. |
|
|
246
248
|
| `ContentPlacement` | `bin` \| `path` | Where to place content files during clone (default: `bin`) |
|
|
249
|
+
| `UserMedia` | `true` \| `false` | Whether to download user media (`/media/<app>/user/...`) during clone. Set on first clone via prompt (or `false` in non-interactive mode). |
|
|
247
250
|
| `<Entity>FilenameCol` | column name | Filename column for entity-dir records (e.g., `ExtensionFilenameCol`) |
|
|
248
251
|
| `dependencies` | string[] | Array of app short-names to auto-clone as read-only local checkouts. Default: `["_system"]` |
|
|
249
252
|
| `dependencyLastUpdated` | object | Map of dependency short-name to last-cloned `_LastUpdated` ISO string |
|
|
@@ -286,20 +289,46 @@ Clone of the original app JSON from the server with entity entries replaced by `
|
|
|
286
289
|
|
|
287
290
|
The `_domain` field in the app metadata file 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.
|
|
288
291
|
|
|
289
|
-
#### `manifest.json`
|
|
292
|
+
#### Root content files (`manifest.json`, `CLAUDE.md`, `README.md`)
|
|
290
293
|
|
|
291
|
-
|
|
294
|
+
Certain files live at the **project root** (not inside `lib/`) but are still tracked and synced to the server as `content` entities. Their metadata companions live in `lib/bins/app/` with a root-relative `@/` reference (`Content: "@/manifest.json"`).
|
|
295
|
+
|
|
296
|
+
| File | Server entity | Public | Description |
|
|
297
|
+
|------|--------------|--------|-------------|
|
|
298
|
+
| `manifest.json` | content | Yes | [PWA web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) |
|
|
299
|
+
| `CLAUDE.md` | content | No | Claude Code project instructions |
|
|
300
|
+
| `README.md` | content | Yes | Project readme |
|
|
301
|
+
|
|
302
|
+
**`dbo clone`** writes all three to the project root. If the server has no record for a file, a stub is generated automatically (`manifest.json` from app metadata, `CLAUDE.md` and `README.md` from app name/description).
|
|
303
|
+
|
|
304
|
+
**`dbo push`** auto-creates the companion metadata in `lib/bins/app/` for any listed root file that exists locally but has no tracked metadata yet — no manual `dbo adopt` needed before the first push.
|
|
305
|
+
|
|
306
|
+
**`dbo adopt <filename>`** called from the project root also works for any root content file: it bypasses the `.dboignore` check and entity inference, and writes the metadata directly to `lib/bins/app/`.
|
|
307
|
+
|
|
308
|
+
**`manifest.json`** generated fields (when produced as a stub):
|
|
292
309
|
|
|
293
310
|
| Field | Source |
|
|
294
311
|
|-------|--------|
|
|
295
|
-
| `name` |
|
|
312
|
+
| `name` | `AppName` |
|
|
296
313
|
| `short_name` | `AppShortName` |
|
|
297
314
|
| `description` | `App.Description` |
|
|
298
315
|
| `start_url` / `scope` | `/app/<ShortName>/ui/` |
|
|
299
|
-
| `background_color` | Extracted from the `widget` extension matching
|
|
316
|
+
| `background_color` | Extracted from the `widget` extension matching `ShortName` (field `String4`), defaults to `#ffffff` |
|
|
300
317
|
| `theme_color` | `#000000` |
|
|
301
318
|
|
|
302
|
-
|
|
319
|
+
#### `rootContentFiles` config
|
|
320
|
+
|
|
321
|
+
The list of root content files is stored in `.app/config.json` under `rootContentFiles`:
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"rootContentFiles": ["CLAUDE.md", "README.md", "manifest.json"]
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- **Absent**: defaults are written on the first `dbo push` — `["CLAUDE.md", "README.md", "manifest.json"]`
|
|
330
|
+
- **`[]`, `false`, or `null`**: feature disabled — no root content files are auto-managed
|
|
331
|
+
- **Custom list**: any filename can be added; metadata is derived from the file extension
|
|
303
332
|
|
|
304
333
|
### `.dboignore`
|
|
305
334
|
|
|
@@ -460,7 +489,7 @@ The project's reference domain is stored in `.app/<shortName>.metadata.json._dom
|
|
|
460
489
|
6. **Creates directories** — processes `children.bin` to build the directory hierarchy based on `ParentBinID` relationships
|
|
461
490
|
7. **Saves `.app/directories.json`** — maps BinIDs to directory paths for file placement
|
|
462
491
|
8. **Writes content files** — decodes base64 content, creates `*.metadata.json` + content files in the correct bin directory. Filename columns and companion file extraction preferences are auto-applied with sensible defaults on first clone (use `--configure` to re-prompt)
|
|
463
|
-
9. **Downloads media files** —
|
|
492
|
+
9. **Downloads media files** — classifies records into three scopes before downloading: `app` (`/media/<app>/app/...` or no FullPath — always downloaded), `user` (`/media/<app>/user/...` — opt-in, preference saved as `UserMedia` in `config.json`), and `foreign` (`/media/<other_app>/...` — silently skipped with stale metadata written). Fetches using a fallback chain: `FullPath` directly (`/media/{app}/{path}`) → `/dir/` route → `/api/media/{uid}`. 404 errors create stale metadata to prevent re-prompting. Errors are logged to `.app/errors.log`
|
|
464
493
|
10. **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/`)
|
|
465
494
|
11. **Processes other entities** — remaining entities with a `BinID` are placed in the corresponding bin directory
|
|
466
495
|
12. **Saves `.app/<shortName>.metadata.json`** — clone of the original JSON with processed entries replaced by `@path/to/*.metadata.json` references
|
|
@@ -526,6 +555,24 @@ This helps keep your app clean by removing database records for media files that
|
|
|
526
555
|
|
|
527
556
|
In non-interactive mode (`-y`), stale cleanup is skipped (conservative default).
|
|
528
557
|
|
|
558
|
+
#### Media scope filtering
|
|
559
|
+
|
|
560
|
+
During `dbo clone`, media records are classified by their `FullPath` relative to the app short name:
|
|
561
|
+
|
|
562
|
+
| Scope | Path pattern | Behavior |
|
|
563
|
+
|-------|-------------|----------|
|
|
564
|
+
| `app` | `/media/<app>/app/...` or no `FullPath` | Always downloaded |
|
|
565
|
+
| `user` | `/media/<app>/user/...` | Downloaded only if `UserMedia=true` in `.app/config.json` |
|
|
566
|
+
| `foreign` | `/media/<other_app>/...` | Never downloaded — stale metadata written to prevent re-prompts |
|
|
567
|
+
|
|
568
|
+
On the first clone with user media present, you'll be prompted once:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
App has 14 user media file(s) (e.g. "avatar.jpg"). Download user media? (saves preference)
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The answer is saved as `UserMedia` in `.app/config.json` and reused on subsequent clones. In non-interactive mode (`-y`), user media defaults to skipped (`UserMedia=false`).
|
|
575
|
+
|
|
529
576
|
#### Path resolution
|
|
530
577
|
|
|
531
578
|
When a content record has both `Path` and `BinID`, the CLI prompts:
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cookbook
|
|
3
|
+
description: >-
|
|
4
|
+
DBO project cookbook — operational DOs and DON'Ts for working correctly in any
|
|
5
|
+
DBO project. Load this skill when editing files, adding new features, debugging,
|
|
6
|
+
or when the white-paper provides architecture context but you need behavioral
|
|
7
|
+
rules. Covers file operations, build recipes, metadata handling, and common
|
|
8
|
+
gotchas.
|
|
9
|
+
user-invokable: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# DBO Project Cookbook
|
|
13
|
+
|
|
14
|
+
Operational rules for working correctly in a DBO project. This is a living document — rules are added as new patterns and pitfalls are discovered.
|
|
15
|
+
|
|
16
|
+
For architecture context, load the **white-paper** skill. For CLI command reference, use `/dbo`. This cookbook focuses on *how to behave* when making changes.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## DOs
|
|
21
|
+
|
|
22
|
+
- **DO use UIDs, not numeric IDs, in templates.** Numeric IDs change across environments (dev → staging → prod). UIDs are stable and portable.
|
|
23
|
+
- **DO run `dbo adopt` before pushing new files.** Every file in `lib/` needs a metadata companion. `dbo adopt path/to/file -e {entity}` creates one. Without it, `dbo push` won't know what server record to target.
|
|
24
|
+
- **DO build from `src/` before pushing compiled assets.** Check `.app/scripts.json` for the build command. Run `npm run build` or the specific build script, then push the compiled output from `lib/`.
|
|
25
|
+
- **DO check `.app/scripts.json` for build/postpush chains before manual deploys.** A file may have postpush hooks that auto-deploy dependent assets. Pushing manually with `dbo deploy` bypasses these chains.
|
|
26
|
+
- **DO read the metadata file before modifying a server-tracked file.** The `.metadata~{uid}.json` companion tells you what entity, column, and record the file maps to. This context prevents wrong-target pushes.
|
|
27
|
+
- **DO use `dbo diff` to verify local vs server state before bulk pushes.** Catches unexpected server-side changes that would be overwritten.
|
|
28
|
+
- **DO check `app_dependencies/` for parent app patterns when building extensions.** Descriptor definitions, CSS design tokens, and JS APIs from the parent app define what's available.
|
|
29
|
+
- **DO use the REST API with `.app/cookies.txt` for data operations, CLI for file operations.** The CLI handles metadata sync; the API handles data reads/writes. Don't cross these boundaries.
|
|
30
|
+
- **DO check `.app/deploy_config.json` for existing shorthands before creating deploy commands.** Shorthands are auto-generated by `dbo clone` and `dbo adopt`.
|
|
31
|
+
- **DO use `dbo push --confirm false` to validate before committing a push.** Dry-run catches issues without touching the server.
|
|
32
|
+
|
|
33
|
+
## DON'Ts
|
|
34
|
+
|
|
35
|
+
- **DON'T push `src/`, `test/`, `node_modules/`, `app_dependencies/`, or `trash/` to the server.** These are strictly local. Only `lib/` and `docs/` contents map to server records.
|
|
36
|
+
- **DON'T use numeric IDs in templates.** They break across environments. Always use UIDs.
|
|
37
|
+
- **DON'T create files in `lib/` without a metadata companion.** Use `dbo adopt` to assign one. Files without metadata are invisible to the CLI and won't sync.
|
|
38
|
+
- **DON'T bypass `dbo push` by deploying raw files.** Push handles build scripts, metadata sync, and postpush hooks. Deploying directly skips all of that.
|
|
39
|
+
- **DON'T assume a deploy shorthand exists — check `.app/deploy_config.json` first.** Not all files have shorthands. Use `dbo push` for files without one.
|
|
40
|
+
- **DON'T modify vendor files in `node_modules/` or `app_dependencies/`.** These are read-only references. Vendor changes belong upstream; dependency changes belong in the source project.
|
|
41
|
+
- **DON'T edit files in `app_dependencies/` directly.** They are cloned read-only mirrors. Edit in the source project and re-clone.
|
|
42
|
+
- **DON'T push minified files without also pushing their unminified source.** The server tracks both. Pushing only `.min.css` leaves the content record out of sync.
|
|
43
|
+
- **DON'T delete metadata files manually.** Use `dbo rm` to remove a file and its metadata together. Orphaned metadata causes ghost records.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## File Operation Recipes
|
|
48
|
+
|
|
49
|
+
### Add a new file to the server
|
|
50
|
+
```bash
|
|
51
|
+
# 1. Create the file in the correct lib/ subdirectory
|
|
52
|
+
# 2. Create metadata companion (entity is usually "content" for text, "media" for binary)
|
|
53
|
+
dbo adopt lib/bins/app/assets/css/new-file.css -e content
|
|
54
|
+
|
|
55
|
+
# 3. Push — inserts a new server record
|
|
56
|
+
dbo push lib/bins/app/assets/css/new-file.css
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Modify an existing server-tracked file
|
|
60
|
+
```bash
|
|
61
|
+
# 1. Edit the file locally
|
|
62
|
+
# 2. Push the change (metadata already exists from clone)
|
|
63
|
+
dbo push lib/bins/app/assets/css/colors.css
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Delete a file from the server
|
|
67
|
+
```bash
|
|
68
|
+
# Removes the local file, its metadata, and the server record
|
|
69
|
+
dbo rm lib/bins/app/assets/css/old-file.css
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Rename or move a file
|
|
73
|
+
There is no rename command. Instead:
|
|
74
|
+
```bash
|
|
75
|
+
# 1. Create the new file with the desired name/location
|
|
76
|
+
# 2. Adopt it as a new record
|
|
77
|
+
dbo adopt lib/bins/app/assets/css/renamed.css -e content
|
|
78
|
+
# 3. Push the new file
|
|
79
|
+
dbo push lib/bins/app/assets/css/renamed.css
|
|
80
|
+
# 4. Remove the old file
|
|
81
|
+
dbo rm lib/bins/app/assets/css/old-name.css
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Move a file between bins
|
|
85
|
+
Same as rename — the bin is determined by the directory path. Moving to a new directory means a new BinID, which requires a new record.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Build Recipes
|
|
90
|
+
|
|
91
|
+
### CSS: SASS source → compiled → minified
|
|
92
|
+
```bash
|
|
93
|
+
# Build compiles src/sass/ → lib/bins/.../css/ and minifies
|
|
94
|
+
npm run build
|
|
95
|
+
# Or run the specific target from .app/scripts.json:
|
|
96
|
+
sass src/sass/colors:lib/bins/app/assets/css
|
|
97
|
+
csso lib/bins/app/assets/css/colors.css --output lib/bins/app/assets/css/colors.min.css
|
|
98
|
+
```
|
|
99
|
+
Then push both the unminified and minified versions.
|
|
100
|
+
|
|
101
|
+
### JS: Rollup bundle
|
|
102
|
+
```bash
|
|
103
|
+
# Bundles ES6 modules from src/ into lib/bins/.../js/app.min.js
|
|
104
|
+
rollup --config src/rollup.config.mjs
|
|
105
|
+
# Then push
|
|
106
|
+
dbo push lib/bins/app/assets/js/app.min.js
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### When does push auto-compile?
|
|
110
|
+
It doesn't. `dbo push` triggers **postpush** scripts (defined in `.app/scripts.json` under `targets.{file}.postpush`), but these are for deploying *dependent* assets, not for compiling. Always build first, then push.
|
|
111
|
+
|
|
112
|
+
### Check what builds exist
|
|
113
|
+
```bash
|
|
114
|
+
# Read the scripts config
|
|
115
|
+
cat .app/scripts.json
|
|
116
|
+
```
|
|
117
|
+
Look at `targets.{file}.build` for compile commands and `targets.{file}.postpush` for post-push deploy chains.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Gotchas
|
|
122
|
+
|
|
123
|
+
### Metadata mismatch after manual file creation
|
|
124
|
+
**Symptom:** `dbo push` says "no changes detected" for a file you just created.
|
|
125
|
+
**Cause:** No metadata companion exists. The CLI doesn't track unregistered files.
|
|
126
|
+
**Fix:** Run `dbo adopt path/to/file -e {entity}` first.
|
|
127
|
+
|
|
128
|
+
### Postpush chain didn't fire
|
|
129
|
+
**Symptom:** You pushed a JS file but the documentation deploy didn't happen.
|
|
130
|
+
**Cause:** You used `dbo deploy` instead of `dbo push`. Deploy bypasses postpush hooks.
|
|
131
|
+
**Fix:** Use `dbo push` for files with postpush chains, or manually run the postpush commands.
|
|
132
|
+
|
|
133
|
+
### Content and media records out of sync
|
|
134
|
+
**Symptom:** CSS looks wrong on the server even though you pushed the file.
|
|
135
|
+
**Cause:** You pushed only the content record but not the media companion (or vice versa).
|
|
136
|
+
**Fix:** Push both — check `deploy_config.json` for paired `{name}` and `{name}_media` entries.
|
|
137
|
+
|
|
138
|
+
### Wrong entity type on adopt
|
|
139
|
+
**Symptom:** Pushed file lands in unexpected server location.
|
|
140
|
+
**Cause:** Used wrong `-e` flag on `dbo adopt` (e.g., `-e media` for a text file).
|
|
141
|
+
**Fix:** Remove with `dbo rm`, re-adopt with correct entity type, re-push.
|
|
142
|
+
|
|
143
|
+
### Extension files not picked up
|
|
144
|
+
**Symptom:** New extension file doesn't appear in the app.
|
|
145
|
+
**Cause:** Extension files follow strict naming: `{Name}.{ContentColumnName}.{ext}` with metadata companion.
|
|
146
|
+
**Fix:** Verify the file name matches the pattern and the descriptor type exists in `app_dependencies/`.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Project-Specific Overlays
|
|
151
|
+
|
|
152
|
+
Individual DBO projects may provide an additional conventions file at `docs/Operator_Claude.md` (or equivalent). When present, load it alongside this cookbook for project-specific DOs, DON'Ts, and patterns. When the project is a dependency of another app, the overlay appears at `app_dependencies/{app}/docs/{App}_Claude.md`.
|
package/src/commands/adopt.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile, writeFile, stat, mkdir } from 'fs/promises';
|
|
|
3
3
|
import { join, dirname, basename, extname, relative } from 'path';
|
|
4
4
|
import { log } from '../lib/logger.js';
|
|
5
5
|
import { loadIgnore } from '../lib/ignore.js';
|
|
6
|
-
import { loadAppConfig, loadAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference } from '../lib/config.js';
|
|
6
|
+
import { loadAppConfig, loadAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference, loadRootContentFiles } from '../lib/config.js';
|
|
7
7
|
import {
|
|
8
8
|
resolveDirective, resolveTemplateCols, assembleMetadata,
|
|
9
9
|
promptReferenceColumn, getTemplateCols, setTemplateCols,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from '../lib/metadata-schema.js';
|
|
12
12
|
import { loadStructureFile, findBinByPath, BINS_DIR } from '../lib/structure.js';
|
|
13
13
|
import { isMetadataFile } from '../lib/filenames.js';
|
|
14
|
-
import { findUnaddedFiles, MIME_TYPES, seedMetadataTemplate, detectDocumentationFile
|
|
14
|
+
import { findUnaddedFiles, MIME_TYPES, seedMetadataTemplate, detectDocumentationFile } from '../lib/insert.js';
|
|
15
15
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
16
16
|
|
|
17
17
|
export const adoptCommand = new Command('adopt')
|
|
@@ -79,6 +79,15 @@ function parseEntitySpec(spec, metadataSchema) {
|
|
|
79
79
|
return { entity, descriptor: null, column: sub };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Metadata templates for known root content files (mirrors push.js ROOT_FILE_TEMPLATES).
|
|
84
|
+
*/
|
|
85
|
+
const ROOT_FILE_TEMPLATES = {
|
|
86
|
+
'manifest.json': { Extension: 'JSON', Public: 1, Active: 1, Title: 'PWA Manifest' },
|
|
87
|
+
'CLAUDE.md': { Extension: 'MD', Public: 0, Active: 1, Title: 'Claude Code Instructions' },
|
|
88
|
+
'README.md': { Extension: 'MD', Public: 1, Active: 1, Title: 'README' },
|
|
89
|
+
};
|
|
90
|
+
|
|
82
91
|
/**
|
|
83
92
|
* Build entity-specific metadata for a file in or outside lib/bins/.
|
|
84
93
|
*/
|
|
@@ -144,6 +153,61 @@ async function buildBinMetadata(filePath, entity, appConfig, structure) {
|
|
|
144
153
|
async function adoptSingleFile(filePath, entityArg, options) {
|
|
145
154
|
const ig = await loadIgnore();
|
|
146
155
|
const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
156
|
+
const fileName = basename(filePath);
|
|
157
|
+
|
|
158
|
+
// --- Root content file check (before ignore): CLAUDE.md, README.md, manifest.json, etc. ---
|
|
159
|
+
// These files live at the project root but their metadata goes in lib/bins/app/.
|
|
160
|
+
// They bypass the dboignore check and entity inference.
|
|
161
|
+
const rootFiles = await loadRootContentFiles();
|
|
162
|
+
if (rootFiles.includes(fileName) && relPath === fileName) {
|
|
163
|
+
const appConfig = await loadAppConfig();
|
|
164
|
+
const structure = await loadStructureFile();
|
|
165
|
+
const appBin = findBinByPath('app', structure);
|
|
166
|
+
const binsAppDir = join(process.cwd(), BINS_DIR, 'app');
|
|
167
|
+
const ext = extname(fileName).replace('.', '').toUpperCase() || 'TXT';
|
|
168
|
+
const stem = basename(fileName, extname(fileName));
|
|
169
|
+
|
|
170
|
+
const metaFilename = `${stem}.metadata.json`;
|
|
171
|
+
const metaPath = join(binsAppDir, metaFilename);
|
|
172
|
+
|
|
173
|
+
// Check for existing metadata
|
|
174
|
+
let existingMeta = null;
|
|
175
|
+
try { existingMeta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
176
|
+
if (existingMeta) {
|
|
177
|
+
if (existingMeta.UID || existingMeta._CreatedOn) {
|
|
178
|
+
log.warn(`"${fileName}" is already on the server (has UID/_CreatedOn) — skipping.`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!options.yes) {
|
|
182
|
+
const inquirer = (await import('inquirer')).default;
|
|
183
|
+
const { overwrite } = await inquirer.prompt([{
|
|
184
|
+
type: 'confirm', name: 'overwrite',
|
|
185
|
+
message: `Metadata already exists for "${fileName}" (no UID). Overwrite?`,
|
|
186
|
+
default: false,
|
|
187
|
+
}]);
|
|
188
|
+
if (!overwrite) return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const tmpl = ROOT_FILE_TEMPLATES[fileName] || { Extension: ext, Public: 0, Active: 1, Title: fileName };
|
|
193
|
+
const meta = {
|
|
194
|
+
_entity: 'content',
|
|
195
|
+
_companionReferenceColumns: ['Content'],
|
|
196
|
+
...tmpl,
|
|
197
|
+
Content: `@/${fileName}`,
|
|
198
|
+
Path: fileName,
|
|
199
|
+
Name: fileName,
|
|
200
|
+
};
|
|
201
|
+
if (appBin) meta.BinID = appBin.binId;
|
|
202
|
+
if (appConfig.AppID) meta.AppID = appConfig.AppID;
|
|
203
|
+
|
|
204
|
+
await mkdir(binsAppDir, { recursive: true });
|
|
205
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
206
|
+
log.success(`Created ${metaFilename} for ${fileName}`);
|
|
207
|
+
log.dim(` Run "dbo push" to insert this record on the server.`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
147
211
|
if (ig.ignores(relPath)) {
|
|
148
212
|
log.dim(`Skipped (dboignored): ${relPath}`);
|
|
149
213
|
return;
|
|
@@ -152,7 +216,6 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
152
216
|
const dir = dirname(filePath);
|
|
153
217
|
const ext = extname(filePath);
|
|
154
218
|
const base = basename(filePath, ext);
|
|
155
|
-
const fileName = basename(filePath);
|
|
156
219
|
|
|
157
220
|
// Check for existing metadata
|
|
158
221
|
const metaPath = join(dir, `${base}.metadata.json`);
|
|
@@ -204,16 +267,6 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
204
267
|
|
|
205
268
|
const appConfig = await loadAppConfig();
|
|
206
269
|
|
|
207
|
-
// --- Special case: manifest.json ---
|
|
208
|
-
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
209
|
-
if (rel.toLowerCase() === 'manifest.json') {
|
|
210
|
-
const manifestResult = await detectManifestFile(filePath);
|
|
211
|
-
if (manifestResult) {
|
|
212
|
-
log.success(`Created manifest metadata at ${manifestResult.metaPath}`);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
270
|
// --- content or media entities: build entity-specific metadata ---
|
|
218
271
|
if ((entity === 'content' || entity === 'media') && !column) {
|
|
219
272
|
const structure = await loadStructureFile();
|
|
@@ -238,6 +291,7 @@ async function adoptSingleFile(filePath, entityArg, options) {
|
|
|
238
291
|
Descriptor: 'documentation',
|
|
239
292
|
Name: docBase,
|
|
240
293
|
};
|
|
294
|
+
if (appConfig.AppID) docMeta.AppID = appConfig.AppID;
|
|
241
295
|
const contentCol = 'String10';
|
|
242
296
|
docMeta[contentCol] = `@/${relPath}`;
|
|
243
297
|
docMeta._companionReferenceColumns = [contentCol];
|
package/src/commands/clone.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readFile, writeFile, appendFile, mkdir, access, readdir, rename, stat, utimes } from 'fs/promises';
|
|
2
|
+
import { readFile, writeFile, appendFile, mkdir, access, readdir, rename, stat, utimes, unlink } from 'fs/promises';
|
|
3
3
|
import { join, basename, extname, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { DboClient } from '../lib/client.js';
|
|
6
|
-
import { loadConfig, updateConfigWithApp, loadClonePlacement, saveClonePlacement, ensureGitignore, saveEntityDirPreference, loadEntityDirPreference, saveEntityContentExtractions, loadEntityContentExtractions, saveAppJsonBaseline, addDeleteEntry, loadCollisionResolutions, saveCollisionResolutions, loadSynchronize, saveSynchronize, saveAppModifyKey, loadTransactionKeyPreset, saveTransactionKeyPreset, loadOutputFilenamePreference, saveOutputFilenamePreference, saveCloneSource, loadCloneSource, saveDescriptorFilenamePreference, loadDescriptorFilenamePreference, saveDescriptorContentExtractions, loadDescriptorContentExtractions, saveExtensionDocumentationMDPlacement, loadExtensionDocumentationMDPlacement } from '../lib/config.js';
|
|
6
|
+
import { loadConfig, updateConfigWithApp, updateConfigUserMedia, loadClonePlacement, saveClonePlacement, ensureGitignore, saveEntityDirPreference, loadEntityDirPreference, saveEntityContentExtractions, loadEntityContentExtractions, saveAppJsonBaseline, addDeleteEntry, loadCollisionResolutions, saveCollisionResolutions, loadSynchronize, saveSynchronize, saveAppModifyKey, loadTransactionKeyPreset, saveTransactionKeyPreset, loadOutputFilenamePreference, saveOutputFilenamePreference, saveCloneSource, loadCloneSource, saveDescriptorFilenamePreference, loadDescriptorFilenamePreference, saveDescriptorContentExtractions, loadDescriptorContentExtractions, saveExtensionDocumentationMDPlacement, loadExtensionDocumentationMDPlacement, loadRootContentFiles } from '../lib/config.js';
|
|
7
7
|
import { buildBinHierarchy, resolveBinPath, createDirectories, saveStructureFile, findBinByPath, BINS_DIR, DEFAULT_PROJECT_DIRS, SCAFFOLD_DIRS, ENTITY_DIR_NAMES, OUTPUT_ENTITY_MAP, OUTPUT_HIERARCHY_ENTITIES, EXTENSION_DESCRIPTORS_DIR, EXTENSION_UNSUPPORTED_DIR, DOCUMENTATION_DIR, buildDescriptorMapping, saveDescriptorMapping, loadDescriptorMapping, resolveExtensionSubDir, resolveEntityDirPath, resolveFieldValue } from '../lib/structure.js';
|
|
8
8
|
import { log } from '../lib/logger.js';
|
|
9
9
|
import { buildUidFilename, buildContentFileName, buildMetaFilename, isMetadataFile, parseMetaFilename, stripUidFromFilename, hasUidInFilename } from '../lib/filenames.js';
|
|
@@ -19,6 +19,7 @@ import { runPendingMigrations } from '../lib/migrations.js';
|
|
|
19
19
|
import { upsertDeployEntry } from '../lib/deploy-config.js';
|
|
20
20
|
import { syncDependencies, parseDependenciesColumn } from '../lib/dependencies.js';
|
|
21
21
|
import { sep } from 'path';
|
|
22
|
+
import { installOrUpdateClaudeCommands } from './install.js';
|
|
22
23
|
|
|
23
24
|
/** True when cwd is inside app_dependencies/ (dependency checkout clone). */
|
|
24
25
|
function isDependencyCheckout() {
|
|
@@ -359,6 +360,26 @@ export function resolveMediaPaths(record, structure) {
|
|
|
359
360
|
return { dir, filename: companionFilename, metaPath };
|
|
360
361
|
}
|
|
361
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Classify a media record based on its FullPath relative to the app short name.
|
|
365
|
+
*
|
|
366
|
+
* Returns:
|
|
367
|
+
* 'app' — /media/<appShortName>/app/... or no FullPath (assumed app media)
|
|
368
|
+
* 'user' — /media/<appShortName>/user/...
|
|
369
|
+
* 'foreign' — /media/<other_app>/... (belongs to a different app entirely)
|
|
370
|
+
*/
|
|
371
|
+
function classifyMediaRecord(record, appShortName) {
|
|
372
|
+
const fp = record.FullPath;
|
|
373
|
+
if (!fp || !appShortName) return 'app';
|
|
374
|
+
const normalized = fp.startsWith('/') ? fp.slice(1) : fp;
|
|
375
|
+
// Must start with media/<appShortName>/
|
|
376
|
+
const appPrefix = `media/${appShortName.toLowerCase()}/`;
|
|
377
|
+
if (!normalized.toLowerCase().startsWith(appPrefix)) return 'foreign';
|
|
378
|
+
const rest = normalized.slice(appPrefix.length);
|
|
379
|
+
if (rest.toLowerCase().startsWith('user/')) return 'user';
|
|
380
|
+
return 'app';
|
|
381
|
+
}
|
|
382
|
+
|
|
362
383
|
/**
|
|
363
384
|
* Extract path components for entity-dir records.
|
|
364
385
|
* Simplified from processEntityDirEntries() for collision detection.
|
|
@@ -1392,9 +1413,10 @@ export async function performClone(source, options = {}) {
|
|
|
1392
1413
|
);
|
|
1393
1414
|
}
|
|
1394
1415
|
|
|
1395
|
-
// Step 5a: Write manifest.json to project root
|
|
1416
|
+
// Step 5a: Write root content files (manifest.json, CLAUDE.md, README.md, etc.) to project root.
|
|
1417
|
+
// Also fixes the duplicate bug: relocates companions from lib/bins/app/ to root and rewrites metadata.
|
|
1396
1418
|
if ((!entityFilter || entityFilter.has('content')) && !isDependencyCheckout()) {
|
|
1397
|
-
await
|
|
1419
|
+
await writeRootContentFiles(appJson, contentRefs);
|
|
1398
1420
|
}
|
|
1399
1421
|
|
|
1400
1422
|
// Step 5b: Process media → download binary files + metadata (skip rejected records)
|
|
@@ -1480,6 +1502,29 @@ export async function performClone(source, options = {}) {
|
|
|
1480
1502
|
if (!options.pullMode) {
|
|
1481
1503
|
log.dim(' Run "dbo login" to authenticate, then "dbo push" to deploy changes');
|
|
1482
1504
|
}
|
|
1505
|
+
|
|
1506
|
+
// Offer to install Claude Code plugins on fresh clone (not pull, not dependency checkout)
|
|
1507
|
+
if (!options.pullMode && !isDependencyCheckout()) {
|
|
1508
|
+
const pluginsDir = join(process.cwd(), '.claude', 'plugins');
|
|
1509
|
+
let pluginsAlreadyInstalled = false;
|
|
1510
|
+
try {
|
|
1511
|
+
const entries = await readdir(pluginsDir);
|
|
1512
|
+
pluginsAlreadyInstalled = entries.length > 0;
|
|
1513
|
+
} catch { /* directory doesn't exist */ }
|
|
1514
|
+
|
|
1515
|
+
if (!pluginsAlreadyInstalled) {
|
|
1516
|
+
const inquirer = (await import('inquirer')).default;
|
|
1517
|
+
const { install } = await inquirer.prompt([{
|
|
1518
|
+
type: 'confirm',
|
|
1519
|
+
name: 'install',
|
|
1520
|
+
message: 'Install Claude Code plugins for this project? (adds /dbo and /track commands)',
|
|
1521
|
+
default: true,
|
|
1522
|
+
}]);
|
|
1523
|
+
if (install) {
|
|
1524
|
+
await installOrUpdateClaudeCommands({ local: true });
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1483
1528
|
}
|
|
1484
1529
|
|
|
1485
1530
|
/**
|
|
@@ -2751,16 +2796,86 @@ async function processExtensionEntries(entries, structure, options, serverTz) {
|
|
|
2751
2796
|
async function processMediaEntries(mediaRecords, structure, options, config, appShortName, serverTz, skipUIDs = new Set(), resolvedFilenames = new Map()) {
|
|
2752
2797
|
if (!mediaRecords || mediaRecords.length === 0) return [];
|
|
2753
2798
|
|
|
2799
|
+
// ── Step 0: Classify records by path scope ──────────────────────────────
|
|
2800
|
+
// foreign = /media/<other_app>/... (not this app — never download)
|
|
2801
|
+
// user = /media/<appShortName>/user/... (opt-in via UserMedia config)
|
|
2802
|
+
// app = /media/<appShortName>/app/... or no FullPath (always download)
|
|
2803
|
+
const foreignRecords = [];
|
|
2804
|
+
const userRecords = [];
|
|
2805
|
+
const appRecords = [];
|
|
2806
|
+
|
|
2807
|
+
for (const record of mediaRecords) {
|
|
2808
|
+
if (skipUIDs.has(record.UID)) continue;
|
|
2809
|
+
const cls = classifyMediaRecord(record, appShortName);
|
|
2810
|
+
if (cls === 'foreign') foreignRecords.push(record);
|
|
2811
|
+
else if (cls === 'user') userRecords.push(record);
|
|
2812
|
+
else appRecords.push(record);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// Silently write stale metadata for foreign records so they are never
|
|
2816
|
+
// re-prompted on future clones. No download is attempted.
|
|
2817
|
+
const foreignRefs = [];
|
|
2818
|
+
for (const record of foreignRecords) {
|
|
2819
|
+
const { metaPath: scanMetaPath, dir: scanDir } = resolveMediaPaths(record, structure);
|
|
2820
|
+
const resolvedName = resolvedFilenames.get(record.UID);
|
|
2821
|
+
const effectiveMetaPath = resolvedName
|
|
2822
|
+
? join(scanDir, buildMetaFilename(resolvedName, String(record.UID || record._id || 'untitled')))
|
|
2823
|
+
: scanMetaPath;
|
|
2824
|
+
if (!(await fileExists(effectiveMetaPath))) {
|
|
2825
|
+
const staleMeta = { _entity: 'media', _foreignApp: true };
|
|
2826
|
+
for (const [k, v] of Object.entries(record)) {
|
|
2827
|
+
if (k !== 'children') staleMeta[k] = v;
|
|
2828
|
+
}
|
|
2829
|
+
try {
|
|
2830
|
+
await mkdir(scanDir, { recursive: true });
|
|
2831
|
+
await writeFile(effectiveMetaPath, JSON.stringify(staleMeta, null, 2) + '\n');
|
|
2832
|
+
if (serverTz && (record._CreatedOn || record._LastUpdated)) {
|
|
2833
|
+
await setFileTimestamps(effectiveMetaPath, record._CreatedOn, record._LastUpdated, serverTz);
|
|
2834
|
+
}
|
|
2835
|
+
} catch { /* non-critical */ }
|
|
2836
|
+
}
|
|
2837
|
+
foreignRefs.push({ uid: record.UID, metaPath: effectiveMetaPath });
|
|
2838
|
+
}
|
|
2839
|
+
if (foreignRecords.length > 0) {
|
|
2840
|
+
log.dim(` Skipped ${foreignRecords.length} foreign media file(s) (not in /media/${appShortName || '?'}/)`);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// ── User media: prompt once if preference is unset ───────────────────────
|
|
2844
|
+
// config.UserMedia: true = include, false = skip, undefined = not yet asked
|
|
2845
|
+
let includeUserMedia = config.UserMedia ?? null;
|
|
2846
|
+
if (userRecords.length > 0 && includeUserMedia === null && !options.yes) {
|
|
2847
|
+
const inquirer = (await import('inquirer')).default;
|
|
2848
|
+
const exampleFile = userRecords[0].Filename || userRecords[0].Name || 'user file';
|
|
2849
|
+
const { includeUser } = await inquirer.prompt([{
|
|
2850
|
+
type: 'confirm',
|
|
2851
|
+
name: 'includeUser',
|
|
2852
|
+
message: `App has ${userRecords.length} user media file(s) (e.g. "${exampleFile}"). Download user media? (saves preference)`,
|
|
2853
|
+
default: false,
|
|
2854
|
+
}]);
|
|
2855
|
+
includeUserMedia = includeUser;
|
|
2856
|
+
await updateConfigUserMedia(includeUserMedia);
|
|
2857
|
+
} else if (userRecords.length > 0 && options.yes && includeUserMedia === null) {
|
|
2858
|
+
// Non-interactive: default to false, save preference
|
|
2859
|
+
includeUserMedia = false;
|
|
2860
|
+
await updateConfigUserMedia(false);
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
if (!includeUserMedia && userRecords.length > 0) {
|
|
2864
|
+
log.dim(` Skipped ${userRecords.length} user media file(s) (UserMedia=false)`);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// Active records = app media + user media (if opted in)
|
|
2868
|
+
const activeRecords = includeUserMedia ? [...appRecords, ...userRecords] : appRecords;
|
|
2869
|
+
|
|
2754
2870
|
// Track stale records (404s) for cleanup prompt
|
|
2755
2871
|
const staleRecords = [];
|
|
2756
2872
|
|
|
2757
2873
|
// Pre-scan: determine which media files actually need downloading
|
|
2758
2874
|
// (new files or files with newer server timestamps)
|
|
2759
2875
|
const needsDownload = [];
|
|
2760
|
-
const upToDateRefs = [];
|
|
2876
|
+
const upToDateRefs = [...foreignRefs];
|
|
2761
2877
|
|
|
2762
|
-
for (const record of
|
|
2763
|
-
if (skipUIDs.has(record.UID)) continue;
|
|
2878
|
+
for (const record of activeRecords) {
|
|
2764
2879
|
|
|
2765
2880
|
const { metaPath: scanMetaPath, dir: scanDir } = resolveMediaPaths(record, structure);
|
|
2766
2881
|
// Use collision-resolved filename for metadata path when available (e.g. "_media" suffix)
|
|
@@ -2791,7 +2906,7 @@ async function processMediaEntries(mediaRecords, structure, options, config, app
|
|
|
2791
2906
|
}
|
|
2792
2907
|
|
|
2793
2908
|
if (needsDownload.length === 0) {
|
|
2794
|
-
log.dim(` All ${
|
|
2909
|
+
log.dim(` All ${activeRecords.length} media file(s) up to date`);
|
|
2795
2910
|
return upToDateRefs;
|
|
2796
2911
|
}
|
|
2797
2912
|
|
|
@@ -2804,7 +2919,7 @@ async function processMediaEntries(mediaRecords, structure, options, config, app
|
|
|
2804
2919
|
const { download } = await inquirer.prompt([{
|
|
2805
2920
|
type: 'confirm',
|
|
2806
2921
|
name: 'download',
|
|
2807
|
-
message: `${needsDownload.length} media file(s) need to be downloaded (${
|
|
2922
|
+
message: `${needsDownload.length} media file(s) need to be downloaded (${activeRecords.length - needsDownload.length} up to date). Attempt download now?`,
|
|
2808
2923
|
default: true,
|
|
2809
2924
|
}]);
|
|
2810
2925
|
canDownload = download;
|
|
@@ -2833,9 +2948,9 @@ async function processMediaEntries(mediaRecords, structure, options, config, app
|
|
|
2833
2948
|
let downloaded = 0;
|
|
2834
2949
|
let failed = 0;
|
|
2835
2950
|
|
|
2836
|
-
log.info(`Downloading ${
|
|
2951
|
+
log.info(`Downloading ${activeRecords.length} media file(s)...`);
|
|
2837
2952
|
|
|
2838
|
-
for (const record of
|
|
2953
|
+
for (const record of activeRecords) {
|
|
2839
2954
|
const filename = record.Filename || `${record.Name || record.UID}.${(record.Extension || 'bin').toLowerCase()}`;
|
|
2840
2955
|
|
|
2841
2956
|
if (skipUIDs.has(record.UID)) {
|
|
@@ -4089,53 +4204,136 @@ async function processOutputHierarchy(appJson, structure, options, serverTz) {
|
|
|
4089
4204
|
}
|
|
4090
4205
|
|
|
4091
4206
|
/**
|
|
4092
|
-
* Write manifest.json to project root.
|
|
4093
|
-
*
|
|
4094
|
-
*
|
|
4207
|
+
* Write root content files (manifest.json, CLAUDE.md, README.md, etc.) to the project root.
|
|
4208
|
+
*
|
|
4209
|
+
* For each filename in rootContentFiles:
|
|
4210
|
+
* - If a matching content record exists in contentRefs: relocate the companion from
|
|
4211
|
+
* lib/bins/app/<filename> to the project root, delete the bins/app copy, and rewrite
|
|
4212
|
+
* the metadata to use Content: "@/<filename>" (root-relative). This fixes the duplicate
|
|
4213
|
+
* bug where processRecord always writes companions next to the metadata file.
|
|
4214
|
+
* - If no server record: generate a fallback stub at the project root.
|
|
4215
|
+
*/
|
|
4216
|
+
async function writeRootContentFiles(appJson, contentRefs) {
|
|
4217
|
+
const rootFiles = await loadRootContentFiles();
|
|
4218
|
+
if (!rootFiles.length) return;
|
|
4219
|
+
|
|
4220
|
+
for (const filename of rootFiles) {
|
|
4221
|
+
const handled = await _writeRootFile(filename, appJson, contentRefs);
|
|
4222
|
+
if (!handled) {
|
|
4223
|
+
await _generateRootFileStub(filename, appJson);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
/**
|
|
4229
|
+
* Find the content record for a root file and relocate its companion to the project root.
|
|
4230
|
+
* Returns true if handled, false if no matching record was found.
|
|
4095
4231
|
*/
|
|
4096
|
-
async function
|
|
4097
|
-
|
|
4232
|
+
async function _writeRootFile(filename, appJson, contentRefs) {
|
|
4233
|
+
const filenameLower = filename.toLowerCase();
|
|
4234
|
+
const stemLower = filenameLower.includes('.') ? filenameLower.slice(0, filenameLower.lastIndexOf('.')) : filenameLower;
|
|
4235
|
+
const extLower = filenameLower.includes('.') ? filenameLower.slice(filenameLower.lastIndexOf('.') + 1) : '';
|
|
4236
|
+
|
|
4098
4237
|
for (const ref of contentRefs) {
|
|
4099
4238
|
let meta;
|
|
4239
|
+
try { meta = JSON.parse(await readFile(ref.metaPath, 'utf8')); } catch { continue; }
|
|
4240
|
+
|
|
4241
|
+
// Match by Name+Extension, or by Content @reference, or by Path
|
|
4242
|
+
const metaName = (meta.Name || '').toLowerCase();
|
|
4243
|
+
const metaExt = (meta.Extension || '').toLowerCase();
|
|
4244
|
+
const metaContent = String(meta.Content || '');
|
|
4245
|
+
const metaPath2 = String(meta.Path || '');
|
|
4246
|
+
const contentRef = metaContent.startsWith('@/') ? metaContent.slice(2)
|
|
4247
|
+
: metaContent.startsWith('@') ? metaContent.slice(1)
|
|
4248
|
+
: null;
|
|
4249
|
+
|
|
4250
|
+
const matchByName = metaName.startsWith(stemLower) && metaExt === extLower;
|
|
4251
|
+
const matchByContent = contentRef && contentRef.toLowerCase() === filenameLower;
|
|
4252
|
+
const matchByPath = metaPath2.replace(/^\//, '').toLowerCase() === filenameLower;
|
|
4253
|
+
|
|
4254
|
+
if (!matchByName && !matchByContent && !matchByPath) continue;
|
|
4255
|
+
|
|
4256
|
+
// Found a matching record — read companion content from bins/app dir
|
|
4257
|
+
const metaDir = dirname(ref.metaPath);
|
|
4258
|
+
const localCompanion = join(metaDir, filename);
|
|
4259
|
+
let content;
|
|
4100
4260
|
try {
|
|
4101
|
-
|
|
4102
|
-
} catch {
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
: [join(dirname(ref.metaPath), refFile), join(process.cwd(), refFile)];
|
|
4115
|
-
let content;
|
|
4116
|
-
for (const candidate of candidates) {
|
|
4117
|
-
try {
|
|
4118
|
-
content = await readFile(candidate, 'utf8');
|
|
4119
|
-
break;
|
|
4120
|
-
} catch { /* try next */ }
|
|
4121
|
-
}
|
|
4122
|
-
if (content !== undefined) {
|
|
4123
|
-
await writeFile('manifest.json', content);
|
|
4124
|
-
log.dim(' manifest.json written to project root (from server content)');
|
|
4125
|
-
} else {
|
|
4126
|
-
log.warn(` Could not find manifest.json companion file`);
|
|
4127
|
-
}
|
|
4261
|
+
content = await readFile(localCompanion, 'utf8');
|
|
4262
|
+
} catch {
|
|
4263
|
+
// Companion may already be at root (re-clone scenario) or never written (no Content on server)
|
|
4264
|
+
try { content = await readFile(join(process.cwd(), filename), 'utf8'); } catch { /* nothing */ }
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
if (content !== undefined) {
|
|
4268
|
+
await writeFile(join(process.cwd(), filename), content);
|
|
4269
|
+
log.dim(` ${filename} written to project root (from server content)`);
|
|
4270
|
+
|
|
4271
|
+
// Delete stale companion in bins/app if it exists there
|
|
4272
|
+
if (localCompanion !== join(process.cwd(), filename)) {
|
|
4273
|
+
try { await unlink(localCompanion); } catch { /* already gone or at root */ }
|
|
4128
4274
|
}
|
|
4129
|
-
|
|
4275
|
+
} else {
|
|
4276
|
+
log.warn(` Could not find companion file for ${filename}`);
|
|
4130
4277
|
}
|
|
4278
|
+
|
|
4279
|
+
// Rewrite metadata: root-relative Content reference
|
|
4280
|
+
try {
|
|
4281
|
+
const updated = { ...meta, Content: `@/${filename}` };
|
|
4282
|
+
if (!updated._companionReferenceColumns) updated._companionReferenceColumns = ['Content'];
|
|
4283
|
+
await writeFile(ref.metaPath, JSON.stringify(updated, null, 2) + '\n');
|
|
4284
|
+
} catch { /* non-critical */ }
|
|
4285
|
+
|
|
4286
|
+
return true;
|
|
4131
4287
|
}
|
|
4132
4288
|
|
|
4133
|
-
//
|
|
4289
|
+
return false; // No server record found
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
/**
|
|
4293
|
+
* Generate a fallback stub for a root content file when the server has no record.
|
|
4294
|
+
* Skips if the file already exists at the project root.
|
|
4295
|
+
*/
|
|
4296
|
+
async function _generateRootFileStub(filename, appJson) {
|
|
4297
|
+
const rootPath = join(process.cwd(), filename);
|
|
4298
|
+
try { await access(rootPath); return; } catch { /* doesn't exist — generate */ }
|
|
4299
|
+
|
|
4300
|
+
const appName = appJson.Name || appJson.ShortName || '';
|
|
4301
|
+
const description = appJson.Description || '';
|
|
4302
|
+
const filenameLower = filename.toLowerCase();
|
|
4303
|
+
|
|
4304
|
+
if (filenameLower === 'manifest.json') {
|
|
4305
|
+
await _generateManifestFallback(appJson);
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
if (filenameLower === 'claude.md') {
|
|
4310
|
+
const stub = `# ${appName}\n\nAdd Claude Code instructions for this project here.\n`;
|
|
4311
|
+
await writeFile(rootPath, stub);
|
|
4312
|
+
log.dim(` CLAUDE.md generated at project root (stub)`);
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
if (filenameLower === 'readme.md') {
|
|
4317
|
+
const parts = [`# ${appName}`];
|
|
4318
|
+
if (description) parts.push('', description);
|
|
4319
|
+
parts.push('');
|
|
4320
|
+
await writeFile(rootPath, parts.join('\n'));
|
|
4321
|
+
log.dim(` README.md generated at project root (stub)`);
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
// Unknown file type — no stub generated
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
/**
|
|
4329
|
+
* Generate manifest.json at project root from appJson values.
|
|
4330
|
+
* Preserves existing behaviour from the old writeManifestJson fallback.
|
|
4331
|
+
*/
|
|
4332
|
+
async function _generateManifestFallback(appJson) {
|
|
4134
4333
|
const shortName = appJson.ShortName || '';
|
|
4135
4334
|
const appName = appJson.Name || '';
|
|
4136
4335
|
const description = appJson.Description || '';
|
|
4137
4336
|
|
|
4138
|
-
// Find background_color from extension children (widget descriptor matching ShortName)
|
|
4139
4337
|
let bgColor = '#ffffff';
|
|
4140
4338
|
if (shortName) {
|
|
4141
4339
|
const extensions = appJson.children?.extension || [];
|
|
@@ -4167,7 +4365,7 @@ async function writeManifestJson(appJson, contentRefs) {
|
|
|
4167
4365
|
icons: [],
|
|
4168
4366
|
};
|
|
4169
4367
|
|
|
4170
|
-
await writeFile('manifest.json', JSON.stringify(manifest, null, 2) + '\n');
|
|
4368
|
+
await writeFile(join(process.cwd(), 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
4171
4369
|
log.dim(' manifest.json generated at project root (from app metadata)');
|
|
4172
4370
|
}
|
|
4173
4371
|
|
package/src/commands/push.js
CHANGED
|
@@ -6,7 +6,7 @@ import { buildInputBody, checkSubmitErrors, getSessionUserOverride } from '../li
|
|
|
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 { loadConfig, loadAppConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline, loadScripts, loadScriptsLocal, addDeleteEntry } from '../lib/config.js';
|
|
9
|
+
import { loadConfig, loadAppConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline, loadScripts, loadScriptsLocal, addDeleteEntry, loadRootContentFiles } from '../lib/config.js';
|
|
10
10
|
import { mergeScriptsConfig, resolveHooks, buildHookEnv, runBuildLifecycle, runPushLifecycle } from '../lib/scripts.js';
|
|
11
11
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
12
12
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
@@ -343,6 +343,33 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
343
343
|
metaPath = found;
|
|
344
344
|
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
345
345
|
}
|
|
346
|
+
if (!meta) {
|
|
347
|
+
// Last resort: project-wide search for cross-directory @reference.
|
|
348
|
+
// Handles cases where the companion file is in a different directory from its metadata
|
|
349
|
+
// (e.g. docs/Operator_Claude.md → lib/extension/Documentation/Operator_Claude.metadata~uid.json).
|
|
350
|
+
const absoluteFilePath = filePath.startsWith('/') ? filePath : join(process.cwd(), filePath);
|
|
351
|
+
const ig = await loadIgnore();
|
|
352
|
+
const allMetaFiles = await findMetadataFiles(process.cwd(), ig);
|
|
353
|
+
for (const candidatePath of allMetaFiles) {
|
|
354
|
+
try {
|
|
355
|
+
const candidateMeta = JSON.parse(await readFile(candidatePath, 'utf8'));
|
|
356
|
+
const metaDir = dirname(candidatePath);
|
|
357
|
+
const cols = [...(candidateMeta._companionReferenceColumns || candidateMeta._contentColumns || [])];
|
|
358
|
+
if (candidateMeta._mediaFile) cols.push('_mediaFile');
|
|
359
|
+
for (const col of cols) {
|
|
360
|
+
const ref = candidateMeta[col];
|
|
361
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
362
|
+
const resolved = resolveAtReference(String(ref).substring(1), metaDir);
|
|
363
|
+
if (resolved === absoluteFilePath) {
|
|
364
|
+
metaPath = candidatePath;
|
|
365
|
+
meta = candidateMeta;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} catch { /* skip unreadable */ }
|
|
370
|
+
if (meta) break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
346
373
|
if (!meta) {
|
|
347
374
|
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
348
375
|
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
@@ -399,70 +426,130 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
399
426
|
}
|
|
400
427
|
// ── End script hooks ────────────────────────────────────────────────
|
|
401
428
|
|
|
402
|
-
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
429
|
+
const success = await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
430
|
+
if (success) {
|
|
431
|
+
const baseline = await loadAppJsonBaseline();
|
|
432
|
+
if (baseline) {
|
|
433
|
+
await updateBaselineAfterPush(baseline, [{ meta, metaPath, changedColumns: null }]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return success;
|
|
403
437
|
}
|
|
404
438
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
439
|
+
* Metadata templates for known root content files.
|
|
440
|
+
* Keyed by filename (case-sensitive). Unknown files in rootContentFiles
|
|
441
|
+
* get a sensible derived template (extension from name, Public: 0).
|
|
408
442
|
*/
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
443
|
+
const ROOT_FILE_TEMPLATES = {
|
|
444
|
+
'manifest.json': {
|
|
445
|
+
_entity: 'content',
|
|
446
|
+
_companionReferenceColumns: ['Content'],
|
|
447
|
+
Extension: 'JSON',
|
|
448
|
+
Public: 1,
|
|
449
|
+
Active: 1,
|
|
450
|
+
Title: 'PWA Manifest',
|
|
451
|
+
},
|
|
452
|
+
'CLAUDE.md': {
|
|
453
|
+
_entity: 'content',
|
|
454
|
+
_companionReferenceColumns: ['Content'],
|
|
455
|
+
Extension: 'MD',
|
|
456
|
+
Public: 0,
|
|
457
|
+
Active: 1,
|
|
458
|
+
Title: 'Claude Code Instructions',
|
|
459
|
+
},
|
|
460
|
+
'README.md': {
|
|
461
|
+
_entity: 'content',
|
|
462
|
+
_companionReferenceColumns: ['Content'],
|
|
463
|
+
Extension: 'MD',
|
|
464
|
+
Public: 1,
|
|
465
|
+
Active: 1,
|
|
466
|
+
Title: 'README',
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* For each filename in rootContentFiles, ensure a companion metadata file
|
|
472
|
+
* exists in lib/bins/app/. Skips if the file is absent at root or if metadata
|
|
473
|
+
* already tracks it. Creates metadata with Content: "@/<filename>" so push
|
|
474
|
+
* always reads from the project root.
|
|
475
|
+
*/
|
|
476
|
+
async function ensureRootContentFiles() {
|
|
477
|
+
const rootFiles = await loadRootContentFiles();
|
|
478
|
+
if (!rootFiles.length) return;
|
|
416
479
|
|
|
417
|
-
// Scan the entire project for any metadata file that already references manifest.json.
|
|
418
|
-
// This prevents creating duplicates when the metadata lives in an unexpected location.
|
|
419
|
-
// Check both @/manifest.json (root-relative) and @manifest.json (local) references,
|
|
420
|
-
// as well as Path: manifest.json which indicates a server record for this file.
|
|
421
480
|
const ig = await loadIgnore();
|
|
422
481
|
const allMeta = await findMetadataFiles(process.cwd(), ig);
|
|
482
|
+
|
|
483
|
+
// Build a set of already-tracked filenames from existing metadata.
|
|
484
|
+
// Also strip stale fields (e.g. Descriptor) from content-entity root file metadata.
|
|
485
|
+
const tracked = new Set();
|
|
423
486
|
for (const metaPath of allMeta) {
|
|
424
487
|
try {
|
|
425
488
|
const raw = await readFile(metaPath, 'utf8');
|
|
426
489
|
const parsed = JSON.parse(raw);
|
|
427
|
-
|
|
428
|
-
|
|
490
|
+
const content = parsed.Content || '';
|
|
491
|
+
const path = parsed.Path || '';
|
|
492
|
+
// Detect both @/filename (root-relative) and @filename (local) references
|
|
493
|
+
const refName = content.startsWith('@/') ? content.slice(2)
|
|
494
|
+
: content.startsWith('@') ? content.slice(1)
|
|
495
|
+
: null;
|
|
496
|
+
if (refName) tracked.add(refName);
|
|
497
|
+
if (path) tracked.add(path.replace(/^\//, ''));
|
|
498
|
+
|
|
499
|
+
// Clean up stale Descriptor field: content entities never have Descriptor.
|
|
500
|
+
// This fixes metadata written by an earlier buggy version of the tool.
|
|
501
|
+
if (parsed._entity === 'content' && parsed.Descriptor !== undefined) {
|
|
502
|
+
const cleaned = { ...parsed };
|
|
503
|
+
delete cleaned.Descriptor;
|
|
504
|
+
await writeFile(metaPath, JSON.stringify(cleaned, null, 2) + '\n');
|
|
505
|
+
log.dim(` Removed stale Descriptor field from ${basename(metaPath)}`);
|
|
506
|
+
}
|
|
429
507
|
} catch { /* skip unreadable */ }
|
|
430
508
|
}
|
|
431
509
|
|
|
432
|
-
// No existing metadata references manifest.json — create one
|
|
433
510
|
const appConfig = await loadAppConfig();
|
|
434
511
|
const structure = await loadStructureFile();
|
|
435
512
|
const appBin = findBinByPath('app', structure);
|
|
436
|
-
|
|
437
513
|
const binsAppDir = join(process.cwd(), BINS_DIR, 'app');
|
|
438
|
-
await mkdir(binsAppDir, { recursive: true });
|
|
439
514
|
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
515
|
+
for (const filename of rootFiles) {
|
|
516
|
+
// Skip if file doesn't exist at project root
|
|
517
|
+
try { await access(join(process.cwd(), filename)); } catch { continue; }
|
|
518
|
+
// Skip if already tracked by existing metadata
|
|
519
|
+
if (tracked.has(filename)) continue;
|
|
520
|
+
|
|
521
|
+
const tmpl = ROOT_FILE_TEMPLATES[filename] || {
|
|
522
|
+
_entity: 'content',
|
|
523
|
+
_companionReferenceColumns: ['Content'],
|
|
524
|
+
Extension: (filename.includes('.') ? filename.split('.').pop().toUpperCase() : 'TXT'),
|
|
525
|
+
Public: 0,
|
|
526
|
+
Active: 1,
|
|
527
|
+
Title: filename,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const meta = {
|
|
531
|
+
...tmpl,
|
|
532
|
+
Content: `@/${filename}`,
|
|
533
|
+
Path: filename,
|
|
534
|
+
Name: filename,
|
|
535
|
+
};
|
|
536
|
+
if (appBin) meta.BinID = appBin.binId;
|
|
537
|
+
if (appConfig.AppID) meta.AppID = appConfig.AppID;
|
|
538
|
+
|
|
539
|
+
await mkdir(binsAppDir, { recursive: true });
|
|
540
|
+
const stem = filename.replace(/\.[^.]+$/, '');
|
|
541
|
+
const metaFilename = `${stem}.metadata.json`;
|
|
542
|
+
await writeFile(join(binsAppDir, metaFilename), JSON.stringify(meta, null, 2) + '\n');
|
|
543
|
+
log.info(`Auto-created ${metaFilename} for ${filename}`);
|
|
544
|
+
}
|
|
458
545
|
}
|
|
459
546
|
|
|
460
547
|
/**
|
|
461
548
|
* Push all records found in a directory (recursive)
|
|
462
549
|
*/
|
|
463
550
|
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID', deletedCount = 0) {
|
|
464
|
-
// Auto-create
|
|
465
|
-
await
|
|
551
|
+
// Auto-create metadata for root content files (manifest.json, CLAUDE.md, README.md, etc.)
|
|
552
|
+
await ensureRootContentFiles();
|
|
466
553
|
|
|
467
554
|
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
468
555
|
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
@@ -1378,12 +1465,14 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
1378
1465
|
if (editResults.length > 0) {
|
|
1379
1466
|
const updated = editResults[0]._LastUpdated || editResults[0].LastUpdated;
|
|
1380
1467
|
if (updated) {
|
|
1468
|
+
// Always update _LastUpdated in memory and on disk (required for baseline sync
|
|
1469
|
+
// even when ServerTimezone is not configured)
|
|
1470
|
+
meta._LastUpdated = updated;
|
|
1471
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
1381
1472
|
const config = await loadConfig();
|
|
1382
1473
|
const serverTz = config.ServerTimezone;
|
|
1383
1474
|
if (serverTz) {
|
|
1384
|
-
//
|
|
1385
|
-
meta._LastUpdated = updated;
|
|
1386
|
-
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
1475
|
+
// Set file mtimes to server timestamp (requires timezone for correct conversion)
|
|
1387
1476
|
await setFileTimestamps(metaPath, meta._CreatedOn, updated, serverTz);
|
|
1388
1477
|
// Update content file mtime too
|
|
1389
1478
|
const contentCols = meta._companionReferenceColumns || meta._contentColumns || [];
|
package/src/lib/config.js
CHANGED
|
@@ -173,6 +173,20 @@ export async function updateConfigWithApp({ AppID, AppUID, AppName, AppShortName
|
|
|
173
173
|
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Persist the UserMedia preference to .app/config.json.
|
|
178
|
+
* @param {boolean} value - true = download user media, false = skip
|
|
179
|
+
*/
|
|
180
|
+
export async function updateConfigUserMedia(value) {
|
|
181
|
+
await mkdir(projectDir(), { recursive: true });
|
|
182
|
+
let existing = {};
|
|
183
|
+
try {
|
|
184
|
+
existing = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
185
|
+
} catch { /* no existing config */ }
|
|
186
|
+
existing.UserMedia = value;
|
|
187
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
// ─── Dependency helpers ───────────────────────────────────────────────────
|
|
177
191
|
|
|
178
192
|
/**
|
|
@@ -1070,6 +1084,39 @@ export async function loadScriptsLocal() {
|
|
|
1070
1084
|
}
|
|
1071
1085
|
}
|
|
1072
1086
|
|
|
1087
|
+
// ─── Root Content Files ───────────────────────────────────────────────────────
|
|
1088
|
+
|
|
1089
|
+
const ROOT_CONTENT_FILES_DEFAULTS = ['CLAUDE.md', 'README.md', 'manifest.json'];
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Load rootContentFiles from .app/config.json.
|
|
1093
|
+
* If the key is absent, writes the defaults and returns them.
|
|
1094
|
+
* If the key is [], false, or null, returns [] (disabled).
|
|
1095
|
+
*/
|
|
1096
|
+
export async function loadRootContentFiles() {
|
|
1097
|
+
let existing = {};
|
|
1098
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
1099
|
+
if (!('rootContentFiles' in existing)) {
|
|
1100
|
+
await saveRootContentFiles(ROOT_CONTENT_FILES_DEFAULTS);
|
|
1101
|
+
return ROOT_CONTENT_FILES_DEFAULTS;
|
|
1102
|
+
}
|
|
1103
|
+
const val = existing.rootContentFiles;
|
|
1104
|
+
if (!val || (Array.isArray(val) && val.length === 0)) return [];
|
|
1105
|
+
return Array.isArray(val) ? val : [];
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Persist rootContentFiles to .app/config.json.
|
|
1110
|
+
* Pass [] to disable root content file tracking.
|
|
1111
|
+
*/
|
|
1112
|
+
export async function saveRootContentFiles(list) {
|
|
1113
|
+
await mkdir(projectDir(), { recursive: true });
|
|
1114
|
+
let existing = {};
|
|
1115
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
1116
|
+
existing.rootContentFiles = list;
|
|
1117
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1073
1120
|
// ─── Tag Config ───────────────────────────────────────────────────────────────
|
|
1074
1121
|
|
|
1075
1122
|
/**
|