bd-manager
v0.6.23
Published
Multi-site Brilliant Directories content manager — Node.js CLI + local web dashboard for git-versioned pull/push of widgets, pages, post types, menus, forms and more across multiple BD sites.
Maintainers
Readme
bd-manager
Multi-site Brilliant Directories content manager — Node.js CLI + local web dashboard for git-versioned pull / push of widgets, pages, post types, menus, forms and more across multiple BD sites.
Each BD site lives as its own independent git repo (with main + bd-live branches). bd-manager orchestrates the round-trip between local git working tree and the live BD database: pull live state → edit locally → commit → push to BD live → resync generated fields back to disk → push to GitHub. All via a single auto-installed PHP bridge widget on each BD site.
Auth + per-site secrets
bd-manager authenticates users through a separate Cloudflare Workers backend — source lives in the private Devorahub-Metaviz/bd-manager-worker repo. The worker:
- Runs GitHub Device Flow for
bd login - Stores each BD site's
site_url+api_keyencrypted (AES-GCM) at rest in D1 - Returns decrypted credentials to CLI users who have at least read access to the matching GitHub repo
- Keeps a webhook-driven mirror of org membership and repo access so permission changes propagate in seconds
End users don't need to clone or deploy the worker — it runs at https://bd.devorahub.com (or whichever domain your org has deployed it to). Just bd login <worker-url> once and the CLI handles the rest.
Only admins who operate the backend clone the worker repo to deploy / update it.
Table of contents
- Why
- Features
- Install
- Quick start
- Directory layout
- CLI reference
- Web dashboard
- How the workflows work
- Supported sections
- Configuration files
- The bridge widget
- HTTP API
- Troubleshooting
- Development
- Security notes
- License
Why
Brilliant Directories stores all customizable content — widgets, page templates, post types, menu structures, form schemas, email templates, SEO templates, settings, labels, redirects, webhooks, plugins, subscriptions — inside its MySQL database. Edits are made through the admin UI, which means:
- No version control
- No local IDE editing with syntax highlighting
- No code review
- No diff / blame / rollback
- No way to replicate a change across multiple sites
- Accidental deletions are permanent
bd-manager treats each BD site's content as source code. You snapshot the live database into a clean directory structure on disk, edit with your normal tools, commit to git, and push changes back to BD with a full audit trail.
Features
Multi-site from day one
bd sites,bd push --all, per-site dashboards in the web UI- Each site is a separate git repo with its own remote
- Sequential multi-site push that halts safely on merge conflict and resumes from where it left off
Round-trip fidelity
- Pull: 20 BD tables → 20 section folders, code fields split into individual files (
.php,.html,.css) with js-beautify formatting - Push: local files → single-column
UPDATE(per-file) or full-itemUPDATE/INSERTvia template-fill - Resync: after every push, auto-generated fields (
widget_id,date_updated,short_code,revision_timestamp) land back in localmeta.json
Git workflow built in
mainbranch = your working treebd-livebranch = snapshot of what BD live looked like before your current push (for conflict detection)- Auto pull-rebase on push main to avoid non-fast-forward rejections
bd remote-sync --forcefor clean-slate alignment when repos diverge
Diff-first UI
- GitHub-style side-by-side diffs via diff2html
- Two compare modes: git (local vs HEAD) and live (local vs BD column)
- File-state tags:
NEW,DELETED,CHANGED,RENAMED
Drift detection
- One
SELECT id, name FROM <table>per pushed section compares live rows to local dirs - Surfaces
only_on_live,only_on_local,id_mismatch,duplicates_on_live,duplicates_on_local - One-click fixes:
pull-live,push-local,delete-live,delete-local,dedupe-live
Safety rails
- Local directory deletion requires explicit
confirm_delete: trueon the server side - Browser
confirm()prompt + red warning banner + red "🗑 Delete on live" button - Auto-commits are path-scoped (
git add -- <paths>), nevergit add -A, so unrelated edits aren't swept into a push commit - Refuses to push when the repo is mid-merge / mid-rebase (would otherwise silently fail)
Cross-platform Node only
- Pure Node.js 18+ (ES modules, native
fetch, nativeAbortController) - No bash / PowerShell / Python dependencies
- Works the same on Windows / macOS / Linux
Install
Prerequisites: Node.js 20+ and git. That's it. Verify:
node --version # v20.x or newer
git --versionThen install bd-manager globally via npm:
npm install -g bd-manager
bd --helpThat's the whole install. No repo to clone, no npm link, no environment setup. The bd command is now available in every terminal.
Why
-g? The-gflag putsbdon your system PATH. If you skip it,bdis only available vianpx bdfrom inside the install directory.
Permissions: on macOS / Linux you may need
sudo npm install -g bd-managerif your npm global directory isn't writable. The preferred fix is to configure npm to use a per-user prefix sosudois never needed.
First-time setup (60 seconds)
Three commands from any terminal, any directory. bd-manager remembers everything after the first run.
# 1. Sign in with GitHub
bd login
# → asks "Worker URL" with a default (press Enter to accept)
# → opens a browser for the GitHub device-flow code
# → stores the session token in your OS keychain
# 2. Decide WHERE you want the sites, cd there, and clone them all
mkdir ~/bd && cd ~/bd # or any path you prefer — e.g. cd ~/projects/bd
bd setup --clone-all
# → uses the CURRENT directory as the sites root (override: bd setup --clone-all /other/path)
# → clones bdtools.directoryup.com, pakbusinessworld.com, etc. here
# → configures each with BD credentials fetched from the worker
# → saves this location to your user config so `bd web` finds it from anywhere
# → you never type a site URL or API key by hand
# 3. Open the multi-site dashboard
bd web
# → http://localhost:4174That's it. After step 2, every bd command works from any terminal, any directory, any drive — the tool remembers the sites root, the worker URL, and your session in a user-scoped config file (not inside the install dir).
Where bd-manager stores things
| Thing | Location | Why there |
|---|---|---|
| Your site repos | Whatever directory you ran bd setup --clone-all in (or explicit bd setup --clone-all <path>) | Your choice — stored in config after first run |
| Config (sites root, default site, web port, worker URL) | Windows: %APPDATA%\bd-manager\config.jsonmacOS: ~/Library/Application Support/bd-manager/config.jsonLinux: ~/.config/bd-manager/config.json | Platform-standard user config dir — survives npm reinstalls, hidden from normal file browsing |
| Session token | OS keychain via keytar (Windows Credential Manager / macOS Keychain / libsecret) | OS-gated; never in a flat file on supported platforms |
| Runtime / cache | <config-dir>/runtime/ | Scratch space for long operations |
Nothing important is stored inside the bd-manager install directory, so npm update -g bd-manager or reinstalling the package never wipes your setup.
Subsequent runs
Once setup has run once:
bd web # open dashboard — session cached, root dir remembered, no re-login
bd whoami # show who you're signed in as + which sites you can access
bd sites # list every registered site + last-sync info
bd sync # force-refresh your session from the worker (picks up new perms immediately)
bd status --all # pending changes across every site
bd push --all # push every dirty site, halt safely on conflicts
bd logout # only if switching GitHub accountsSessions roll on a 30-day sliding TTL. If yours expires or gets revoked by an admin, any CLI command prompts you to sign in again and retries — no lost work.
Moving your sites to a different folder
Just run bd setup --clone-all /new/path again — or edit sites_root in the config file directly:
{
"sites_root": "/absolute/path/to/new/location",
...
}Then mv / rsync your existing site dirs over to the new path. bd-manager picks it up on the next command.
How the worker URL works
bd login with no argument suggests https://bd.devorahub.com as the default — the Devorahub-Metaviz deployment. Press Enter to accept, or type a different URL if your org runs its own worker.
Your choice is saved; subsequent bd login / bd sync / etc. reuse it silently.
Override for a fresh start:
bd login https://your-worker.example.com # explicit URL, replaces stored one
BD_WORKER_URL=https://… bd login # env var takes effect on first run onlyOnly your admin does this once — not you
bd-manager connects to a Cloudflare Workers backend that holds every site's encrypted BD credentials, mirrors GitHub org access, and runs the admin UI. Backend setup (GitHub App, deployment, secrets, org wiring) is a one-time task done by an admin from the private Devorahub-Metaviz/bd-manager-worker repo. Regular CLI users never touch it.
Adding a new BD site
Admin adds the repo + BD credentials at https://bd.devorahub.com/admin. Within seconds a webhook updates every logged-in user's session. On your next bd web load (or run bd sync immediately), the new site appears in the sidebar and bd setup --clone-all picks it up.
If you prefer to do everything by hand:
bd add # interactive: name, site URL, API key, optional git remote
# (uses local creds instead of the worker; fallback for air-gapped use)Directory layout
bd-manager lives in its own directory. Site content lives as siblings. Nothing is ever inside bd-manager's repo except the tool itself.
<parent-root>/
├── bd-manager/ ← this tool, its own git repo
│ ├── bin/bd.js ← CLI entry
│ ├── server/ ← Hono + Preact/htm + SSE dashboard
│ ├── src/ ← workflow modules, site-aware
│ ├── docs/ ← BD API reference, DB schemas
│ ├── php/bd-db-bridge.php ← bridge widget source (auto-deployed to each site)
│ ├── .bd-manager.json ← tool config
│ └── package.json
│
├── bdtools.directoryup.com/ ← site 1, separate git repo
│ ├── .bd-site.json ← site metadata (committed)
│ ├── .env.local ← BD_SITE_URL + BD_API_KEY (git-ignored)
│ ├── .gitignore
│ ├── widgets/ ← per-record dirs, code + meta.json per item
│ ├── pages/
│ ├── post-types/
│ ├── menus/, forms/, email-templates/, sidebars/, subscriptions/, seo-templates/
│ ├── settings/website_settings.json, settings/design_settings.json
│ ├── labels/labels.json
│ ├── redirects/, webhooks/, plugins/, categories/, workflows/, widget-placements/
│ └── .git/
│
├── anothersite.example.com/ ← site 2, another separate git repo
└── thirdsite.example.com/ ← site 3, dittoA directory is recognized as a registered site only if it contains .bd-site.json — an .env.local alone is not enough. This prevents any stray folder with a BD-looking env file from being treated as a site.
CLI reference
Site management
| Command | Purpose |
|---|---|
| bd sites | List every registered site |
| bd add [<name>] | Interactive: create a new site directory, init git, install bridge widget, snapshot, first commit, create bd-live, push both branches. Idempotent — safe to run on an existing site. |
| bd update <name> [--rename=…] [--remote=…] [--url=…] [--key=…] | Non-destructive. Each flag touches only what you provide. --rename renames the directory on disk. |
| bd remove <name> [--purge] | Drop from registry. --purge also deletes the directory. |
| bd default <name> | Set default site (used when <site> is omitted) |
| bd setup [<name>] | Re-runnable bootstrap — fills in missing pieces if present, adds if absent |
| bd remote <name> <url> | Set / change a site's git remote |
| bd remote-sync [<site>…] [--force] | Reconcile local git with origin. Fetch → fast-forward → rebase → (with --force) force-push. Auto-aborts any pending rebase/merge. |
Data flow
| Command | Purpose |
|---|---|
| bd snapshot [<site>] [--c=N] | Full fresh snapshot (concurrency N, default 30). --all for every site. |
| bd pull [<site>] [<section>] [<id\|name>] | Pull. Any omitted arg triggers an interactive picker. |
| bd push [<site>] [-m "<msg>"\|"<msg>"] | Push one site. Halts on conflict. If no site given, scans all sites — if exactly one is dirty, pushes it; if several, lists them and asks for --all. "Dirty" = working-tree changes or commits already on main that haven't been mirrored to bd-live yet (e.g. after a manual git commit). Custom commit message via -m / --message or as a bare positional. Deletions prompt for confirmation unless --yes / -y is passed. |
| bd push --all [-m "<msg>"] | Push every dirty site sequentially. Halts at the first conflict; other dirty sites stay for after you resolve and re-run. |
| bd push [<site>] --dry-run (-n) | Preview the push without mutating anything. Lists each item that would be pushed, each pending deletion, and each branch that would go to origin. Useful before --all. |
| bd rollback [<site>] [--count=N] [-m "<msg>"] | Revert the last N commits on main (default 1) and push the revert through the normal flow to bd-live + BD live + origin. Creates new revert commits — safe, no history rewrite. Prompts for confirmation unless --yes. |
Push message forms (all equivalent for providing a custom message):
bd push -m "fix: your message"bd push "fix: your message"(bare positional auto-detected as message if it doesn't resolve to a registered site)bd push <site> -m "fix: your message"bd push <site> "fix: your message"git add -A && git commit -m "fix: your message" && bd push(manual commit,bd pushpicks it up, mirrors tobd-live+ live)
Inspection
| Command | Purpose |
|---|---|
| bd status [<site>\|--all] | Local changes grouped by section/item |
| bd warnings [<site>\|--all] | Config / git / bridge health |
| bd sections | List all 20 known sections |
| bd test [<site>] [--get\|--crud\|--all\|--cleanup] | Live smoke-test via the bridge |
| bd test [<site>] --creation [--keep] | Create fresh local items per section, push, verify live + resync |
| bd test [<site>] --recreation | After manually deleting items from BD admin, edit one code part locally, push, verify re-creation |
Web
| Command | Purpose |
|---|---|
| bd web [--port=N] | Launch the multi-site dashboard at http://localhost:4174 |
Run bd help for the live version.
Web dashboard
Navigation is hash-based so every page is bookmarkable and the back button works:
#/ Home — site cards with per-site stats
#/all All-sites aggregate + Push all
#/site/<name> Site overview — section tiles
#/site/<name>/section/<key> Items list + drift panel
#/site/<name>/section/<key>/item/<item> Item viewer — file tree + code preview
#/site/<name>/changes Pending changes as rich cards
#/site/<name>/warnings Health panel
#/site/<name>/activity Live logSidebar — site list with pending-change badges, Add Site button, per-site nav.
Home — rich cards per site: total local items, pending count, top sections, remote status.
Overview — tiles for each section with local item count + pending count. Click a tile to drill in.
Section view — searchable items list. Simple-section rows are expandable to show secondary fields. Per-record rows navigate to the item viewer. "Scan drift" button + drift panel with action buttons.
Changes tab — one card per item with pending files. Click anywhere on a card → 90vw × 90vh diff modal (side-by-side or unified). Per-file chips with NEW / DEL / MOD badges, compare button, push-file button. State banners for deleted / new items.
Compare modal — two modes:
- git: local working tree vs last commit (for change cards)
- live: local file content vs BD live column (for item pages)
File headers tag each file as ADDED, DELETED, CHANGED, or RENAMED.
Add / Edit site modal — 4 inputs (directory name, site URL, API key, optional git remote). URL hostname is auto-used as default directory name.
Activity log — live SSE feed with a 300-item ring buffer on the server, so activity persists across page reloads.
How the workflows work
Pull flow (per site)
1. ensure bd-live branch exists (create if missing)
2. checkout bd-live
3. pullSection(s) — one request per section / item via bridge SELECT
4. if uncommitted changes on bd-live: commit "pull from live … — <scope>"
5. if remote: push bd-live to origin
6. checkout main
7. merge bd-live ← conflicts? halt, surface them
8. if remote: push main to originPush flow (per site)
1. scan pushable changes = working-tree edits ∪ commits on main not yet on bd-live
(so a manual `git commit` still triggers the live + remote push)
2. compute reconcile plan — skip items with no local id or id missing on live
(they'll be INSERTed by pushToLive directly, not reconciled)
3. if working-tree changes exist: git add -A && git commit
- use -m/--message or bare positional message if provided
- else auto-generate a detailed message listing <site> ← <sections/items/files>
- if no working-tree changes but -m was supplied: `git commit --amend -m "<msg>"`
4. pull --rebase origin main ← avoids non-fast-forward at step 10
5. checkout bd-live
6. pull each reconcile-scoped item from live
7. commit + push bd-live
8. checkout main, merge bd-live ← conflicts? halt, surface them
9. pushToLive per changed item
- id matches live → UPDATE
- id doesn't match but name does → UPDATE (adopt)
- neither → INSERT with template-fill
- dir deleted + confirm_delete → DELETE (+ users_meta cleanup)
10. refresh BD cache
11. commit resync on main (new ids, date_updated, revision_timestamp)
12. push main to originThe reconcile plan is computed from main's state before switching branches, so items that look "new or recreated" skip the bd-live pre-pull (which would otherwise write live's old state and clash with the user's intent).
Drift detection
Triggered automatically after every successful push-item for any per_record section, and on-demand via the "Scan drift" button in the Section view.
SELECT widget_id, widget_name FROM data_widgets -- one query, no LIMITClassifies every row:
| Classification | Meaning | Fix actions available |
|---|---|---|
| matched | Both sides agree on id + name | — |
| id_mismatch | Same name, different id | pull-live (adopt live's id) |
| only_on_live | Row on BD, no local dir | pull-live, delete-live |
| only_on_local | Dir on disk, no row on BD | push-local, delete-local |
| duplicates_on_live | Same name on multiple live rows | dedupe-live (keep lowest id) |
| duplicates_on_local | Same name in multiple dirs | manual (rare) |
Cost is ~1 bridge call per section scan. Scaling to 1000s of rows stays a constant cost.
Deletion
Client deletes widgets/foo/ locally → git reports all its files as D.
bd pushvia CLI:pushToLivesees the missing dir, reads the last committed meta viagit show HEAD:…(falls back toHEAD^,HEAD~2), runsDELETE FROM <table>+ cleansusers_meta.bd webor/api/sites/:n/push-item: returns HTTP 409requires_confirmation: "delete"unlessconfirm_delete: trueis passed. The UI detects the deletion and shows awindow.confirm()before re-sending with the flag.
Fallback path — if the git history lookup fails (e.g., item was never committed), the handler queries SELECT id FROM <table> WHERE name = ? using the dir name.
Supported sections
Twenty BD tables are currently mapped. Add a new section = add one entry in src/sections.js — every workflow picks it up.
| Kind | Key | Table | Notes |
|---|---|---|---|
| simple | labels | website_labels | 759+ rows |
| simple | website_settings | website_settings | 322 rows |
| simple | design_settings | website_design_settings | 778 rows |
| simple | redirects | `301_redirects` | table name needs backticks |
| simple | webhooks | bd_webhooks | |
| simple | plugins | plugin_records | |
| simple | categories | list_professions | bridge-only (API 405s) |
| simple | services | list_services | |
| simple | category_groups | category_group | |
| simple | data_flows | data_flows | |
| simple | widget_placements | data_widgets_rel | |
| per_record | subscriptions | subscription_types | |
| per_record | seo_templates | list_seo_template | |
| per_record | sidebars | sidebars | |
| per_record | email_templates | email_templates | body.html |
| per_record | pages | list_seo | 7 code files per page |
| per_record | widgets | data_widgets | html.php, style.css, script.php |
| per_record | post_types | data_categories | 8 code files per post type |
| menus | menus | menus + menu_items | |
| forms | forms | forms + form_fields | bridge-only (API is broken) |
per_record sections store each row as a folder containing code files (split by column mapping) + meta.json for all non-code fields. simple sections store the whole table as a JSON array. menus and forms have special handling for their 2-table structures.
Configuration files
bd-manager/.bd-manager.json (committed)
{
"sites_root": "..",
"default_site": "bdtools.directoryup.com",
"web": { "port": 4174 },
"concurrency": 30
}<site>/.bd-site.json (committed to the site's own repo)
{
"name": "bdtools.directoryup.com",
"site_url": "https://bdtools.directoryup.com",
"remote": "[email protected]:org/bdtools.git",
"branches": { "main": "main", "live": "bd-live" },
"created": "2026-04-18"
}<site>/.env.local (git-ignored, never committed)
BD_SITE_URL=https://bdtools.directoryup.com
BD_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThe bridge widget
All SQL operations on BD go through a single PHP widget — bd-db-bridge — deployed at /api/widget/get/json/bd-db-bridge on each BD site. Its source is php/bd-db-bridge.php in this repo. bd add and bd setup auto-install it via BD's widget API the first time.
Actions:
| action | Purpose |
|---|---|
| query | SELECT / SHOW / DESCRIBE — read-only |
| execute | INSERT / UPDATE / DELETE — blocks DROP, TRUNCATE, ALTER, RENAME, GRANT, REVOKE |
| tables | list all tables |
| columns | describe a table |
Authentication validates the request's API key against the bd_api_keys table using plain / md5 / sha256 / bcrypt matching.
HTTP API
The local web server (bd web) exposes these endpoints. Use them from external tools or scripts.
Manager
GET /api/health—{ ok, version, build, time }GET /api/manager— tool config + sites rootGET /api/sections— all known section definitions
Sites
GET /api/sites— list of sites + pending-change counts + defaultGET /api/sites/:name— single site summary + warnings + changesGET /api/sites/:name/overview— rich overview (per-section counts, totals)POST /api/sites— add site (body:name, siteUrl, apiKey, remote)PATCH /api/sites/:name— update metadata (rename, url, key, remote)DELETE /api/sites/:name?purge=1— remove from registry (optionally delete directory)POST /api/default—{ name }— set default site
Data flow
POST /api/sites/:name/snapshot— kick off full snapshotPOST /api/sites/:name/pull— pull (body:section?, item?)POST /api/sites/:name/push— full push flow for this sitePOST /api/push-all— sequential multi-site push, halt on first conflictPOST /api/sites/:name/push-item— push one item (body:section, item). Returns 409requires_confirmation: "delete"if item dir is gone; retry withconfirm_delete: true.POST /api/sites/:name/push-file— single-column UPDATE (body:section, item, file). Falls back to full push-item if id is missing.POST /api/sites/:name/pull-item— pull one item without the full git dance
Navigation
GET /api/sites/:name/section/:section/items— all local items in a section with metaGET /api/sites/:name/section/:section/item/:item— item meta + all file contents
Diff
GET /api/sites/:name/diff?path=<rel>— git-style unified diff (local vs HEAD, or vs/dev/nullfor untracked). Returnsstate: "new" | "deleted" | "modified".POST /api/sites/:name/diff-live—{ section, item, file }→ unified diff of local file vs BD live column
Drift
GET /api/sites/:name/section/:section/drift— one SELECT per section, classifies all rowsPOST /api/sites/:name/section/:section/drift/fix—{ action, target }— apply one ofpull-live,push-local,delete-local,delete-live,dedupe-live
SSE
GET /api/stream— server-sent events with replay of the last 300 events (so a page reload picks up recent activity)
Troubleshooting
git pull from the parent root fails — "not a git repository"
The parent directory isn't a repo. Each site has its own .git. cd into <site>/ first, or just use bd push <site> which handles this correctly.
Push main rejected — "non-fast-forward"
The remote moved ahead of you. bd push will auto pull --rebase on your next run if the histories share an ancestor. If the histories are unrelated (you reused an existing repo with different content), run once:
bd remote-sync <site> --forcebd push produces no output
Your site repo is probably mid-rebase or mid-merge from an earlier aborted operation. bd now detects this and refuses to proceed silently, but older data might still be stuck. Run:
cd <site-dir>
git status
git rebase --abort # or merge --abort / cherry-pick --abortBrowser shows old UI / old button labels
The server sends no-cache headers and stamps ?v=<buildStamp> on every asset URL, but one hard refresh (Ctrl+F5) is needed to load the first post-upgrade page. The build stamp in the sidebar (Manager · v0.3.0 · build XYZ) should match GET /api/health → build — that confirms you're on fresh JS.
Can't delete a widget from the web UI
The Delete button on deleted-locally cards shows a browser confirm() prompt first. After confirming, the server receives confirm_delete: true and proceeds. Without confirmation, you'll see a toast saying the request was refused (HTTP 409 by design).
Widget dir with trailing space breaks git on Windows
BD sometimes stores widget names with trailing whitespace. The slug sanitizer in src/format.js (via sanitize-filename) strips these automatically on pull. If you already have offending dirs on disk, the cleanup script in our repo history (scripts_tmp_rename.mjs) shows the UNC-path rename pattern.
Development
Stack
- Node.js 18+ (ES modules, native fetch)
- Hono +
@hono/node-server— local dashboard server - Preact + htm via esm.sh — zero-build UI
- diff2html — GitHub-style diff rendering
- chokidar — file watcher
- dotenv — env file parsing
- sqlstring — MySQL-safe escaping
- sanitize-filename — cross-platform safe dir names
- prompts — interactive CLI
- mri — argv parser
- js-beautify — HTML/CSS/JS formatting on pull
- diff — unified-diff generation for live-compare
Running from source
npm install
node bin/bd.js help # or: `bd help` after `npm link`
node bin/bd.js web # starts the dashboardProject structure
src/
bridge.js bd-db-bridge client (POST /api/widget/get/json/bd-db-bridge)
api.js BD REST API v2 client (used for widget create/update)
http.js thin fetch wrapper
git.js all git commands, cwd-bound to a SiteContext
config.js per-site env loader (.env + .env.local)
manager-config.js tool-level config (.bd-manager.json)
site.js SiteContext class + metadata read/write
sites-registry.js discover / resolve / set default site
sections.js the 20-section registry
pull.js per-section pullers, users_meta merge
push-live.js pushToLive + pushFileToLive + deletion paths
diff.js scanChanges (git porcelain → bySection → items)
drift.js computeDrift + applyDriftFix
fs-utils.js saveJson/readJson/splitRecord/mergeRecord
format.js slugify / safeDirName / js-beautify
workflow.js runSnapshotFlow / runPullFlow / runPushSiteFlow / runMultiSitePush
site-actions.js runAddSite / runUpdateSite / runRemoveSite / runSetup
remote-sync.js runRemoteSync (branch-by-branch reconcile)
bridge-installer.js auto-install the bd-db-bridge widget on live
warnings.js per-site health checks
interactive-pull.js 3-step picker (site → section → item)
pool.js concurrency limiter
events.js event bus + 300-item SSE replay buffer
log.js colored CLI output
prompt.js wrapping prompts lib
paths.js path constants
test-runner.js --get + --crud smoke tests
test-creation.js --creation test harness
test-recreation.js --recreation test harness
server/
index.js Hono app + SSE + chokidar watcher
ui/
index.html
styles.css
main.js single-file Preact app (Home/Site/Section/Item/Changes/Warnings/Activity)
bin/bd.js CLI entry
docs/ BD API reference, DB schemas (read by pull logic)
php/bd-db-bridge.php bridge widget sourceAdding a new section
Open src/sections.js and append to SECTIONS:
my_new_section: {
kind: "per_record",
table: "bd_some_table",
idField: "row_id",
nameField: "row_name",
listSql: "SELECT row_id, row_name FROM bd_some_table ORDER BY row_id",
dir: "my-new-section",
slugFn: "identity",
mappings: [
["some_code_column", "code.php"],
],
mergeMeta: true,
},Also add it to DISPLAY_ORDER to control where it shows on the Site overview. Run bd pull <site> my_new_section and it'll create the folder.
Security notes
.env.localis in.gitignoreby default, both forbd-managerand each site. Never commit your API keys.- The bridge widget blocks dangerous SQL (
DROP,TRUNCATE,ALTER,RENAME,GRANT,REVOKE) at the server side. - All user input flowing into SQL goes through
sqlstring(MySQL-compatible escaping, same lib used bymysql2). - The local dashboard binds to
127.0.0.1only — it's a localhost-only tool, not a public service. - Dir names are sanitized via
sanitize-filenamebefore hitting disk (Windows reserved names, control chars,<>:"/\|?*). push-itemandpush-fileauto-commit only the path-scoped files they just pushed — nevergit add -A— so unrelated local edits aren't swept into the commit.- Deletions require a separate explicit confirmation (
confirm_delete: true) on the HTTP path. Without it, the server returns HTTP 409.
License
MIT (or your choice — adjust as needed).
