Skip to content

Latest commit

 

History

History
151 lines (111 loc) · 6.66 KB

File metadata and controls

151 lines (111 loc) · 6.66 KB

Contributing to hubstaff-cli

Thanks for your interest in contributing. This guide is the map for landing a change.

Architecture

hubstaff is a schema-driven CLI: every subcommand you can run is derived from the live OpenAPI document at https://api.hubstaff.com/v2/docs. There is no per-endpoint handler code. The schema is fetched once, cached on disk, and traversed on each invocation to turn hubstaff <resource> <action> into an HTTP call.

System Overview

flowchart LR
    ARGS["CLI args"] --> CLAP["clap<br/>(main.rs)"]
    CLAP --> DISPATCH["dispatch<br/>(api.rs)"]
    DISPATCH --> IDX["command_index.rs<br/>slug → operationId"]
    DISPATCH --> SCHEMA["schema.rs<br/>OpenAPI cache"]
    IDX --> SCHEMA
    SCHEMA --> CLIENT["client.rs<br/>HTTP + auth refresh"]
    AUTH["auth.rs<br/>token store"] --> CLIENT
    CONF["config.rs<br/>TOML"] --> CLIENT
    CLIENT --> API[("Hubstaff API v2")]
    API --> CLIENT
    CLIENT --> OUT["formatted output<br/>(colored_json)"]
Loading

Command Dispatch

flowchart TD
    IN["hubstaff projects list --org 42"] --> PARSE["clap: resource='projects', action='list'"]
    PARSE --> LOOKUP["command_index: (projects, list) → operationId"]
    LOOKUP -->|miss| ERR1["error: unknown command"]
    LOOKUP -->|hit| OP["schema.operation(operationId)"]
    OP --> BIND["bind path/query/body params<br/>from remaining args"]
    BIND --> REQ["client.send(method, url, auth)"]
    REQ -->|401| REFRESH["auth.refresh() → retry once"]
    REQ -->|2xx| RENDER["render JSON<br/>(pretty or raw)"]
    REQ -->|4xx/5xx| ERR2["error → exit code"]
Loading

Schema Caching Lifecycle

flowchart LR
    START["CLI start"] --> HAS{"cache present?"}
    HAS -->|no| FETCH["GET /v2/docs"]
    HAS -->|yes| COND["GET /v2/docs<br/>If-None-Match: <etag>"]
    COND -->|304| USE["use cached docs.json"]
    COND -->|200| WRITE
    FETCH --> WRITE["write docs.json + meta.toml<br/>(atomic via persistence.rs)"]
    WRITE --> BUILD["rebuild command_index.json"]
    USE --> READY
    BUILD --> READY["ready to dispatch"]
Loading

Cache lives under $XDG_CONFIG_HOME/hubstaff/schema/v2/ (or the OS equivalent). It is keyed by api_url, so pointing at staging gives you a separate cache.

Module Map

Module Responsibility
src/main.rs clap entrypoint, top-level subcommand routing
src/api.rs dynamic dispatch for schema-driven commands
src/command_index.rs slug ↔ operationId mapping; snapshot-tested command table
src/schema.rs OpenAPI fetch, ETag-conditional refresh, disk cache
src/client.rs HTTP (reqwest blocking + rustls), auth-aware retries
src/auth.rs personal access token storage and refresh
src/config.rs TOML config I/O, XDG path resolution, defaults
src/config_commands.rs hubstaff config subcommand handlers
src/commands_list.rs hubstaff list — discovery output
src/check.rs hubstaff check diagnostics (config, credentials, reachability, schema cache)
src/persistence.rs atomic file writes (tempfile + rename)
src/error.rs error enum → exit code (1 API, 2 auth, 3 config, 4 network)

Agent skills

skills/ holds Markdown playbooks that teach an AI agent to accomplish a task with this CLI. They ship as plain files — no MCP server, no extra credentials. Contributions are welcome; see skills/CONTRIBUTING.md for the authoring workflow, the verification we expect, and the acceptance checklist.

Skills live in this repo rather than in the docs site because their commands are derived from the API schema, so CI can assert that every command a skill names still resolves. Hosted elsewhere they would rot silently the first time a path changed.

Local Development

Toolchain is pinned by rust-toolchain.toml (Rust 1.95.0, edition 2024). Install the tooling once:

just install-tools    # cargo-deny, cargo-audit, cargo-auditable

Testing

just test                              # cargo test --all-features
cargo test --test commands_test        # integration suite only
cargo test <name>                      # filter by test name

Unit Tests

#[cfg(test)] blocks in src/* cover the internals: auth, client, config, command_index, schema, check, error. Keep new unit tests next to the code they exercise.

Integration Tests

tests/commands_test.rs runs the compiled binary end-to-end. HTTP is stubbed with mockito and each test gets an isolated XDG home so cache state never leaks between cases. Helpers at the top of the file:

  • cli_bin() — absolute path to the compiled hubstaff binary.
  • temp_xdg() — fresh temp dir to use as XDG_CONFIG_HOME.
  • run(&["args", ...], &xdg_dir) — spawn the binary, return (stdout, stderr, exit_code).
  • seed_schema_cache(&xdg_dir) — preload tests/fixtures/schema.json into the cache so tests don't hit the network.
  • seed_schema_cache_with_source_url(&xdg_dir, url) — same, but tags the cache with a specific api_url for environment-switching tests.

Follow the existing pattern: spin up a mockito server, call set api_url against it, seed the cache (or let the test exercise the fetch path), then run the command under test.

Snapshot Tests

src/command_index.rs::schema_command_table_snapshot uses insta to freeze the schema-to-command-table rendering. If your change alters the command index (new endpoints, renamed resources, argument reshuffles), the snapshot will diff — refresh it:

just refresh-schema-fixture

That recipe re-downloads tests/fixtures/schema.json from production and runs INSTA_UPDATE=auto cargo test schema_command_table_snapshot. Review the diff carefully before committing the regenerated fixture and snapshot — this is the review surface for command-shape drift.

Code Quality

This project uses just as a task runner. Install with brew install just or cargo install just.

Run before submitting:

just ci        # lint + deny + test + audit — mirrors CI exactly

Faster local gate:

just check     # lint + test

Lint policy is enforced via Cargo.toml:

  • unsafe_code = "deny" — no unsafe Rust.
  • clippy::pedantic = "deny" with three targeted allows: doc_markdown, must_use_candidate, missing_errors_doc. CI runs cargo clippy --all-targets --all-features -- -D warnings, so pedantic findings block merge.

Release builds run through cargo auditable so dependency provenance is embedded in the binary — this is what just build-release does.

Supply chain: cargo deny check gates license and advisory policy (deny.toml), and cargo audit runs against RustSec. Both are part of just ci.