Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
24 KiB
Command reference
Every command works under both binary names: logos-scaffold and the shorter
alias lgs. They are functionally identical.
Each subcommand documents copy-paste examples under --help:
lgs deploy --help
lgs test-node start --help
Global -q / --quiet (or LOGOS_SCAFFOLD_QUIET=1) suppresses echoed
external commands.
The flags below were verified against logos-scaffold 0.3.0. --help is
always authoritative.
Create or adopt a project
logos-scaffold create <name> [--template NAME] [--vendor-deps] [--lez-path PATH] [--cache-root PATH]
logos-scaffold new <name> [--template NAME] [--vendor-deps] [--lez-path PATH] [--cache-root PATH]
logos-scaffold init [--dry-run] [--no-backup]
Set up, build, generate
logos-scaffold setup [--prebuilt]
logos-scaffold build [project-path] [--prebuilt]
logos-scaffold build idl [project-path]
logos-scaffold build client [project-path]
Run the local sequencer
logos-scaffold localnet start [--timeout-sec N]
logos-scaffold localnet stop
logos-scaffold localnet status [--json]
logos-scaffold localnet logs [--tail N] [--json]
logos-scaffold localnet reset (--yes | --dry-run) [--reset-wallet] [--verify-timeout-sec N]
Deploy
logos-scaffold deploy [program-name] [--program-path PATH] [--json]
The inner loop
logos-scaffold run [--profile NAME] [--reset | --no-reset] [--post-deploy <cmd>...] [--no-post-deploy] [--watch] [--watch-debounce-ms MS] [--localnet-timeout N]
See configuration.md for [run] profiles, post-deploy
hooks, and watch mode.
Wallet
logos-scaffold wallet list [--long] [--json]
logos-scaffold wallet topup [<address> | --address <address-ref>] [--dry-run] [--json]
logos-scaffold wallet default set <address-ref>
logos-scaffold wallet default set --address <address-ref>
logos-scaffold wallet -- <wallet-command...>
Pass through to spel
logos-scaffold spel -- <spel-command...>
Test nodes
Isolated, short-lived sequencer instances for integration tests. Unlike
localnet (one long-lived developer sequencer per project on a fixed port),
each test node gets its own RPC port, config, database, log, and runtime
directory under .scaffold/test-nodes/<id>.
Lifecycle:
logos-scaffold test-node pins [--project DIR] [--lez-source URL|DIR] [--lez-ref REF] [--json]
logos-scaffold test-node prepare [--project DIR] [--cache-root DIR] [--lez-source URL|DIR] [--lez-ref REF] [--json]
logos-scaffold test-node doctor [--project DIR] [--json]
logos-scaffold test-node start [--project DIR] [--state DIR] [--port N] [--work-dir DIR] [--preserve-work-dir] [--timeout-sec N] [--block-create-timeout-ms MS] [--retry-pending-blocks-timeout-ms MS] [--json]
logos-scaffold test-node status --node <id|dir> [--json]
logos-scaffold test-node stop --node <id|dir> [--preserve-work-dir]
logos-scaffold test-node run [--project DIR] [--state DIR] [--timeout-sec N] [--block-create-timeout-ms MS] [--retry-pending-blocks-timeout-ms MS] [--serial | --parallel N] -- <command...>
Transactions:
logos-scaffold test-node tx submit --url URL --file PATH [--encoding borsh-base64|borsh] [--json]
logos-scaffold test-node tx wait --url URL --hash HASH [--after-block N] [--timeout-sec N] [--json]
logos-scaffold test-node tx submit-and-wait --url URL --file PATH [--encoding borsh-base64|borsh] [--timeout-sec N] [--json]
Blocks and clock:
logos-scaffold test-node blocks head --url URL [--json]
logos-scaffold test-node blocks range --url URL --from N --to N [--json]
logos-scaffold test-node blocks wait --url URL --after N [--count N] [--timeout-sec N] [--json]
logos-scaffold test-node clock read --url URL [--json]
logos-scaffold test-node clock wait-stable --url URL [--samples N] [--timeout-sec N] [--json]
Account and proof reads:
logos-scaffold test-node account get --url URL --account-id ID [--at-block N] [--json]
logos-scaffold test-node account batch-get --url URL --account-id ID... [--at-block N] [--json]
logos-scaffold test-node proof get --url URL --commitment HEX|BASE58 [--at-block N] [--json]
logos-scaffold test-node snapshot accounts --url URL --account-id ID... --output PATH [--json]
State snapshots and seeding:
logos-scaffold test-node state schema [--project DIR] [--json]
logos-scaffold test-node state export --url URL --account-id ID... --output PATH [--json]
logos-scaffold test-node state seed [--project DIR] --input PATH [--output DIR] [--json]
The same surface is available in Rust via
logos_scaffold::api::testnode::{TestNode, TestNodeConfig}.
Basecamp
logos-scaffold basecamp setup
logos-scaffold basecamp modules [--path PATH]... [--flake REF]... [--show]
logos-scaffold basecamp install [--print-output]
logos-scaffold basecamp launch <profile> [--log-file[=PATH]]
logos-scaffold basecamp develop <module> [--dev-shell ATTR]
logos-scaffold basecamp build [--variant lgx|lgx-portable|all] [--module NAME]
logos-scaffold basecamp build-portable [--module NAME]
logos-scaffold basecamp run <module> [--host standalone]
logos-scaffold basecamp paths <profile> [--json]
logos-scaffold basecamp doctor [--json]
logos-scaffold basecamp docs
Project contract for modules: basecamp-module-requirements.md.
Diagnostics
logos-scaffold doctor [--json]
logos-scaffold report [--out PATH] [--tail N]
logos-scaffold completions <bash|zsh>
logos-scaffold help
Command semantics
createandneware aliases.initwritesscaffold.toml(schema v0.2.0) with defaults into the current directory so an existing project can use the scaffold workflow. It creates.scaffold/{state,logs}and appends.scaffoldto.gitignore. Whenscaffold.tomlalready exists at an older schema,initmigrates it in place viatoml_editso comments, key ordering, and unrelated sections survive the rewrite — old[basecamp].pin/.source/.lgpm_flakemove to[repos.basecamp]/[repos.lgpm]; old[basecamp.modules.*]move to top-level[modules.*]; legacyurlfields on[repos.{lez,spel}]are dropped. Migrations write ascaffold.toml.baknext to the original by default (skip with--no-backup); preview either form with--dry-run. Already-current configs succeed, leavescaffold.tomlunchanged, and refresh the shipped AI skills. Runsetupnext after a fresh init or migration.setupsyncs LEZ andspelto their pinned commits (read from[repos.lez]/[repos.spel]), builds the standalonesequencer_service,wallet, andspelbinaries locally, and seeds a project default wallet when none is set. Seeding takes the first preconfigured public account from the pinned LEZ debug wallet config when it ships any (deterministic); when it ships none — LEZ v0.2.0 does not —setupinstead runs the wallet CLI once so it creates its own storage and adopts the first public account it lists, which is freshly generated key material guarded byLOGOS_SCAFFOLD_WALLET_PASSWORD(see SECURITY.md). All binaries are project-local and are not installed to PATH — uselogos-scaffold wallet .../logos-scaffold spel -- ...to interact with them. By default[repos.lez].path/[repos.spel].pathare empty inscaffold.toml; the on-disk location is resolved at runtime from<cache_root>/repos/<name>/<pin>, so the file is portable across machines and CI.--vendor-depsprojects keep relative.scaffold/repos/{lez,spel}literals; an explicit absolutepathset inscaffold.tomlis honored as-is.build [project-path]runssetupand thencargo build --workspace.deploy [program-name]deploys one or all guest programs discovered inmethods/guest/src/bin/*.rsusing prebuilt.binartifacts. After each successful submission it printsprogram_id: <hex>(the risc0 image ID, computed locally from the submitted ELF) and includes it in--jsonoutput on both code paths. Use--jsonfor machine-readable output (recommended for automation):--program-path … --jsonemits the bare per-program object, and the discovery path emits{"deploys":[…]}with one such object per attempted program (see FURPS Functionality #9 for the field contract).build idl [project-path]regenerates the IDL from the project source using the vendoredspelbinary.build client [project-path]regenerates client bindings from the current IDL using the vendoredspelbinary.localnet startwaits until localnet is actually ready (pid alive+127.0.0.1:3040reachable), otherwise fails with diagnostics. The sequencer is daemonized viasetsidso it survives shell/tmux session closure — closing the terminal or detaching a tmux session does not kill the localnet.localnet statusdistinguishes managed process, stale state, and foreign listeners.localnet resetstops the sequencer, clears sequencer chain state, restarts, and verifies blocks. Destructive:--yesis required unless--dry-runis passed (--dry-runprints the plan without changing anything).--reset-walletalso deletes the project wallet home and default-address state (irrecoverable).test-nodemanages isolated, short-lived sequencer instances for integration tests — unlikelocalnet(one long-lived developer sequencer per project on a fixed port), each test node gets its own RPC port, config, database, log, and runtime directory under.scaffold/test-nodes/<id>. Test-node commands follow the caller project's pins:pinsreports the LEZ source/ref, resolved commit, checkout location and ownership, sequencer binary path, and circuits version/path that test-node commands will use — each value annotated with where it came from (CLI override →scaffold.toml→ scaffold default).prepareresolves those pins, materialises the LEZ checkout and circuits release, and builds the standalone sequencer for them; managed cache checkouts may be cloned/re-synced, while caller-provided checkouts ([repos.lez].path, or a local directory passed via--lez-source) are only validated — clean worktree at the requested commit, any origin URL form — and never reset or force-checked-out.doctorreports pin drift, missing/dirty/mismatched checkouts, missing binaries, missing circuits, and unsupported platforms as separate categorized checks.startspawns a node (--port 0/default picks a free port;--state DIRseeds the database from a caller-provided state directory;--block-create-timeout-ms/--retry-pending-blocks-timeout-msoverride sequencer block timing in milliseconds, from 1 to 3,600,000) and prints connection details — machine-readable with--json(rpc_url,pid,state_dir,config_path,log_path,genesis_block_id,block_height).status --node <id>reports health and the served RPC URL.stop --node <id>terminates only that node and removes its runtime state unless--preserve-work-diris passed.run -- <cmd>starts a node, waits for health, runs the command withLGS_TEST_NODE_RPC_URL/LGS_TEST_NODE_PORT/LGS_TEST_NODE_STATE_DIR(and friends) exported, forwards the command's exit status, and stops the node;--serialcaps machine-wide node concurrency at one for low-resource CI,--parallel Nat N. The same surface is available in Rust vialogos_scaffold::api::testnode::{TestNode, TestNodeConfig}, with timing overrides throughBlockTimingOverrideshelper APIs.tx submit/tx wait/tx submit-and-waitgive test harnesses definitive transaction outcomes:submitreturns the tx hash or a structured stateless rejection;submit-and-wait --jsonprints exactly one terminal outcome object —committed(with the actual sequencerblock_idandtimestamp),rejected(phase: stateless|stateful, with reason orobserved_after_block_id),timeout(last_observed_block_id),transport_error, orwire_mismatch— and exits non-zero for anything butcommitted. Stateful rejection follows an explicit observation rule (a configurable number of new blocks past the submission boundary without inclusion), never a single sleep; transport failures are never reported as business rejections. In Rust:node.client().submit_and_wait(&TransactionBytes::borsh_base64(..)?, &WaitOptions::default()).blocks head|range|waitexpose deterministic block context for replay: per block they report the real sequencerblock_idandtimestamp, transaction count, and explicit classification — genesis (the only zero-transaction block; no clock tick to replay), clock-only (empty post-genesis blocks still advance clock state via the mandatory clock transaction), and user transactions — plus per-transaction hashes for public/deployment transactions.clock readreturns all three/LEZ/ClockProgramAccount/…accounts with their decodedblock_id/timestampdata;clock wait-stableis a read barrier that requires consecutive identical samples (head + clock state) before returning, so tests comparing local expected state against an always-ticking sequencer get a consistent snapshot or a retryable timeout. In Rust:client.block_head(),client.blocks(BlockRange { from, to }),client.wait_blocks(after, count, timeout),client.clock_snapshot(ClockReadMode::Stable { samples, timeout }).account get/account batch-get/proof getgive parity assertions stable, block-scoped state reads: every read reports theblock_idit was performed at (head verified identical before and after, retried when a clock block landed mid-read, structured retryable error when the head keeps advancing);batch-getreads all accounts at ONE consistent boundary. Account values distinguishpresent(with lossless base64 account bytes plus decoded balance/nonce/owner/data),missing(never written), anddecode_error(raw payload preserved); proof reads distinguish a missing commitment (proof: null), an invalid commitment (local error before any RPC), and transport failures.snapshot accountswrites a block-consistent JSON snapshot for later comparison. In Rust:client.account(id, ReadAt::Latest),client.accounts(&ids, ReadAt::Block(n)),client.proof(commitment, ReadAt::Latest).state schema|export|seedsupport caller-provided state:schemaidentifies the exact snapshot formats the project's pins accept;seedvalidates a snapshot (lgs-state-snapshot/1JSON with public balances + private commitment accounts, thelgs-account-snapshot/1output ofsnapshot accounts, or a rocksdb state directory) and produces a state directory fortest-node start --state— validation distinguishes format mismatch, storage-schema mismatch (e.g. public-account data, which the pinned genesis config cannot seed), LEZ pin mismatch, and account decode errors, and the output reports the LEZ commit, state format version, and account counts. Config-seeded nodes start from exactly the snapshot's accounts (the sequencer builds genesis state frominitial_public_accounts/initial_private_accounts; no testnet defaults or implicit wallets); database-seeded nodes resume from the copied rocksdb exactly.exportwrites named public-account balances from a running node (the pinned RPC has no account enumeration or private-state export; for full fidelity, stop a node with--preserve-work-dirand seed from its state directory). In Rust:StateSchema::for_project,StateSnapshot::from_file/new,api::testnode::seed_state(&project, input, output).wallet listshows known wallet accounts (wallet account list).wallet topupchecks account state first (wallet account get --account-id ...), runswallet auth-transfer init --account-id ...only when the destination is uninitialized, then performs Piñata faucet claim (wallet pinata claim --to ...). If address is omitted, scaffold uses project default wallet from.scaffold/state/wallet.state.wallet default setstores a project-scoped default wallet address in.scaffold/state/wallet.state.wallet -- ...forwards raw wallet CLI arguments to the project-local wallet binary while preserving project wallet environment.runcombines build (which chainssetup), IDL build, localnet start, wallet topup, and deploy into a single command — the inner loop for day-to-day development. It works with no configuration. If a[run]section withpost_deployis present inscaffold.toml, each hook is executed after deploy viash -c(cwd = project root) withSEQUENCER_URL,NSSA_WALLET_HOME_DIR,LEE_WALLET_HOME_DIR,SCAFFOLD_PROJECT_ROOT,SCAFFOLD_IDL_DIR,SCAFFOLD_TOPUP_SKIPPED, andSCAFFOLD_DEPLOY_SKIPPEDenv vars; when the project has exactly one deployable program,SCAFFOLD_PROGRAM_IDandSCAFFOLD_GUEST_BINare also set. If a localnet is already running it is reused; otherwise it is started, and deploy is skipped when the guest binaries + IDL + config and the sequencer instance are unchanged.--profile NAMEselects a named pipeline from[run.profiles.<name>];--resetwipes sequencer state + wallet and re-seeds before the run (--no-resetoverrides a config-set default);--post-deploy <cmd>(repeatable) overrides the configured hooks and--no-post-deployskips them entirely;--watchre-runs the pipeline on file changes.runcovers the deploy loop only — it does not runwallet -- check-healthor anybasecampcommand.spel -- ...forwards raw spel CLI arguments to the project-vendoredspelbinary so any spel subcommand (inspect,pda,generate-idl, …) runs against the project's pinned version without a global install.basecamp setuppins basecamp +lgpm(read from[repos.basecamp]/[repos.lgpm]— bothbuild = "nix-flake"), builds both (logged to.scaffold/logs/<timestamp>-setup-*.log), and seeds per-profile XDG directories foraliceandbobunder.scaffold/basecamp/profiles/. The two pins move as a set: scaffold's defaultlgpmrev is the one the pinned basecamp release locks, because that same package-manager library is what the app uses to read the moduleslgpminstalled. Existing projects keep whatever they pinned inscaffold.toml— bumping means editing both pins and re-runningsetup. Runtime config (port_base,port_stride) is in[basecamp].basecamp modulesis the sole writer of the captured module set, which lives in top-level[modules.<name>]sections (each withflakeandrole = "project" | "dependency"). Modules aren't basecamp's property — they're the project's Logos modules, which basecamp happens to be one consumer of. Zero-arg runs auto-discovery: walks project flakes (root.#lgxfirst, else immediate sub-flakes), derives amodule_nameper source (frommetadata.json.namefor local paths; heuristic from the github repo slug for remote refs, with a one-line assumption note you can correct inscaffold.toml), then resolves each declared dep name by: (1) already keyed in[modules], (2) the list of modules basecamp bundles itself, (3) the source's ownflake.lock, (4) scaffold-default pin. Unresolved deps fail fast — no silent skip.--flake <ref>/--path <file>capture explicit project sources;--showprints the current set without mutating. Re-runs are idempotent: existing[modules]entries are preserved so hand-edits survive. Project contract: see docs/basecamp-module-requirements.md.basecamp installis pure replay: builds every captured source (dependencies first, then project modules — fail-fast on a broken companion pin) and installs them into bothaliceandbobvialgpm. No source-set flags. If the state is empty on first call it transparently invokesbasecamp modulesin auto-discover mode, prints what was captured, and proceeds. Each nix build logs to.scaffold/logs/<timestamp>-install.logwith a one-line progress status (duration on both success and failure);--print-output(orLOGOS_SCAFFOLD_PRINT_OUTPUT=1) opts back into streaming nix output directly for CI.basecamp launch <profile>scrubs the profile's data/cache under.scaffold/basecamp/profiles/<profile>/, replays captured modules, assigns per-profile ports, and execsbasecampwith the profile's XDG environment plus an absoluteLOGOS_USER_DIR(basecamp 0.2.x resolves its data tree from that; on macOS it ignoresXDG_DATA_HOME, so without it every profile would share one tree). Under 0.2.x the scrub also discards the profile'smodule_data/and basecamp's ownlogs/, both of which live under that root.--log-filetees basecamp's output to.scaffold/basecamp/profiles/<profile>/basecamp.log, or to--log-file=PATH. Before exec, prints a one-line variant-check summary of installed modules so the freeze-on-first-click case (upstream manifest variant mismatch) is visible. The scrub is scoped to the project's own profiles directory and is the whole point of the command — clean-slate semantics on every launch. Custom launch env is declarative viascaffold.toml:[basecamp.env]sets plain vars on every profile,[basecamp.env_append]:-joins path lists (e.g.QT_PLUGIN_PATH,LD_LIBRARY_PATH) onto the valuelgsinherited so basecamp's own paths aren't clobbered, and[basecamp.profiles.<name>.env]sets per-profile vars that win over the global[basecamp.env](e.g. distinctLOGOS_STORAGE_API_PORTforalicevsbob).basecamp develop <module>resolves the module's flake from[modules.<module>], strips its#lgxoutput fragment, and execsnix develop <flake>from the project root — so an in-shelllgsresolves this project via its normal cwd-upward search (the dev shell starts in the project root). It also exportsSCAFFOLD_PROJECT_ROOT/LOGOS_PROFILEas context for scripts in the shell (project discovery itself doesn't read them).--dev-shell <attr>selects a non-default dev shell (nix develop <flake>#<attr>). An unknown module name fails fast with the captured-module list before anynixinvocation. This is the verb-set-symmetry wrapper so contributors stop reaching for rawcd <module> && nix develop.basecamp build-portablerebuilds everyrole = "project"entry in[modules]with attr-swapped#lgx-portablefor hand-loading into a basecamp AppImage. Sources come from scaffold.toml (managed viabasecamp modules);--module NAMEnarrows the run to one of them.role = "dependency"entries are intentionally skipped — the target AppImage provides its own release companion modules via its Package Manager catalog. Output is ordered topologically bymetadata.jsondependencies (leaves first, so basecamp's AppImage can resolve each module's deps before loading it), and symlinked into.scaffold/basecamp/portable/as<NN>-<module_name>.lgxso the AppImage's "install lgx" file picker has browsable, human-named files in the right order. The directory is wiped and recreated per run.basecamp buildbuilds the project's.lgxartefacts without installing them.--variantpickslgx,lgx-portable, orall(the default);--module NAMErestricts the build to one captured project module.build-portableis the alias forbuild --variant lgx-portable.basecamp run <module>runs a captured module standalone vianix run <flake>, without starting basecamp itself.basecamp paths <profile>prints the resolved per-profile path manifest: XDG dirs, runtime dir, module and plugin dirs, and the log file.--jsonfor machine-readable output.basecamp docsprints the embedded copy of basecamp-module-requirements.md, so the project contract is available offline and at the pinned version.basecamp doctoremits a basecamp-specific health report: the resolved basecamp/lgpmpin set (warning only when one of the pair is at scaffold's default and the other isn't — the two are read as one set), captured modules summary (each entry's flake ref, parsed tag/commit annotation for github refs, and any API headers already installed in alice's profile), manifest variant check per seeded profile (flags modules whosemainis missing the current-platform-devkey — the freeze-on-first-click failure mode), dep-pin drift (capturedrole = "dependency"rev vs. scaffold default), and auto-discovery drift (project sources discoverable today but absent from the captured set).--jsonfor machine-readable output.doctorprints actionable checks and next steps;--jsonis for CI/machine parsing.reportcreates a.tar.gzdiagnostics bundle for GitHub issues using strict allowlist collection with redaction and explicit skip reporting.completions <shell>prints a shell completion script to stdout. Supported shells:bash,zsh. The generated script covers bothlgsandlogos-scaffold.- Wallet-facing commands accept
LOGOS_SCAFFOLD_WALLET_PASSWORDfor password override (fallback: local dev default).