Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
6.3 kB

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-core as pure functions over data (fn plan(...) -> Plan), keep I/O in wispctl thin. 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!. Use wispplace_ui:
    • results (listings, --json, final URLs) → wispplace_ui::out / out_text (stdout, so --json | jq works);
    • 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.cyan headline→s::accent, pc.cyan URL→s::link, pc.green→s::ok, pc.yellow→s::warn, pc.red→s::danger.
  • Errors: anyhow::Result in wispctl; typed errors (thiserror-free enums are fine) in wispplace-core. main prints Error: {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] in cli-rs/Cargo.toml and 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 #

  1. scripts/build-cli-binaries.sh builds every target into binaries/ with SHA256SUMS, under the legacy wisp-cli-* names that CI pipelines curl.
  2. Copy the new checksums into binaries/index.html, docs/src/content/docs/cli.md and apps/main-app/public/editor/tabs/CLITab.tsx, then deploy binaries/ as nekomimi.pet/wisp-cli-binaries without --spa/--directory (keeps its settings).
  3. bun cli-rs/scripts/npm-packages.ts stages wispctl (a node shim) and the @wispplace/wispctl-<os>-<cpu> binary packages in target/npm/. Publish the platform packages first, then wispctl.
  4. [package.metadata.binstall] in wispctl/Cargo.toml points cargo binstall at the same wisp-cli-binaries URLs. It only takes effect from crates.io: cargo binstall --git wants a Cargo.toml at 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, same kv table, 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 at app-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; logout removes 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 with BlobNotFound whenever 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 --json output is clean.