@guinetik/cardstock-cli
v0.7.0
Published
Command-line companion for Cardstock
Readme
Cardstock CLI
Command-line companion for Cardstock. Initialize tracker configuration, validate Markdown offline, preview changes, and sync Markdown with the board using explicit conflict resolution and recovery. Apply requires a server deployed with sync protocol 5 and its database migrations.
Requires Node.js 22 or newer. Bun is only needed by package developers.
Features in 0.7.0
Sync learns a board's tags and lanes instead of rejecting them: a tag or lane the
board already declares gets written back into cardstock.json on the next sync, so
the tracker's scheme catches up to the board without a manual edit. A card downloaded
from the board keeps the tracker's own spelling for its tags, not the board's.
A single invalid card no longer holds up the whole board: sync skips that card and
applies the rest, and both status and sync --dry-run name it under skipped
before any write happens. --dry-run validates the same content a real sync would
write, not just the tracker as it sits on disk, so a preview catches what a write
would actually do.
Previous releases
Version 0.6.0 adds single-card sync: cardstock sync --card 19 (or
status --card 19) reads, validates, uploads/downloads and checkpoints only that
card, leaving unrelated changes and conflicts pending; JSON reports card: "19" and
scopes clean/remaining to it. Recovery backups moved under .cardstock/ instead
of beside Markdown files. Deploy migration 20260921000000_cli_single_card_sync.sql
for per-card snapshots; older protocol-5 servers still work but return full board
snapshots.
Version 0.5.0 adds database-managed checklist items using ## Checklist in Markdown.
Checklist items sync independently of the card body; concurrent checklist changes require
--ours <id>:checklist or --theirs <id>:checklist. A missing section preserves remote
items on first contact; removing a tracked section clears the list.
Version 0.4.0 adds explicit deletion and requires sync protocol 4. Version 0.3.0 does not support deletion and uses sync protocol 3.
- Portable
cardstock.jsonconfiguration and offline Markdown validation. - Board-allocated card IDs with
cardstock new, counting reserved deleted IDs. - Browser-approved sign-in; no database credentials or app checkout required.
- Read-only status and dry-run plans with field-level uploads, downloads and conflicts.
- Bidirectional sync using a saved baseline, explicit
ours/theirschoices, transactional uploads, revision/identity checks and resumable recovery. - Free-form areas, board-defined epics and tags, explicit audience classification, and preservation of untouched formatting and unknown nested frontmatter.
- Explicit single/bulk card deletion, retained deletion snapshots, recoverable local backups, and delete-versus-edit conflicts across checkouts.
This release requires sync protocol 5 for writes. Upgrade the server and database before upgrading a tracker client; older apply protocols are refused, not silently downgraded. See Generic fields and audience and Interrupted sync and recovery.
Install and connect
npm install -g @guinetik/[email protected]
cardstock --version
mkdir tracker
cardstock init --project acme --board product --dir tracker --remote https://cardstock.example.com
cardstock login
cardstock validate --json
cardstock sync --dry-run --json
# After reviewing the plan:
cardstock sync --json
cardstock status --jsonUse existing project/board slugs from the website. init creates local configuration,
not a hosted project or board. An empty tracker downloads remote cards on its first
sync. If files already exist on both sides, resolve any first-contact differences
before applying; never delete a baseline to force a result.
Or run without a global installation (pin the same version for repeatable use):
npx --yes @guinetik/[email protected] --versionLicensed under GPL-3.0-only; see LICENSE.
Agent quick reference
Use the installed cardstock command from the tracker repository. In the Cardstock
source repository only, bun run cli <command> runs the development source instead.
Read the existing configuration before changing it; --config <path> selects a
specific tracker and --remote <url> overrides its configured server.
| Command | Effect |
| --- | --- |
| validate --json | Offline checks; no writes. |
| new "<title>" --json | GET the board for the next free ID and write <id>.md; no board writes. |
| status --json or sync --dry-run --json | GET-only plan; no tracker/baseline changes. |
| baseline --json | Save agreed state locally; no board writes. |
| sync --json | Apply both directions and save verified baseline checkpoints. |
| sync --card 19 --dry-run --json | Preview only #19; no writes. |
| sync --card 19 --json | Sync only #19, preserving other cards and their baselines. |
| status --card 19 --json | Check only #19; clean describes that card. |
| sync --ours 19:body --json | Choose local content for that conflicting field only. |
| sync --theirs 19:frontmatter.priority --json | Choose the board value for that conflicting field only. |
| delete 19 --dry-run --json | Preview explicit deletion of only #19; no writes. |
| delete --file deletions.txt --json | Delete only the listed IDs; retain local backups and remote snapshots. |
| sync --resume --json | Continue the recorded operation, preserving its retry ID. |
| sync --abort --json | Archive an interrupted intent; does not undo completed writes. |
| login --no-browser | Print a browser approval URL for the user. |
| logout | Revoke the token and remove the local credential. |
A normal task-update workflow:
cardstock status --json
# If the reviewed plan has safe pending changes, sync before editing.
cardstock sync --json
# Edit the appropriate tracker/<id>.md; preserve unrelated edits.
cardstock validate --json
cardstock sync --dry-run --json
# Apply only after reviewing the new plan:
cardstock sync --json
cardstock status --json
git diff -- trackerOnly write to the board when the user's task authorizes it. For a read-only review,
stop after status/dry-run. Do not select a conflict winner merely to obtain a clean
result: inspect the baseline/local/remote values and choose per field, or ask the
user. ours is local Markdown; theirs is the board, never an ownership flag.
For automation, read JSON from stdout and keep stderr separate. Preview counts are
under counts; a completed apply reports pending work under remaining and remote
write IDs under applied (including deletions/restorations; downloads are not listed there). Exit 0 means success,
not necessarily a clean board. Require ok: true, clean: true, and no diagnostics
when checking completion. Exit 1 signals conflicts/validation failures; exit 2
signals configuration, authentication, filesystem, network or execution failure.
Errors can occur after a remote commit: inspect the journal before retrying.
An already-synced tracker checks one snapshot and returns without creating a
journal or rewriting its baseline (baseline.saved: false). When cards change,
only cards needing publication or a refreshed baseline enter the recovery journal.
For one-card work, add --card <id> to the status, preview and sync commands above.
It supports local-only cards, remote-only cards and deletion tombstones. A missing
ID is an error; a missing local file still never requests deletion. Conflict
choices must refer to that card. Resume uses the saved selection automatically;
--card cannot be combined with --resume, --abort, baseline or delete.
Keep .cardstock/, *.md.cardstock-*.before and *.md.cardstock-*.tmp out of Git.
Never commit credentials or print tokens, never infer deletion from a missing card
file, and never overwrite a corrupt baseline or live lock. Recovery is described
below. A downloaded board edit changes local files: review those changes and commit
them only when the task includes committing work.
Configuration and validation
Minimal cardstock.json (paths are relative to this file):
{
"version": 1,
"project": "acme",
"board": "product",
"tracker": "tracker",
"remote": "https://cardstock.example.com"
}init creates cardstock.json in the current directory and refuses to replace it.
validate searches this directory and its parents for that file. --config <file>
selects it explicitly. Tracker paths resolve relative to the configuration file.
The tracker directory must already contain <id>.md files; other filenames are
ignored, matching the existing ETL. No issue files are changed during validation.
Validation uses the website's frontmatter contract and checks IDs against filenames.
An optional scheme adds board-specific workflow, tag vocabulary and cardinality,
open-item summary and required-section checks. Scheme validation also checks strict
YAML syntax (including duplicate keys); minimal configurations retain the existing
lenient parser. JSON diagnostics include a code and field, plus a scheme document
reference for board rules.
Exit codes: 0 success, 1 invalid issues, 2 command/configuration/filesystem error.
Import an existing tracker configuration
cardstock init --from backlog/board.json --out cardstock.migrated.json --dry-run --json
cardstock init --from backlog/board.json --out cardstock.migrated.json
cardstock validate --config cardstock.migrated.jsonOmit --out to create cardstock.json when it does not already exist. Both normal
creation and preview refuse an existing destination. Preview writes no files.
After reviewing and validating an alternative output, back up the existing
configuration and adopt the new file as cardstock.json, or keep selecting it
with validate --config. --from cannot be combined with --project, --board
or --dir; board identity comes from the legacy file.
The importer embeds the old scheme and mapping in the version-1 configuration.
It resolves tracker, mapping and seed paths relative to board.json. For legacy
repository-relative scheme_doc, it searches from that directory through its
ancestors and reports the first existing document it finds. Tracker, scheme
document and seed references are then saved relative to the new configuration.
Keep that relative directory layout when relocating the tracker. The referenced
files and tracker directory must exist at import time; validation subsequently
only needs the new configuration and Markdown files.
Missing files, unknown keys and unsupported rules fail before configuration is
written. $comment metadata is preserved. provisioning.seed records the SQL
seed for administrators and recovery; the CLI never executes it. Credentials
remain outside the configuration. --remote <url> is optional for offline work.
Configuration import preserves legacy mapping metadata and reports changed
semantics. Group aliases resolve tag spelling; legacy audience rules are never
evaluated. init and offline validate do not contact the board to verify tags
or lanes; sync and status do, as described under
Apply and conflict choices.
Migrating clients must pass Cardstock's contract, not reproduce their old project's
validator. Keep provisioning artifacts until the new workflow is verified.
To recover from an incorrect configuration, select the saved original file or correct the imported one and rerun validation. Init and validation do not modify issue files or the remote board, and do not create a sync baseline.
Sign in
cardstock login opens the Cardstock website for approval. Sign in with your
existing Cardstock account, approve the request, then return to the terminal.
The credential is a personal access token saved in your user configuration
directory, outside the tracker and repository. cardstock logout revokes that
token remotely and removes the local credential.
Pass --remote or configure a remote URL with cardstock init. Use
--no-browser to print the approval URL without opening it.
File a new card
cardstock new "<title>" allocates the next card ID and writes <id>.md in the
configured tracker directory.
cardstock new "Cards do not show how old they are" --tags bug,card
cardstock new "Board is slow to load" --summary "It takes ages to appear." --jsonThe ID comes from the board, not from the folder. The snapshot lists live cards and the IDs of deleted ones, which stay reserved, and local files are counted too, so an unsynced card cannot be handed out twice. This is why the command requires a configured remote and a signed-in session: an ID picked offline can collide with a card someone created on the site. It is still not a reservation against concurrent creation — preview before uploading, as in Preview and baseline.
The file is written and nothing else. No card is created on the board and no
baseline is touched; run validate, then sync, once the ## Ask says what was
actually asked.
| Flag | Default |
|---|---|
| --summary <text> | the title, because an open item needs a nonempty summary |
| --status <status> | backlog |
| --lane <lane> | unsorted |
| --epic <name> | Board & cards; --epic "" writes no epic |
| --area <name> | UI |
| --tags <tag,tag> | enhancement |
| --effort H\|M\|L, --priority 1\|2\|3 | omitted |
A new card never starts at a gate: the status and lane defaults keep filing
separate from promotion. The result is checked against cardstock.json's scheme
before anything is written, so an unknown tag, a status its lane cannot hold, or a
missing required section fails with the same diagnostics validate would print,
and no file is created.
Preview and baseline
status and sync --dry-run produce the same plan. Both support --config,
--remote, and --json, use the credential saved by login, and make only GET
requests to the board API. They write no tracker files, configuration or baseline
state. An empty tracker can preview cards to download; no missing file is treated
as an instruction to delete a remote card.
The planner compares each frontmatter field and the body against a saved baseline.
It reports local-only edits as uploads, remote-only edits as downloads, identical
edits on both sides as equal changes, and different edits to one field as conflicts.
One card may have both upload and download fields. JSON includes each field's
baseline, local and remote values and whether the field is present; removing a
field differs from setting it to null. Counts are per card and directions can
overlap. Unknown nested fields are compared, tags/relations compare as sets, and
tag aliases and unambiguous bare tags use the board vocabulary. Formatting-only
frontmatter changes, CRLF versus LF and the leading title H1 do not create edits.
To avoid ambiguous comparisons, preview requires valid YAML even when a minimal
configuration permits lenient parsing during validate.
Without a baseline, a local-only card is a proposed upload and a remote-only card is a proposed download. Differing existing cards require reconciliation; the CLI does not infer which side changed. Different titles under the same ID are flagged as a possible identity collision. A previously tracked card disappearing remotely without a retained deletion record is a conflict, never an automatic recreation or deletion. A confirmed deletion downloads only when the local copy is unchanged; local edits require an explicit delete-versus-edit choice. Duplicate IDs and filename/ID mismatches fail planning.
Once both sides agree and validation passes, explicitly record their agreed state:
cardstock baseline --remote https://cardstock.example.comThis writes only .cardstock/<scope-hash>.json beside the configuration. The scope
includes remote, project, board, relative tracker path and mapping, keeping state
separate for each checkout and board. Ignore .cardstock/ in version control;
baseline files contain card Markdown. They contain no credentials. Equal edits
since a previous baseline can be recorded with the same command. status and
sync --dry-run never advance it.
Baseline writes use an exclusive lock and atomic replacement. Corrupt, mismatched
or concurrently changed state fails without resetting it. If a run is interrupted,
inspect the reported baseline path and make sure no CLI process is using it before
removing its stale .lock/.tmp artifacts. Preserve the last valid JSON file;
discarding it loses the information needed to classify earlier edits. Recreating
a missing baseline still requires agreement on both sides.
Preview exit codes: 0 for a successfully computed plan without conflicts or
validation errors (pending uploads/downloads can still exist); 1 for conflicts,
scheme validation errors, or a refused baseline; 2 for configuration, credentials,
filesystem, malformed input/snapshot, or network errors. clean means there are
no pending data changes or conflicts; check ok/diagnostics as well before using
a plan. Snapshot and tracker changes during a read require retrying the preview.
Apply and conflict choices
cardstock sync --remote https://cardstock.example.com --json
cardstock sync --dry-run --ours 17:body --theirs 18:frontmatter.priority
cardstock sync --ours 17:body --theirs 18:frontmatter.priorityOurs always means local Markdown; theirs always means the hosted board. A choice
affects only the selected conflicting fields, preserving unrelated edits from both
sides. Omit :field to select all conflicts on a card. Repeat flags for additional
cards. Missing/replaced identities cannot be resolved by choosing a content side.
Legacy app-owned/file-owned body flags never choose the winner in this sync path.
The CLI records the complete intent before writes. Remote uploads are a single transaction containing the card, tags, links and history. Existing rows require immutable UUIDs and revisions; new rows use insert-only creation. An operation ID makes retries idempotent, including when the server committed but its response was lost. Snapshots are read back before local publication and baseline advancement. Each verified card checkpoints independently; a failure never advances unverified cards. Later edits can still appear as pending work in the final preview.
Older baselines lack immutable IDs. Once both sides agree, rerun baseline against
the updated server. Alternatively, sync --adopt-identities explicitly trusts the
currently observed identity mapping for this one upgrade. It does not override a
known UUID mismatch or resolve any content conflicts.
Optional field removal is a real removal, not an instruction to retain an old database value. Untouched source formatting and nested unknown YAML are preserved. Rank is a position in a lane: invalid or contradictory positions fail the entire remote batch. Ambiguous tied lane ranks require a board reorder before applying. Nonempty tag/epic/area tag-derivation overrides remain unsupported: put the desired tags directly in frontmatter before removing those rules. Normal tag references, unambiguous bare tags and group aliases are supported. No alias name is built in.
sync and status read the board's own tag groups and lanes and validate against
them, so a tag or lane added on the board never blocks the run that learns it.
sync then writes whatever the configuration lacked into cardstock.json,
appending to the arrays those entries belong in and leaving $comment, key order
and formatting alone, and reports it under learned. A card that fails validation
is skipped instead of holding the batch: the other cards apply, a skipped card
stays pending for the next run, and each one is named with its diagnostics under
skipped. Any skip keeps ok: false, so a partial apply cannot be read as a clean
one. sync --dry-run and status report both fields as what a real sync would do,
and write nothing themselves.
Explicit card deletion
Deletion requires the same signed-in board-admin access as other sync writes. It never needs database credentials, SQL seeds or a missing-file heuristic.
cardstock delete 17 --dry-run --json
# Review the exact card and action before applying:
cardstock delete 17 --json
# Bulk: one positive integer card ID per line; blank lines and # comments allowed.
cardstock delete --file deletions.txt --dry-run --json
cardstock delete --file deletions.txt --jsonYou can also pass several IDs: cardstock delete 17 18. Choose positional IDs or
--file, not both. Duplicate/invalid IDs and empty lists are errors; at most 1,000
IDs may be selected. The command affects only those cards, not unrelated pending
edits. --config, --remote and --json work as for sync. The final remaining
counts cover the whole tracker, so a successful scoped deletion may not be clean.
Deleting a live card requires a saved baseline with its immutable UUID. First sync
or establish agreement with baseline; new local-only cards must be synced before
this command can delete them. Removing a Markdown file by itself still causes a
download, never a remote delete. Completed tasks should normally be marked done
or archived, not deleted. Agents must have user authorization for the selected IDs.
Each deleted identity stays reserved remotely with its last sheet snapshot and a
new revision. Ordinary sync carries that deletion to other checkouts. Unchanged
local copies move to .cardstock/<scope>.json.backups/<operation>/17.md.before; locally edited copies
produce an existence conflict. The JSON plan reports delete_remote,
delete_local, or restore_remote explicitly; existence values are booleans
(false means deleted). Never interpret an absent API card as a tombstone.
| Conflict | --ours <id> | --theirs <id> |
| --- | --- | --- |
| delete 17: hosted card changed since baseline | Delete the hosted card; preserve local bytes in a backup. | Cancel deletion and download the hosted content. |
| sync: hosted deletion versus local content | Restore the local content remotely under the same UUID. | Accept deletion and move the local file to a backup. |
cardstock delete 17 --ours 17 --dry-run --json
cardstock delete 17 --ours 17 --json
# In another checkout with a local edit that should survive a hosted deletion:
cardstock sync --ours 17 --dry-run --json
cardstock sync --ours 17 --jsonExistence conflicts are whole-card decisions; content-field flags cannot resolve
them. A replaced UUID is still an identity error, not an ours/theirs choice.
To restore after accepting a deletion, copy the preserved .before file back to
17.md, inspect status, then explicitly choose sync --ours 17. Simply restoring
a file never silently recreates the hosted card. Restoration validates current
lanes/tags and restores sheet content and outgoing links, not deleted comments,
event history or incoming links. Keep administrator backups when full undo matters.
The remote batch is atomic and revision-checked. Repeated requests use the same
operation receipt; interrupted deletion uses sync --resume, not a new delete
command. sync --abort archives intent without undoing an already committed
deletion. Keep journal archives and .before files until results are verified.
Administrator deletions after the migration also create tombstones. Deletions before it was installed cannot be reconstructed and remain missing-identity conflicts. Tombstones remain until explicit restoration or deletion of the parent board; there is no automatic expiry, CLI purge or reuse of deleted IDs.
Deployment: apply migrations through 20260919000000_card_checklist.sql and run etl/backfill-checklist.ts --apply, deploy the protocol-5
server, then upgrade the CLI. Complete pending operations with their matching
client/server before upgrading. This release refuses older servers for writes;
the new server refuses protocol-3 writes. Provisioning remains administrator-only:
the CLI does not execute provisioning.seed or add a --seed command.
Generic fields and audience
area: Customer experience
epic: Improve onboarding
audience: internal
tags: [enhancement]Area is free-form nonempty text, not an enum. Epic is an optional assignment by
name (up to 200 characters): sync reuses an epic on that board or creates it.
Clear the assignment with an omitted, empty or null epic; this never deletes the
epic or its other cards. Epics created on the site can be assigned to cards and
round-trip normally. Empty epics without cards have no standalone Markdown file.
Legacy scheme.areas and scheme.epics lists are retained as suggestions only,
and a legacy required-epic rule does not prevent an unassigned card.
Audience is an independent filter classification: all (the UI's General option)
or internal. It is not an access-control boundary. A board needs no internal
tag; a tag named internal or any area/epic name has no automatic effect on audience.
Legacy audience_internal_when is retained as inert metadata, never evaluated by
sync or the importer. Existing database audience values are not migrated or reset.
Omitted audience means all. Protocol-3 snapshots expose the existing stored
classification, so old baselines download an internal value before future edits.
Removing a previously synced audience: internal is an explicit change back to
all. Field merging, revision checks and recovery apply as for other fields.
Older clients/servers cannot apply across this protocol change. Finish or archive
pending journals before upgrading; do not delete the baseline or recovery files.
Interrupted sync and recovery
cardstock sync --resume --remote https://cardstock.example.com
cardstock sync --resume --recover-lock --remote https://cardstock.example.com
cardstock sync --abort --remote https://cardstock.example.comResume uses the saved intent and retry ID, not a freshly guessed winner. Do not
pass new --ours/--theirs choices to resume. If either side changed incompatibly,
preserve those edits, abort the old intent, then preview/reconcile a fresh plan.
Abort archives the journal; it does not roll back completed remote or local
writes. An explicit baseline replacement is refused while a journal is pending.
The scope lock excludes other cooperating sync/baseline writers. --recover-lock
only reclaims a lock recorded on this machine whose process has exited. Unknown
owners and live processes require inspection, never automatic lock stealing.
Local files are staged and flushed, then published with an exclusive filesystem
link so a file that appears concurrently is never overwritten. An existing file
is first moved to its per-operation .before name and checked again. New backups
are stored at .cardstock/<scope>.json.backups/<operation>/<id>.md.before, beside
the baseline and journal. A crash in
that brief filename gap is recoverable with --resume; file contents are never
published partially. If tracker and configuration are on different filesystems,
backups stay beside the Markdown files so the move remains atomic.
Displaced originals are retained to preserve writes from
editors holding an old file descriptor. These backups and journal archives contain
Markdown, not credentials. Journals from older releases still recover backups
beside the original files; existing backups are not moved or deleted. Finish new
operations with CLI 0.6.0 or newer. Add these patterns to each tracker's ignore file:
.cardstock/
*.md.cardstock-*.before
*.md.cardstock-*.tmpDo not delete recovery artifacts until the run is verified and any concurrent editor changes have been reconciled. Hard-link publication requires a filesystem that supports hard links; an unsupported filesystem stops with originals retained. File data is flushed; directory durability follows platform filesystem guarantees (Windows does not expose POSIX directory fsync through Node).
.before files are intentional recovery copies, not sync inputs. They remain
after success because an editor can still hold and write the displaced original.
There is no automatic expiry. Once the operation is verified and editors have
closed or reopened the affected files, obsolete backups can be removed manually.
Apply exits 0 when the recorded operation completes without remaining conflicts, 1 for plan/validation conflicts, and 2 for failed or interrupted execution. JSON errors include the pending journal path and recovery guidance when available.
