@rozek/sds-command
v0.0.13
Published
CLI tool for shareable-data-store — interactive REPL, one-shot and batch commands
Maintainers
Readme
@rozek/sds-command
CLI tool for the shareable-data-store (SDS) family. Provides a one-shot command line interface, an interactive REPL shell, and a batch script runner for managing CRDT stores, entries, items, links, and tokens.
Prerequisites
| requirement | details |
| --- | --- |
| Node.js 22.5+ | required to run sds. npx is included with Node.js — no separate install needed. Download from nodejs.org. |
| SDS server | only required for network operations (store sync, store ping, token issue). Local read/write commands work entirely offline against the SQLite file. |
| JWT tokens | server operations need a client token (--token, scope read or write) or an admin token (--admin-token, scope admin). Tokens are issued by the server's POST /api/token endpoint via sds token issue. |
SQLite support is built directly into Node.js since version 22.5 via the node:sqlite module — no separate database driver, no native C++ addon, and no build toolchain is needed. The sds binary therefore runs with Node.js as its only dependency.
Note on stability:
node:sqliteis classified as Stability 1 — Experimental in the Node.js 22 and 24 documentation. In practice the API has been stable since its introduction, with no breaking changes across any release. The experimental classification reflects the Node.js team's ongoing stabilisation process; the module is expected to reach Stability 2 — Stable with Node.js 26. Thesdsbinary suppresses the associated runtime warning automatically.
Installation
pnpm add @rozek/sds-commandRequires Node.js 22.5+. After installation the sds binary is available in the project's node_modules/.bin/ directory and — when installed globally — directly on the PATH.
Without installation via npx
npx can invoke sds directly without a global installation:
npx @rozek/sds-command entry get <id>npx downloads @rozek/sds-command and all its dependencies on first use and caches them locally; subsequent invocations reuse the cached version. Every argument after @rozek/sds-command is passed directly to sds, so all commands, options, and global flags work exactly as documented below.
Concepts
The tool supports three modes of operation:
| mode | how to trigger |
| --- | --- |
| one-shot | sds <command> [options] — runs a single command and exits |
| script | sds --script <file> — reads commands line-by-line from a file or stdin (-) |
| REPL | sds shell — opens an interactive read-eval-print loop |
sds without any arguments or sub-command prints the help text and exits.
Every command shares the same set of global options and environment variable overrides. Options take precedence over environment variables; environment variables take precedence over built-in defaults.
Global options
| option | env var | description |
| --- | --- | --- |
| --server <url> | SDS_SERVER_URL | WebSocket server base URL |
| --store <id> | SDS_STORE_ID | store identifier (= local SQLite file basename) |
| --token <jwt> | SDS_TOKEN | client JWT with read or write scope |
| --admin-token <jwt> | SDS_ADMIN_TOKEN | admin JWT with admin scope |
| --persistence-dir <path> | SDS_PERSISTENCE_DIR | directory for local SQLite files (default: ~/.sds) |
| --format <fmt> | — | output format: text (default) or json; any other value exits with code 2 |
| --on-error <action> | — | error handling in script/batch mode: stop (default), continue, or ask; ignored in the interactive REPL (which always continues on error); any other value exits with code 2 |
| --version | — | print the installed sds version number and exit with code 0 |
General behavior
Repeated options
When the same option is supplied more than once on the command line, the last occurrence wins. For example:
sds --store notes entry create --label "Draft" --label "Final"
# stored label: "Final"This applies to all single-value options (--label, --mime, --value, --file, --target, --container, --at, --info, --depth, --only, --to, --ttl, etc.). The --info.<key> pattern follows the same rule: if --info.author alice and --info.author bob are both given, only bob is stored.
Command reference
token
sds token issue --sub <subject> --scope <scope> [--exp <duration>]token issue requests a new JWT from the server's POST /api/token endpoint. Requires --admin-token. --sub <subject> sets the user identifier (required, e.g. an email address); --scope must be read, write, or admin (required); --exp <duration> sets the expiry as a number followed by s, m, h, or d (default: 24h).
store
sds store info
sds store ping
sds store sync [--timeout <ms>]
sds store destroy
sds store export [--encoding json|binary] [--output <file>]
sds store import --input <file>store info shows existence, entry count, and DB path of the local store. JSON output: { storeId, exists: false } when not found, or { storeId, exists: true, entryCount, dbPath } when found.
store ping checks WebSocket server reachability (requires --server and --token).
store sync connects to the server, exchanges CRDT patches, and disconnects. Use --timeout <ms> to wait longer on slow links (default: 5 000 ms).
store destroy permanently deletes the local SQLite file and its WAL/SHM companions. JSON output: { storeId, destroyed: true }.
store export writes the current store snapshot to a file or stdout. --encoding json (default) or --encoding binary; any other value exits with code 2. --output <file> writes to a file instead of stdout — if the file already exists it is silently overwritten. JSON output (with --format json --output): { exported: true, file: "<path>", format: "json"|"binary" }.
store import reads the snapshot file given by --input <file> and CRDT-merges it into the local store — conflicts are resolved automatically. Accepts both JSON and binary format (auto-detected). If the file content begins with { or [ but is not valid JSON, the command exits with code 2. JSON output: { imported: true, file: "<path>" }.
entry
All entry operations — creating, reading, listing, and modifying both items and links — are handled by a single entry command group. <id> accepts a canonical UUID or the well-known aliases root, trash, and lost-and-found (also accepted without hyphens as lostandfound).
sds entry create [--target <item-id>] [--container <item-id>] [--at <index>]
[--label <label>] [--mime <type>] [--value <string>] [--file <path>]
[--info <json>] [--info.<key> <value>] [--info-delete.<key>]
sds entry get <id> [--kind] [--label] [--mime] [--value] [--info] [--info.<key>] [--target]
sds entry list <id> [--recursive] [--depth <n>] [--only items|links]
[--label] [--mime] [--value] [--info] [--info.<key>]
sds entry update <id> [--label <label>] [--mime <type>] [--value <string>]
[--file <path>] [--info <json>] [--info.<key> <value>] [--info-delete.<key>]
sds entry move <id> --to <target-id> [--at <index>]
sds entry delete <id>
sds entry restore <id> [--to <target-id>] [--at <index>]
sds entry purge <id>entry create creates a new entry and prints its UUID to stdout. Without --target an item is created; --target <item-id> creates a link pointing at that item instead. If the store does not exist yet it is created automatically (items only — for links the store must already exist). --container <item-id> sets the outer container (default: root) — also accepts the well-known aliases root and lost-and-found; the container must be an item, not a link; passing a link ID exits with code 3 (NotFound). --at <index> controls the insertion position as a zero-based integer (0 = first, default: append at end); a negative value exits with code 2; a value larger than the current child count is silently clamped to the end. --label <label> sets an initial label; --info <json> and --info.<key> <value> set initial info entries; --info-delete.<key> is accepted for consistency with entry update but is a no-op on a new entry (there are no existing keys to remove). Item-only options (--mime, --value, --file) exit with code 2 when combined with --target. --value and --file are mutually exclusive; using both at once exits with code 2. JSON output: { id, created: true, kind: "item" } for items, { id, created: true, kind: "link", target: "<target-id>" } for links.
entry get displays fields of an entry. Without display flags all available fields are shown. --kind returns item or link; --label includes the label; --mime includes the MIME type (items only); --value includes the stored value (items only); --info includes the full info map as a JSON object; --info.<key> includes only the single info entry named <key> (output key: info.<key>); --target includes the target item's UUID (links only). --mime, --value, and --info.<key> are silently ignored for links; --target is silently ignored for items. JSON output: { id, [kind], [label], [mime], [value], [info], [target] } — only the requested fields are included; mime, value, and info only appear for items; target only appears for links.
entry list traverses the direct inner entries of a container item and prints one entry per line. The container <id> accepts a canonical UUID or the well-known aliases root, trash, and lost-and-found (also lostandfound without hyphens). Without display flags only the UUID of each entry is printed. --recursive enables a depth-first walk; --depth <n> limits the recursion depth (only effective with --recursive). --only items (or the singular form --only item) restricts output to items; --only links (or --only link) restricts to links; any other value exits with code 2. The display flags (--label, --mime, --value, --info, --info.<key>) work exactly as described for entry get. JSON output: array of { id, kind, [label], [mime], [value], [info] } — id and kind are always present; additional fields follow the same display-flag rules as entry get.
entry update modifies an existing entry — works on both items and links. Only explicitly specified fields are changed. --label is accepted for both kinds. --mime, --value, and --file are items-only; using them on a link exits with code 2. --value and --file are mutually exclusive; using both at once exits with code 2. --info <json> and --info.<key> <value> are merged into the existing info map (individual keys are added or overwritten, not replaced entirely). --info-delete.<key> removes a single key from the info map; any number of delete flags may be combined with --info and --info.<key> flags in the same command. JSON output: { id, updated: true }.
entry move moves a live entry to a different container. --to <target-id> is required and also accepts the well-known aliases root, trash, and lost-and-found; --at <index> sets the insertion position as a zero-based integer (0 = first, default: append at end); a negative value exits with code 2; a value larger than the current child count is silently clamped to the end. Moving an entry into its own descendant exits with code 6 (Forbidden). JSON output: { id, movedTo: "<target-id>", at: <index>|"end" }.
entry delete soft-deletes the entry by moving it to the trash. System entries (root, trash, lost-and-found) cannot be deleted — attempting to do so exits with code 6 (Forbidden). JSON output: { id, deleted: true }.
entry restore moves a trashed entry back to a live container. The entry must already be in the trash; attempting to restore a live entry exits with code 6 (Forbidden). --to <target-id> sets the destination container (default: root) — also accepts the well-known aliases root and lost-and-found; --at <index> sets the insertion position as a zero-based integer (0 = first, default: append at end); a negative value exits with code 2; a value larger than the current child count is silently clamped to the end. JSON output: { id, restoredTo: "<target-id>", at: <index>|"end" }.
entry purge permanently deletes an entry. The entry must already be in the trash; purging a live entry exits with code 6 (Forbidden). JSON output: { id, purged: true }.
trash
sds trash list [--only items|links]
sds trash purge-all
sds trash purge-expired [--ttl <ms>]trash list lists all entries currently in the trash. --only items (or --only item) restricts to items; --only links (or --only link) restricts to links; any other value exits with code 2. JSON output: array of { id, kind: "item"|"link", label }.
trash purge-all permanently deletes every entry in the trash. JSON output: { purged: <count> }.
trash purge-expired permanently deletes trash entries older than --ttl milliseconds (default: 30 days = 2 592 000 000 ms). --ttl must be a positive integer; zero or negative values exit with code 2 (UsageError). JSON output: { purged: <count>, ttlMs: <n> }.
tree
sds tree show [--depth <n>]Displays the entire store as an indented ASCII tree starting from the root item. --depth <n> limits the number of levels rendered; without --depth the full tree is shown. JSON output: { root: [<tree-node>, ...] } where each tree node is { Id, Kind, Label, [TargetId], Children: [...] } — TargetId is present only for link nodes.
shell
sds shellOpens an interactive REPL. Each line is parsed and executed as an sds command without the sds prefix. Blank lines and lines starting with # are ignored. Type exit or quit to close the session. Global options set when starting the shell (e.g. sds --store mystore shell) apply to every command in the session. The prompt is displayed in bold to help distinguish it from command output.
The REPL continues after errors — a failing command prints the error message but does not end the session.
Getting help inside the REPL:
| what you type | what you get |
| --- | --- |
| help | list of all available commands |
| help <command> | help for a top-level command group (e.g. help entry) |
| <command> --help | help for a command or sub-command (e.g. entry list --help) |
--script
sds --script <file>
sds --script - (read from stdin)Reads commands from a file or stdin, executing them one per line (same syntax as the REPL). Error handling follows --on-error: stop aborts on the first error (default), continue keeps going, ask prompts interactively (TTY only). Global options set at invocation time (e.g. sds --store mystore --script file.sds) apply to every command in the script — individual lines do not need to repeat them.
The --info.<key> pattern
Several commands support dynamically named info options where <key> is the name of an info map entry and is embedded directly in the option name:
In read commands (entry get, entry list): --info.<key> (no value argument) selects a single info entry for output. For example, --info.priority outputs only the info.priority field instead of the full info map.
In write commands (entry create, entry update): --info.<key> <value> sets or updates one info entry. The value is parsed as JSON if valid, otherwise stored as a plain string. For example, --info.priority high stores the string "high" and --info.count 42 stores the number 42. Multiple --info.<key> options can be given in a single command; they are merged with any object supplied via --info <json>.
Key naming rules: <key> must be a valid JavaScript identifier — it may contain letters, digits, _, and $, but must not start with a digit and must not contain spaces, hyphens, dots, or other special characters. Examples of valid keys: author, _private, $ref, createdAt. Keys that violate this rule cause sds to exit with a usage error (exit code 2). The same rule applies to keys inside a JSON object supplied via --info <json>.
The --info-delete.<key> pattern
In write commands (entry create, entry update): --info-delete.<key> (no value argument) removes a single info entry from the stored info map. For example, --info-delete.author removes the author key. Multiple --info-delete.<key> flags can be combined with --info and --info.<key> flags in the same command — all changes are applied together. On entry create the flag is accepted but has no effect (there are no existing keys to remove). The same key-naming rules apply: <key> must be a valid JavaScript identifier.
Conflict rule — delete wins: If the same key is specified by both --info.<key> (or --info <json>) and --info-delete.<key> in the same command, the delete takes precedence and the key will be absent after the command. The write is applied first and the delete second, regardless of the order the flags appear on the command line.
Concurrent access with sds-sidecar
The sds CLI and the sds-sidecar daemon can safely operate on the same store at the same time. Both share the same SQLite database (WAL mode), and the sync engine automatically merges patches written by other processes before writing a checkpoint snapshot. CLI mutations — such as trash purge-all, entry create, or entry update — are never silently lost, even while the sidecar is running.
Examples
Local store — first steps
Create a store implicitly with the first entry create, explore the resulting tree, then retrieve a specific entry by its UUID:
# First entry create also initialises the local SQLite store
sds --store notes entry create --label "Recipes" --mime text/plain --value "Pasta: boil, sauce, enjoy"
# Show the full tree
sds --store notes tree show
# List root with labels and MIME types
sds --store notes entry list root --label --mime
# Get a specific entry (UUID printed by entry create)
sds --store notes entry get <uuid> --label --valueWorking with info metadata
Attach arbitrary key/value metadata to entries, update individual keys, and remove keys that are no longer needed:
# Create an entry with two info keys
sds --store notes entry create --label "Report Q1" \
--info.author "alice" --info.status "draft"
# Update the status, add a reviewer key, and remove the now-unnecessary author key
# All three changes happen atomically in a single command
sds --store notes entry update <uuid> \
--info.status "final" \
--info.reviewer "bob" \
--info-delete.author
# Conflict rule: if the same key appears in both --info.<key> and --info-delete.<key>,
# the delete always wins and the key is absent after the command
sds --store notes entry update <uuid> \
--info.status "published" \
--info-delete.status
# → "status" will NOT be present in the info map afterwardsStore with persistence and synchronisation
Set environment variables once for the session so every command picks them up automatically:
export SDS_SERVER_URL=ws://my-sds-server.example.com
export SDS_STORE_ID=team-wiki
export SDS_TOKEN=eyJhbGci...
# Verify that the server is reachable
sds store ping
# Create items locally — persisted immediately to SQLite
sds entry create --label "Meeting Notes" --mime text/plain
sds entry create --label "Action Items" --mime text/plain
# Push local CRDT patches to the server and pull remote changes
sds store sync
# Allow more time for patch exchange on a slow link
sds store sync --timeout 10000The local SQLite file in ~/.sds/team-wiki.db is updated on every write, so work continues safely while offline. store sync merges changes in both directions whenever a connection is available.
Setting up on a new machine: if the store already exists on the server and you want to fetch its full current state, run store sync first — before any entry or tree commands. store sync bootstraps the local SQLite file from scratch when it does not yet exist:
export SDS_SERVER_URL=ws://my-sds-server.example.com
export SDS_STORE_ID=team-wiki
export SDS_TOKEN=eyJhbGci...
# download the full store from the server (creates the local SQLite file)
sds store sync
# the local store is now populated — all commands work offline from here
sds tree showIssuing client tokens
An admin token is required to issue scoped JWTs for regular clients:
# Write token for Alice, valid for 7 days
sds --server ws://my-sds-server.example.com \
--admin-token $SDS_ADMIN_TOKEN \
token issue --sub alice --scope write --exp 7d
# Read-only token for a public reader, valid for 30 days
sds --server ws://my-sds-server.example.com \
--admin-token $SDS_ADMIN_TOKEN \
token issue --sub reader --scope read --exp 30dBatch setup with a script file
A script file initialises a folder structure in one step:
# init-wiki.sds — one command per line, # lines are comments
entry create --label "Documentation" --mime text/plain
entry create --label "Meeting Notes" --mime text/plain
entry create --label "Decisions" --mime text/plain
store syncRun it against the configured store:
sds --store team-wiki --script init-wiki.sdsPipe from stdin to avoid a temporary file:
printf 'entry create --label "Quick note" --value "Remember this"\nstore sync\n' \
| sds --store team-wiki --script -Interactive REPL session
The shell is useful for ad-hoc inspection and edits without retyping global flags:
$ sds --store team-wiki shell
SDS interactive shell — type "help [command]" for help, "exit" to quit
sds> store info
sds> entry list root --label --mime
sds> entry create --label "Ideas" --mime text/plain
sds> entry get <uuid> --label --value --kind
sds> tree show --depth 2
sds> store sync
sds> exitExit codes
| code | constant | meaning |
| --- | --- | --- |
| 0 | OK | success |
| 1 | GeneralError | unspecified runtime error |
| 2 | UsageError | bad arguments or missing required option |
| 3 | NotFound | entry, store, or file not found |
| 4 | Unauthorized | authentication failed (missing or invalid token) |
| 5 | NetworkError | WebSocket or HTTP connection error |
| 6 | Forbidden | operation not permitted for this scope |
License
MIT License © Andreas Rozek
