wispctl (Rust) #
A from-scratch Rust port of the TypeScript cli/ (wispctl 1.3.x). The TS
source is the contract: same commands, flags, defaults, prompts, wording,
on-disk state and network behaviour, unless this file says otherwise.
Layout #
| crate | what | effects |
|---|---|---|
wispplace-lexicons |
place.wisp.* types generated by jacquard-codegen (scripts/lexgen.sh). Never hand-edit. |
none |
wispplace-core |
Pure logic: CID/gzip/MIME rules, ignore rules, tree building, subfs split + expansion, path validation, _redirects, generated pages, OAuth scope maths, constants. |
none (callbacks for fetches) |
wispplace-ui |
All terminal output: prompts, spinners, progress, panels, printed lines. ratatui inline viewport. | terminal |
wispctl |
clap CLI, auth + account store, XRPC, commands. | everything |
Conventions #
- Functional core, imperative shell. Put decisions in
wispplace-coreas pure functions over data (fn plan(...) -> Plan), keep I/O inwispctlthin. Prefer immutable values, iterators and small enums over mutable state and flags. Traits only where an effect needs a fake in tests. - Never
println!/eprintln!. Usewispplace_ui:- results (listings,
--json, final URLs) →wispplace_ui::out/out_text(stdout, so--json | jqworks); - status →
note,success,failure,warning,info,blank; - work in flight →
spinner(..)then.succeed(..)/.fail(..); counted work →progress(label, total, Direction::Up|Down); - long-lived dashboards →
panel(..); - prompts →
crate::prompts::*(handles cancel → exit 0, and the no-TTY case with a hint about which flag to pass). - colour mapping from the TS code:
pc.bold→s::bold,pc.dim→s::muted,pc.cyanheadline→s::accent,pc.cyanURL→s::link,pc.green→s::ok,pc.yellow→s::warn,pc.red→s::danger.
- results (listings,
- Errors:
anyhow::Resultinwispctl; typed errors (thiserror-free enums are fine) inwispplace-core.mainprintsError: {err:#}and exits 1. - Keep the TS wording for user-facing messages; agents and scripts grep them.
- Tests next to the code (
#[cfg(test)]). Port the TS tests (cli/**/*.test.ts) and add fixtures generated from the TS implementation wherever byte-compatibility matters (CIDs, gzip, JSON shapes). - Latest crate versions; add deps to
[workspace.dependencies]incli-rs/Cargo.tomland reference them with.workspace = true. Binary size matters:default-features = false, rustls only, no second async runtime, no second HTTP stack if jacquard's reqwest already covers it.
jacquard #
We build against ana's fork, https://tangled.org/okami.mom/jacquard main
(upstream main, 0.13.0 unreleased, plus fixes pending upstream: repo scope
collection params, permission-set rpc audiences per spec, and always writing
aud on rpc scopes). Cargo.lock pins the commit; cargo update -p jacquard
moves it. scripts/lexgen.sh runs the fork's jacquard-codegen from a local
checkout (JACQUARD_DIR, default ../../jacquard); unlike 0.12 it also accepts
the permission-set lexicons (aud: "*").
Releasing #
scripts/build-cli-binaries.shbuilds every target intobinaries/withSHA256SUMS, under the legacywisp-cli-*names that CI pipelines curl.- Copy the new checksums into
binaries/index.html,docs/src/content/docs/cli.mdandapps/main-app/public/editor/tabs/CLITab.tsx, then deploybinaries/asnekomimi.pet/wisp-cli-binarieswithout--spa/--directory(keeps its settings). bun cli-rs/scripts/npm-packages.tsstageswispctl(a node shim) and the@wispplace/wispctl-<os>-<cpu>binary packages intarget/npm/. Publish the platform packages first, thenwispctl.[package.metadata.binstall]inwispctl/Cargo.tomlpointscargo binstallat the samewisp-cli-binariesURLs. It only takes effect from crates.io:cargo binstall --gitwants aCargo.tomlat the repository root.
crates.io waits for a jacquard release carrying the fork's fixes (git
dependencies cannot be published); the crates are already named wispplace-*
because wisp-core and wisp-ui are taken there.
Compatibility decisions #
- State DB stays at
~/.config/wispctl/state.sqlite, samekvtable, same keys (account:,handle:,dir:,default_account,store_version,dir_oauth_port:), same JSON account shape, 0600/0700 permissions. Existing installs keep their accounts, directory links and default account. - Keychain service
wispctl. App passwords stay atapp-password:<did>(shared with the TS CLI). jacquard OAuth sessions use their own namespace (oauth-v2:...keychain accounts,oauth_session_v2:/oauth_state_v2:kv fallback keys) and never read or overwrite the TS CLI's<did>/oauth_session:<did>entries;logoutremoves both old and new entries. - Blob bytes must match what the TS CLI uploads (gzip level 9, Node zlib header) so unchanged files are reused across both CLIs.
Deliberate differences from the TypeScript CLI #
- Records are written through the generated types (
Fs,SubfsRecord,Settings) and jacquard's typed record calls, not hand-built JSON. - Subfs record keys carry a per-deploy TID (
{site}-{tid}-subfs-1,{site}-{tid}-chunk-0). Reusing the previous deploy's keys overwrote records that still referenced reused blobs; the PDS deletes unreferenced blobs immediately, so the next write failed withBlobNotFoundwhenever the chunk layout changed. Old records are deleted after the new manifest lands. - When a directory is too large for one subfs record, splitting descends into an oversized subdirectory instead of chunking the parent's entries. The TS planner put a 600-file nested folder into one 180 KB chunk with a 600-entry directory, which the lexicon's 500-entry limit rejects.
- Uploads use a replenishing worker pool instead of fixed batches; file order is sorted and deterministic.
- New gzip blobs always use OS byte 3; reuse matches the OS bytes Node/Bun write on any platform, so blobs from either CLI are reused.
- Directory listings sort case-insensitively instead of with ICU collation.
- Status goes to stderr and results to stdout, so
--jsonoutput is clean.