Bob Plugins¶
bob plugins manages Bryan's custom Bob Obsidian plugins from the
bobs-org/bob-plugins repo, which
is the source of truth for the custom plain-JavaScript community plugins.
Typical order: bob plugins (or bob plugins list) to see repo versions
versus the vault, bob plugins sync --dry-run to preview, then
bob plugins sync to deploy. The repo holds one folder per plugin under
plugins/<id>/, each with a manifest.json, a main.js, and an optional
styles.css. Do not edit installed copies under ~/bob/.obsidian/plugins/ as
the source of truth โ those files are overwritten on the next sync.
Contents¶
Commands¶
bob plugins [-b|--bob-dir DIR] [-f|--format table|json] [-n|--no-pull] [-r|--repo DIR]
bob plugins list [-b|--bob-dir DIR] [-f|--format table|json] [-n|--no-pull] [-r|--repo DIR]
bob plugins sync [-B|--backup-dir DIR] [-b|--bob-dir DIR] [-d|--dry-run] [-F|--force] [-f|--format table|json] [-n|--no-pull] [-p|--plugin ID] [-r|--repo DIR]
list is read-only. Running bob plugins with no subcommand runs list with
the same options. sync deploys the repo into the vault (see Sync).
Repo Refresh¶
Before list or sync analyzes plugin files, it runs a non-interactive
git pull in the plugins repo so the checked-out files reflect the latest
committed source. Pass -n, --no-pull to skip this refresh and use the current
checkout exactly as-is.
A repo path that is not a Git worktree, or a system without git, skips the
refresh silently. If git pull fails because the network is unavailable,
authentication would require a prompt, histories diverged, or local edits block
the pull, the command writes a warning to stderr and continues with the
existing checkout. Pull output never goes to stdout, so table and JSON output
stay parseable.
Discovery¶
list reads the repo's plugins/ directory and builds one row per plugin
folder. The plugin id, version, and description come from that folder's
manifest.json; an empty or absent manifest id falls back to the folder name.
A folder whose manifest.json is missing or unparseable is reported as an error
on stderr and the command exits non-zero, but the remaining plugins still list.
Two roots feed the report:
- Repo root. Resolves from
-r, --repo, then theBOB_PLUGINS_DIRenvironment variable, then the default~/projects/github/bobs-org/bob-plugins. Plugins live under<repo>/plugins/<id>/. - Vault root. Resolves from
-b, --bob-dir, thenBOB_DIR, then~/bob. Installed plugins live under<bob-dir>/.obsidian/plugins/<id>/, and the enabled set is read from<bob-dir>/.obsidian/community-plugins.json.
Columns¶
| Column | Source |
|---|---|
PLUGIN |
manifest id (repo folder name when the manifest omits it) |
VERSION |
manifest version |
SYNC |
repo files vs. vault files |
VAULT |
community-plugins.json plus the installed-folder check |
DESCRIPTION |
manifest description, truncated to the remaining terminal width (COLUMNS when it is a positive integer, otherwise 100 columns) |
SYNC state¶
SYNC byte-compares the managed files โ manifest.json, main.js, and
styles.css when the repo has one โ against the vault copy:
syncedโ every managed repo file is present and byte-identical in the vault.driftโ the vault has the plugin folder, but at least one managed file is missing or differs.missingโ the vault has no folder for this plugin.
Only the managed files are compared. Runtime files such as data.json are
never read.
VAULT state¶
VAULT reports the plugin's enable state in the vault:
enabledโ the id is listed incommunity-plugins.json.disabledโ the plugin folder exists in the vault but the id is not enabled.not installedโ the vault has no folder for this plugin.
A missing or unreadable community-plugins.json is treated as "nothing
enabled" rather than an error, so installed plugins then read as disabled.
Header and Footer¶
The header names the repo and the plugin count, such as
Bob Plugins ยท 6 ยท /home/bryan/projects/github/bobs-org/bob-plugins. The
footer summarizes the sync states, such as
6 synced ยท 0 drift ยท 0 not installed. On a non-color or piped stream the
separator renders as - and the colored state glyphs are dropped.
Exit Status¶
list exits 0 even when plugins drift or are not installed โ those are
reportable states, not failures. It exits 1 only on a real error, such as an
unreadable repo plugins/ directory or an unparseable manifest, and writes the
error to stderr.
JSON Output¶
-f, --format json prints a single stable object for scripting:
{
"ok": true,
"repo": "/home/bryan/projects/github/bobs-org/bob-plugins",
"bob_dir": "/home/bryan/bob",
"count": 6,
"synced": 6,
"drift": 0,
"not_installed": 0,
"plugins": [
{
"id": "block-id-prompt",
"version": "1.0.0",
"description": "Prompt for a custom block ID when a wiki block link uses the ^^ marker.",
"sync": "synced",
"vault": "enabled"
}
]
}
The sync field is synced, drift, or missing; the vault field is
enabled, disabled, or not_installed. On error, JSON mode prints
{"ok": false, "error": "..."} instead.
Sync¶
sync deploys the repo into the vault. For each plugin it copies the managed
files โ manifest.json, main.js, and styles.css when the repo has one โ
from <repo>/plugins/<id>/ into <bob-dir>/.obsidian/plugins/<id>/. Runtime
files such as data.json are never read or written, so plugin settings survive
a sync. The repo and vault roots resolve exactly as they do for list.
The bob-cli just install-all command runs this sync against the sibling
bob-plugins checkout after pulling it. It previews the sync with
bob plugins sync --dry-run --format json to decide whether to restart
Obsidian and, on macOS, posts a notification before restarting Obsidian.
Before any existing vault file is overwritten, sync copies the current vault
file to a timestamped backup directory. Backups default to
~/.local/state/bob-cli/plugin-backups/<timestamp>/<plugin-id>/<file>, outside
the vault Git repo. The base directory resolves from -B, --backup-dir, then
BOB_PLUGIN_BACKUPS_DIR, then the default above. If a backup cannot be written,
that file is not overwritten and the command exits non-zero.
For every changed text file, sync prints a unified diff from the current vault
copy to the incoming repo copy. Non-UTF-8 or large minified files are summarized
by byte size instead of dumping unreadable content. New files get a compact
(new file, N lines) note because no existing vault content is at risk.
For every managed file, sync reports one of:
copied <file>โ the vault file was missing ((new)) or differed and was rewritten from the repo. Existing overwritten files are backed up first, and the backup path is printed.up to dateโ every managed file already matched the repo byte-for-byte, so nothing was written.skipped <file> (dirty in vault; use -F/--force)โ the vault file differs and has uncommitted changes in the vault Git repo, so it was left untouched. The diff is still printed so you can decide whether to rerun with--force.
Options¶
-B, --backup-dir <DIR>writes overwrite backups under this directory instead ofBOB_PLUGIN_BACKUPS_DIRor~/.local/state/bob-cli/plugin-backups.-b, --bob-dir <DIR>selects the vault root.-d, --dry-runpreviews every action, including diffs and backup paths, without writing vault files or backups.-F, --forceoverwrites a vault file even when it has uncommitted Git changes.-f, --format <FORMAT>istable(default) orjson.-n, --no-pullskips the defaultgit pullbefore analyzing the plugins repo.-p, --plugin <ID>syncs a single plugin instead of all of them. An id that the repo does not contain is an error.-r, --repo <DIR>selects the plugins repo root.
Foreign-checkout guard¶
A bare sync (no --repo) deploys the resolved repo root (--repo, then
BOB_PLUGINS_DIR, then ~/projects/github/bobs-org/bob-plugins). When the
current directory sits inside a different bob-plugins checkout โ for
example a SASE linked worktree โ the bare sync would silently deploy the
resolved checkout instead of the worktree, rolling back whatever the
worktree just deployed. So the sync refuses before any pull or copy (exit 2,
in --dry-run too) and prints the --repo command to run instead:
bob plugins sync --repo <checkout> [-p <id>]
A checkout counts as a bob-plugins checkout when its origin remote points
at bobs-org/bob-plugins, or when its root holds a plugins/ dir plus the
monorepo package.json. An explicit --repo, a bare sync from inside the
resolved checkout itself, and a bare sync from outside any bob-plugins
checkout all behave as before. From a SASE worktree, deploy with
bob plugins sync --repo <worktree> -p <id> to move one plugin.
Dirty-file guard¶
Before overwriting an existing vault file that differs from the repo, sync
runs git status --porcelain on it. If the file has uncommitted changes it is
skipped with a warning so local edits are never clobbered silently; pass
-F, --force to overwrite anyway. A vault that is not a Git repo has no
committed state to protect, so the copy proceeds. A file that already matches
the repo is reported as unchanged and never triggers the guard.
Dry-run mode follows the same decision path and prints the same diffs, but it
uses would copy and would back up to wording and writes nothing. This makes
bob plugins sync -d the review step before a real sync.
Exit status¶
sync exits 0 even when it skips dirty files โ a refusal is a deliberate
warning, not a failure, matching how list treats drift. It exits 1 only on a
real error such as an unreadable repo, an unknown --plugin id, or a failed
copy, and writes the cause to stderr. A refused bare sync from a foreign
checkout exits 2 (see Foreign-checkout guard).
JSON output¶
-f json prints a single object and no table or diffs. Pull diagnostics stay
on stderr.
{
"ok": true,
"dry_run": false,
"repo": "/home/bryan/projects/github/bobs-org/bob-plugins",
"bob_dir": "/home/bryan/bob",
"copied": 1,
"skipped": 0,
"unchanged": 5,
"plugins": [
{
"id": "bob-project-tasks",
"files": [
{ "name": "manifest.json", "action": "unchanged", "backup": null },
{
"name": "main.js",
"action": "updated",
"backup": "/home/bryan/.local/state/bob-cli/plugin-backups/20261004-120000/bob-project-tasks/main.js"
}
]
}
]
}
action is created, updated, forced, unchanged, skipped_dirty, or
failed. backup is the backup path when the file is backed up (or, in a dry
run, would be), otherwise null. In a dry run, copied counts the files that
would be copied. On error, JSON mode prints {"ok": false, "error": "..."}
instead of echoing issues to stderr.
Examples¶
bob plugins
bob plugins list
bob plugins list -f json
bob plugins list -b ~/bob -r ~/projects/github/bobs-org/bob-plugins
bob plugins sync --dry-run
bob plugins sync -d -f json
bob plugins sync --no-pull --dry-run
bob plugins sync -p bob-project-tasks
bob plugins sync -F -b ~/bob -r ~/projects/github/bobs-org/bob-plugins