CLI tool for sync.danholloran.me
npm install -g @markpost/cliOnce installed, run the CLI with the markpost command.
Run markpost help (or markpost --help / -h) to see aggregated usage for
every command. A bare markpost with no arguments prints that help and exits
non-zero — it does not sync, so an accidental invocation can't delete
server-side records.
| Command | Description |
|---|---|
markpost sync [--dry-run] |
Fetch all pending records, write each to a markdown file, and (when autoDelete is enabled) delete the written records from the server. --dry-run reports the exact write/delete plan without writing or mutating anything |
markpost push [--dry-run] <path...> |
Create records from one or more markdown files, directories, or glob patterns. --dry-run reports which files would be pushed, plus any missing or unreadable inputs, without creating any records |
markpost get <uuid> [--json] |
Fetch and display a single record; pass --json for machine-readable output |
markpost sources <list|create|update|delete|rotate-secret> [uuid] [--yes] |
Manage sources; sources list --json prints machine-readable output. sources delete asks to confirm first (deleting a source is irreversible — it drops the ingest config and one-time signing secret) and needs an interactive terminal; in scripts pass a uuid with --yes (sources delete <uuid> --yes) to skip the prompt. rotate-secret [uuid] mints/replaces the signing secret of a provider source (github/zapier/shortcuts reveal a fresh secret once; stripe prompts for the new value) |
markpost records list [--source <type>] [--status <status>] [--search <text>] [--json] |
List records without deleting them, optionally filtered by source, status, or search text; pass --json for machine-readable output |
markpost config <get|set|path> [key] [value] |
View or change the stored API token and output directory |
markpost settings <get|set> [key=value ...] |
View or change server-side sync settings (autoSync, autoDelete, frontmatter, conflictStrategy) |
markpost help |
Show aggregated usage |
The destructive fetch/write/delete sync runs only under the explicit
markpost sync command.
Commands that accept --json (get, sources list, records list) emit a
single, uniform failure shape so a script can parse any failure the same
way. On failure the CLI writes nothing to stdout (stdout stays the clean
--json | jq data channel), sets a non-zero exit code, and prints one JSON
object to stderr:
{ "error": "<code>", "message": "<human-readable explanation>" }error is one of a small, stable set of machine-readable codes:
error code |
When it happens |
|---|---|
config_required |
A required value (API token or output directory) is not configured and --json mode will not prompt. Also includes a missing field naming the config key. |
usage |
A bad or missing argument/subcommand, or --json passed where it is not supported. |
fetch_failed |
The requested operation could not be completed (a failed or empty fetch, or an error thrown while carrying it out — e.g. an auth/5xx failure). |
Any string in message that is server-derived is sanitized so it cannot inject
a live terminal escape sequence. Additional fields (such as missing) may
appear alongside error and message depending on the code; consumers should
key off error and ignore unknown fields.
markpost sync writes your records to OUTPUT_DIRECTORY, honoring your
markpost account settings:
autoSync— when on (markpost's default), the sync does not exit after one pass: it self-schedules and re-runs every 5 minutes, staying in the foreground until you stop it withCtrl-C. A one-line banner announces this at startup. When off,markpost syncsyncs once and exits. Records already written during a session are not re-written on later iterations; a record edited on the server after it was synced is not re-fetched until you restart the process (the record contract carries no mutation timestamp to detect the edit). Each iteration fetches only records stillpendingon the server (withautoDeleteoff, written records are markedsyncedso later passes skip them), so the per-interval fetch cost tracks your outstanding backlog rather than growing with your full history.autoDelete— when on (markpost's default), records written locally are deleted from the server after a successful write; when off, they stay on the server. If a delete fails, the record is retried on the nextautoSynciteration rather than abandoned.frontmatter— when on (markpost's default), synced files include a YAML frontmatter block. When off, records that carry markpost metadata are written with just their# Titleheading and body; records with no metadata (e.g.markpost pushcreated) are written as bare content either way.conflictStrategy— how same-name files are handled (suffix,overwrite, orskip).
Run markpost sync --dry-run to preview a sync before committing to it: it
fetches your pending records and reports the exact plan — which files would be
written (and whether each is a fresh write, an overwrite, or a skip under your
conflictStrategy) and which records would be deleted or marked synced on the
server — without writing any file, creating the output directory, deleting, or
marking anything. A dry run previews once and never self-schedules, even with
autoSync on.
These settings live on your markpost account. View or change them from the CLI
with the settings command (each set field is a key=value pair; pass more
than one to change several at once):
markpost settings get # print current settings
markpost settings set autoDelete=false # change one field
markpost settings set autoSync=false conflictStrategy=overwrite # change severalset validates every field name and value against markpost's contract before
sending, so a typo'd key or off-contract value fails locally rather than
silently doing nothing.
The CLI stores your API token and output directory in a conf file on disk.
On first run it prompts for anything missing. Use the config command to
inspect or change those values afterwards without hand-editing the file:
markpost config get # show all stored config
markpost config get apiToken # show one value
markpost config set apiToken <token> # change the stored API token
markpost config set outputDirectory '~/notes' # quote it: a leading ~, $HOME, or ${HOME} is expanded when writing
markpost config path # print the config file locationThe stored API token is a secret, so config get never prints it in full: it
shows only the first and last four characters (e.g. sk_a****wxyz), and fully
masks tokens too short to redact safely. The output directory is a plain path
and shown in full.
Note that config set apiToken <token> puts the token in your shell history.
Prefer a leading space (with HISTCONTROL=ignorespace, or setopt HIST_IGNORE_SPACE in zsh) to keep it out, or set API_TOKEN in the
environment instead.
- Node.js
- npm
git clone https://github.com/neonpixels-studio/markpost-cli.git
cd markpost-cli
npm installCopy .envrc and populate your values. If you use direnv, run direnv allow to load them automatically.
| Variable | Description |
|---|---|
API_TOKEN |
API token for sync.danholloran.me |
BASE_URL |
Base URL of the sync API (e.g. http://localhost:8888 for local dev) |
OUTPUT_DIRECTORY |
Path to the directory where synced files are written; a leading ~, $HOME, or ${HOME} is expanded to your home directory. A relative path is resolved against the current working directory, so prefer an absolute path or a ~ prefix for scheduled runs |
| Command | Description |
|---|---|
npm run build |
Compile TypeScript to dist/ |
npm run watch |
Watch and recompile on changes |
npm test |
Run tests with Vitest |
npm run test:ci |
Run tests once (CI mode) |
npm run test:ui |
Run tests with Vitest UI |
npm run lint |
Check formatting and linting |
npm run lint:fix |
Auto-fix formatting and linting issues |
npm run sync:contract |
Refresh the vendored markpost API contract (see below) |
npm run sync:markdown-serialization |
Refresh the vendored markpost serialization slice (see below) |
The CLI talks to markpost's API, so its
request/response types need to match markpost's real contract exactly — a
structural mismatch here previously caused real pagination and
error-swallowing bugs. Instead of hand-mirroring markpost's types (which drift
silently), src/types/vendor/markpost-api.types.ts is a vendored, verbatim
copy of markpost's server/types/api.types.ts, and src/types/api.types.ts
re-exports the generic envelope types (ApiError, ApiRequest,
ApiResourceObject, ApiResponse) from it.
- Refreshing it: run
npm run sync:contract(optionally-- --from <path-to-a-local-markpost-checkout>; without--fromit shallow-clones markpost fresh). This is a human-run step, not part of CI — it needs network access (or a local checkout) to fetch the current contract, and a test that depends on network access would be flaky and fail offline. Review the resulting diff, runnpm run buildandnpm test, then commit it like any other change. - Catching drift:
tests/types/contract-drift.test.tsruns on everynpm test/npm run test:ciand fails if either (a) the committed vendored file stops exporting the type names the CLI depends on, or (b) the CLI's ownsrc/no longer compiles against it (it recompiles the real project with the TypeScript compiler API, usingtsconfig.jsondirectly — not a hand-written stand-in). No network access, no CI workflow changes needed. - Wiring into CI: this is already covered by the existing
npm test/npm run test:ciinvocation in your CI workflow — no new step is required. If you want an explicit, separate CI signal for contract drift specifically (e.g. to label it distinctly in the checks UI), add:after your existing install step.- name: Check markpost contract drift run: npx vitest run tests/types/contract-drift.test.ts
- What this does not do: it does not detect when markpost's real
upstream contract has changed and the vendored copy has fallen behind — that
would require network access at test time (flaky, and fails offline CI).
Re-run
npm run sync:contractperiodically or whenever a markpost API change is suspected.
The CLI writes synced records to disk as markdown, and those files must be
byte-identical to what markpost itself would write — otherwise a markpost push re-wraps or corrupts them. markpost's server/utils/markdown.ts is the
source of truth for that format (quoteYamlScalar, serializeTagsLine,
serializeFrontmatter, assembleMarkdownDocument); src/libs/frontmatter.ts
hand-mirrors it. That mirror used to have zero automated guard — a change to
markpost's quoting or block layout would silently corrupt synced files with no
test failing. This closes that gap the same way the contract sync does.
- Refreshing it: run
npm run sync:markdown-serialization(optionally-- --from <path-to-a-local-markpost-checkout>; without--fromit shallow-clones markpost fresh). Like the contract sync this is a human-run step, not part of CI — it needs network access (or a local checkout). It extracts just the serialization slice ofmarkdown.ts(the four functions above plus the two types they use, leaving the turndown-dependent server code behind) and writes it verbatim totests/libs/vendor/markpost-markdown-serialization.generated.ts, alongside a manifest recording the exact source commit. The file lives undertests/so it never ships in the publisheddist/. Review the diff, runnpm test, then commit. - Catching drift:
tests/libs/frontmatter-drift.test.tsruns on everynpm test/npm run test:ci. It executes markpost's real (vendored) serialization functions and the CLI's mirrored ones over a shared battery of inputs — plain values, empty and multi-tag lists, every YAML metacharacter, whitespace, and escape sequences — and fails if any input serializes differently. No network access needed. When markpost's serialization changes, re-run the sync: the vendored slice updates, and if the CLI mirror has not been updated to match, this test goes red. - What this does not do: it does not detect when markpost's upstream
serialization has changed and the vendored slice has fallen behind — that
would require network access at test time. Re-run
npm run sync:markdown-serializationwhenever a markpost markdown change is suspected.
This repo runs a deterministic security-scanner layer in two places: a local pre-commit hook and GitHub Actions CI.
gitleaks scans for committed secrets.
The ruleset lives in .gitleaks.toml: it extends the gitleaks
default rules and adds custom rules for Clerk secret keys (sk_live_ /
sk_test_) and Postgres/Neon connection strings that embed credentials. Example
and test-fixture files are allowlisted.
- Locally, the
.husky/pre-commithook runsgitleaks git --stagedand blocks the commit on any finding. Install gitleaks to enable it (brew install gitleaks, or see the install docs). If gitleaks is not installed the hook prints a notice and continues — CI still enforces the scan, so nothing slips through. - In CI, the
gitleaksjob in.github/workflows/security.ymldownloads the pinned gitleaks release and scans the pull-request commit range on PRs and the full history on push tomain. Any finding fails the build.
The dependency-audit job in the same workflow runs npm audit. Moderate and
low advisories are printed as a summary; the build fails only on high or
critical severity. .github/dependabot.yml opens
weekly dependency-update PRs, grouping minor and patch bumps into a single PR.