Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open more actions menu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
49 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

aps

A tiny Swift CLI that dogfoods AppState outside SwiftUI: declare typed app state, get/set/watch/dump it, and show dependency injection.

Current release line: 1.0.0. Targets macOS, Linux, and Windows where AppState allows. Keys come from <state-root>/schema.json (demo defaults materialize on first use); see the dynamic schema RFC.

This repository is gated by the CorvidLabs trust toolchain (fledge, spec-sync, augur, attest). See AGENTS.md.

Install

Homebrew:

brew install 0xLeif/tap/aps

fledge plugin (this repo is the plugin; no separate hub repo needed):

fledge plugins install 0xLeif/aps-cli
fledge aps keys --json

Mint (builds the SwiftPM executable from source):

mint install 0xLeif/aps-cli

Source (Swift 6.0+):

git clone https://github.com/0xLeif/aps-cli.git
cd aps-cli && swift build -c release
.build/release/aps --help

Foreign GitHub Actions workflows: use the reusable composite Action to install the release binary without a Swift toolchain:

- uses: 0xLeif/aps-cli/.github/actions/install-aps@8e2108601b182584e59b3e534b67199247593a0a
  with:
    version: 1.0.0
- run: aps set note "run-${{ github.run_id }}"

The Action selects the release asset for Linux x64, macOS x64, or macOS arm64, verifies its .sha256 sidecar before moving it into the job-scoped ${RUNNER_TEMP}/aps/bin, and adds that directory to PATH. Linux releases are portable tar bundles containing the Swift runtime libraries, so no Swift toolchain is needed. The existing v1.0.0 release predates this Action and does not contain the portable Linux bundle; use a release built by the current release workflow for Linux. The Action defaults APS_HOME to ${RUNNER_TEMP}/aps-home through GITHUB_ENV; an existing APS_HOME is preserved. Pin the Action to a release tag or pass an explicit semantic version. Windows is not supported until a Windows release asset is published.

Commands

aps [--state-dir PATH] <subcommand> ...
aps get <key> [--json] [--state-dir PATH]
aps set <key> <value> [--json] [--state-dir PATH]
aps watch <key> [--count N] [--timeout SEC] [--jsonl] [--interval MS] [--state-dir PATH]
aps dump [--json] [--state-dir PATH]
aps keys [--json] [--state-dir PATH]
aps key add|remove|list ...
aps stats [--json] [--watch] [--count N] [--timeout SEC] [--state-dir PATH]
aps reset <key> [--json] [--state-dir PATH]
aps reset --all [--json] [--state-dir PATH]
aps reset --registered [--json] [--state-dir PATH]
aps schema [--json] [--state-dir PATH]
aps --help
aps --version

State root resolution: --state-dir (root form aps --state-dir PATH <cmd> or on the subcommand; subcommand wins) > APS_HOME > ~/.aps.

Default demo keys (seed schema)

On first use, aps materializes <state-root>/schema.json with these seed keys:

Key Type Storage Lifetime
counter Int State Process (in-memory)
message String State Process (in-memory)
flag Bool StoredState Persisted (UserDefaults; CLI calls synchronize() so Linux flushes)
note String FileState Persisted ($APS_HOME/note.json)
profile {name,version} FileState Persisted structured Codable ($APS_HOME/profile.json)
secret String EncryptedFile Persisted encrypted under the state root (secret.enc)
profileName String Slice Projection of profile.name via AppState Slice

Add or remove keys at runtime:

aps key add smokeNote --type String --storage FileState --path smoke-note.json --initial ''
aps set smokeNote hello
aps key remove smokeNote --purge
aps key list --json

Encrypted-file secret store (secret)

secret is backed by an age-style encrypted envelope under the state root (issue #35), not the Keychain: ephemeral X25519 ECDH + HKDF + ChaCha20-Poly1305 via swift-crypto, the same construction as AlgoChat's message encryptor.

  • secret.enc holds the encrypted envelope (ephemeral public key, nonce, ciphertext, tag, base64 JSON). Nothing plaintext at rest.
  • Key file mode (default): a recipient key is generated on first use at <state-root>/secret.key (base64 X25519, mode 0600), like an SSH key. Zero prompts, works headless and in CI, on every OS.
  • Passphrase mode: set APS_SECRET_PASSPHRASE to derive the key from a passphrase via HKDF-SHA256 (no key file). Wrong passphrases fail loudly with secretUnlockFailed on both get and set.
  • Stateful gating: until secret.enc exists, the first write seals with whichever recipient is active (key file or passphrase). After that, set must unlock the existing envelope before rewrite; a wrong passphrase cannot silently re-key.
  • A corrupt secret.enc envelope blocks both get and set with decoding_failed until you run aps reset secret (or repair the file).
  • Interactive opt-in: with APS_SECRET_USE_PASSPHRASE=1 on a TTY, aps prompts once itself (its own getpass prompt, not macOS Keychain's).
  • aps reset secret deletes secret.enc; the key file is kept for future writes.
  • aps reset --all restores DemoKey seed keys only (safe for agent FileState keys). Use aps reset --registered to wipe every key in schema.json.
  • The previous AppState SecureState / Keychain backend was replaced: ad-hoc signed CLI binaries can never earn durable Keychain trust, so every access prompted for a password. AppState itself is unchanged; SecureState remains dogfooded in AppStateExamples.

Dependencies

aps injects real services with @AppDependency / Application.dependency, plus one @ObservedDependency consumer for AppState's observable dependency surface:

  • clock : wall clock for dump timestamps (@AppDependency)
  • jsonCoding : shared JSON encoder helpers for aps dump (@AppDependency)
  • stats : process-local mutation counters (DemoStats / @ObservedDependency); aps stats reads them and aps stats --watch surfaces Combine updates

Requirements

  • Swift 6.0+
  • macOS 14+ (primary CI on macos-latest). Linux smoke runs on ubuntu-latest. Windows smoke runs on windows-latest via Scripts/smoke.ps1.
  • For the trust gate locally: corvid-trust (brew install CorvidLabs/tap/corvid-trust)
  • SpecSync 5.2.0 (see .specsync/version). Trust CI mirrors that exact release; brew spec-sync latest should match.

Build and run

git clone https://github.com/0xLeif/aps-cli.git
cd aps-cli
swift build
swift run aps --help

Or through fledge:

fledge lanes run verify

Fledge plugin shim

This repo ships a root plugin.toml, so aps-cli is itself the fledge plugin (fledge-plugin-aps v1.0.0 tracks the CLI version). Install straight from this repo: fledge plugins install 0xLeif/aps-cli. The earlier hub snapshot repo (0xLeif/fledge-plugin-aps) is retired; aps-cli is the only source of truth.

# From a clone of aps-cli:
fledge plugins install .          # live-link; rebuilds release binary via the build hook
fledge aps keys --json            # same CLI, invoked through fledge
fledge plugins validate .         # also runs in the verify lane (manifest drift fails CI)

Release build:

swift build -c release
.build/release/aps dump

Examples

swift run aps keys
swift run aps set counter 3
swift run aps set flag true
swift run aps set note "saved across launches"
swift run aps set profile '{"name":"agent","version":1}'
swift run aps dump
swift run aps watch note --interval 200 --count 2 --timeout 5
swift run aps reset --all
swift run aps reset --registered

Agent usage

swift run aps schema                # self-describing contract: keys, commands, payloads, errors
swift run aps get note --json
swift run aps set counter 3 --json
swift run aps dump --json
swift run aps keys --json
swift run aps reset note --json

APS_HOME=/tmp/aps-agent swift run aps set note "isolated state"
swift run aps --state-dir /tmp/aps-agent get note --json
swift run aps get note --json --state-dir /tmp/aps-agent

swift run aps watch note --count 2 --timeout 5 --jsonl

swift run aps set profile '{"name":"agent","version":1}' --json
swift run aps get profile --json
swift run aps set profileName agent --json
swift run aps stats --json

swift run aps key add agentNote --type String --storage FileState --path agent-note.json --initial ''

aps schema is the contract endpoint: one cacheable JSON document with cliVersion, integer schemaVersion (bumped when the document shape changes; currently 4), live userSchema meta (formatVersion, keyCount, hash), state-root precedence, every registered key and command, payload shapes, and the error-code table. Live values stay in aps dump. ArgumentParser's full command tree is also available as JSON via aps <cmd> --experimental-dump-help.

watch uses Swift Observation for in-process updates and polls as a fallback so disk-backed FileState / StoredState changes can still surface, including updates written by another aps process. For note and profile, polling reads the JSON files directly so AppState's FileState cache cannot hide cross-process writes.

Output modes (human and agent)

aps follows the git porcelain rule: human output may be pretty, machine output is a frozen contract.

  • On a TTY: keys renders an aligned table with a bold header, JSON is pretty-printed, and semantic color is used sparingly (set NO_COLOR to disable).
  • When piped: keys is byte-stable TSV with no ANSI escapes, and all JSON is single-line compact.
  • watch --json is an alias for --jsonl; keys --quiet prints key names only (handy for xargs aps reset).
  • Shell completions: aps --generate-completion-script bash|zsh|fish (install into your shell's completion directory).

watch termination is observable in both channels. Exit codes: 0 when --count is satisfied, 124 on --timeout (GNU convention), 130 on SIGINT, 143 on SIGTERM. In --jsonl mode the stream ends with a terminal {"type":"end","reason":"count|timeout|sigint|sigterm",...} event and never contains non-JSON lines; in human mode a one-line summary goes to stderr. An unbounded watch prints a one-time stderr hint suggesting --count / --timeout.

Multi-process FileState semantics

aps expects one writer per key at a time. Concurrent aps set on the same FileState key (note / profile) is last-writer-wins and is not locked: AppState writes files non-atomically, so two writers can tear a JSON file.

When a FileState file exists but is undecodable (torn write), aps does not fall back to AppState's initial/cached value on the direct disk path:

  • get / watch for note, profile, and profileName fail with corruptState and exit code 65 (EX_DATAERR)
  • watch --jsonl emits one {"type":"error","error":"corruptState",...} line before exiting
  • Missing files still resolve to the key's initial value (same as a fresh store)

AppState itself is unchanged; this is an aps dogfood/CLI contract only. Repair with aps reset <key> (or delete the torn file under the state root).

Error contract

Domain errors always print a human line to stderr and keep stdout empty, with a sysexits-aligned exit code:

Code Meaning When
0 success stdout contract satisfied
64 EX_USAGE caller-fixable input: bad key/flags, invalid value, unknown_key, schema_conflict
65 EX_DATAERR corrupt or undecodable persisted state, or invalid schema.json (schema_invalid)
70 EX_SOFTWARE internal bug
73 EX_CANTCREAT write did not persist (unwritable state root)

64 means fix the invocation; 65+ means environment or data; 70 means an aps bug. Missing state files are not errors: they mean the initial value.

With --json / --jsonl, or when APS_ERROR_JSON=1, stderr additionally gets one structured envelope:

{"error":{"code":"invalid_value","hint":"Run `aps keys` to see expected types per key.","message":"Invalid value 'nope' for counter (Int)"}}

code is stable and safe to match on: invalid_value, encoding_failed, decoding_failed, persistence_failed, corrupt_state, schema_invalid, unknown_key, schema_conflict, secret_unlock_failed.

Tests and smoke

swift test
./Scripts/smoke.sh
# Windows / PowerShell 7+ (same behavioral coverage as smoke.sh):
pwsh ./Scripts/smoke.ps1

CI

Workflow Runner Role
.github/workflows/ci.yml macos-latest build / test / smoke
.github/workflows/linux-smoke.yml ubuntu-latest Linux build + smoke
.github/workflows/windows-smoke.yml windows-latest Windows swift test + PowerShell smoke
.github/workflows/trust.yml macos-latest CorvidLabs Trust gate

Trust toolchain

File Purpose
fledge.toml Tasks + verify lane
.trust.toml Unified Trust policy
.augur.toml Diff-risk thresholds
.attest.json Provenance policy
.specsync/ SpecSync 5.2.0 config + SDD change tracking (.specsync/version)
specs/ Module contracts (aps-cli, state-store)
GOAL.md Shipped 1.0.0 release record
AGENTS.md Standing rules (managed block required by CI)
fledge trust doctor
fledge trust verify

Layout

Package.swift
Sources/aps/
Tests/apsTests/
specs/
docs/design/
Scripts/smoke.sh
Scripts/smoke.ps1
GOAL.md
LICENSE
.github/workflows/{ci,linux-smoke,windows-smoke,trust,release,post-release-formula}.yml

Next goal

1.0.0 is shipped and public: release v1.0.0, GOAL.md record, go-public checklist #40. Next steps live in the issue backlog, starting with release binaries and the Homebrew tap formula (#68).

AppState surface coverage

AppState surface Demo key / command Status
State counter, message Dogfooded
StoredState flag Dogfooded
FileState note, profile Dogfooded
SecureState (none) Not dogfooded here; moved to AppStateExamples after the Keychain prompt issue (issue #35)
Slice profileName Dogfooded
@AppDependency clock, jsonCoding Dogfooded
@ObservedDependency stats / aps stats Dogfooded
SyncState - No-go (spike)
ModelState - No-go (spike)
OptionalSlice / DependencySlice - Not planned

Non-goals

  • No iCloud SyncState or SwiftData ModelState (see docs/spikes/)
  • No in-process plugin/daemon/network API inside aps itself (the repo may still be a fledge plugin shim; see above)

Windows / tri-OS readiness

Audit findings and per-OS gaps live in docs/windows-readiness.md. CI runs the full matrix on GitHub-hosted runners (macos-latest, ubuntu-latest, windows-latest).

Design

Related

Morty Proxy This is a proxified and sanitized view of the page, visit original site.