AGENTS.md 9.2 K raw
1
# AGENTS.md
2
3
Notes for whoever (human or agent) works on this codebase next.
4
5
## Layout
6
7
```
8
src/
9
├── main.rs            # clap CLI: import / list / open (default) / info / edit / rename / delete / path
10
├── lib.rs             # re-exports the modules below for the binary + tests
11
├── app.rs             # App state, vim mode machine, ratatui run loop
12
├── input.rs           # keymap: Normal / Insert / Command modes, popups
13
├── ui.rs               # rendering: sidebar, url bar, editor tabs, response, popups
14
├── highlight.rs         # JSON/XML tokenizer -> styled ratatui Lines
15
├── model.rs            # Collection / SavedRequest / KeyValueRow / FieldDoc / OAuthConfig (serde)
16
├── store.rs            # ~/.config/cielago persistence, AppConfig
17
├── openapi/
18
│   ├── loader.rs        # load spec from file path or http(s) URL, JSON/YAML
19
│   ├── resolve.rs        # local `#/...` $ref resolution (cycle-safe)
20
│   ├── examples.rs        # schema -> example JSON value generation
21
│   ├── docs.rs             # schema -> FieldDoc (types, enums) for the Docs tab
22
│   └── import.rs            # Spec -> Collection conversion
23
└── http/
24
    ├── client.rs           # reqwest request building + response capture
25
    ├── oauth.rs              # client-credentials token exchange
26
    ├── send.rs                # send_with_auth: cache/refresh token, 401 retry
27
    ├── url_input.rs            # pasted URL -> origin / path / query (inverse of build_url)
28
    └── vars.rs                 # {{name}} + dynamic ({{uuid}}, {{randomInt}}…) substitution
29
```
30
31
`tests/` has fixture-driven integration tests: `import_tests.rs` (spec →
32
collection), `http_tests.rs`/`app_send_tests.rs` (wiremock-backed HTTP +
33
OAuth), `input_tests.rs` (full keymap flows over an in-memory `App`),
34
`ui_tests.rs` (draws into a ratatui `TestBackend` and asserts on cell colours
35
— the only place rendering is covered).
36
37
## Design decisions worth knowing
38
39
- **The project was renamed `getman` → `stableman` → `manpost` → `cielago`.**
40
  `store::config_dir` moves a leftover `~/.config/{manpost,stableman,getman}`
41
  onto `~/.config/cielago` on first use (see `LEGACY_DIR_NAMES`), so existing
42
  collections survive. Drop those migrations once they've had time to run
43
  everywhere.
44
- **No remote `$ref`s.** `openapi::resolve` only follows local
45
  `#/components/...` JSON pointers. Specs that split across files aren't
46
  supported — bundle them first if you hit this.
47
- **Secrets are plaintext.** `OAuthConfig.client_secret` is saved as-is in
48
  the collection JSON (explicit user choice, not an oversight). The
49
  in-memory `OAuthToken` obtained from it is never persisted.
50
- **Tags become sidebar groups**, first tag only; untagged requests land in
51
  a `default` group. This wasn't asked for explicitly but was cheap and
52
  matches how most specs are organized.
53
- **`Method::parse`, not `FromStr`** — deliberately not the trait, to dodge
54
  a clippy lint; nothing else depends on `FromStr`.
55
- **Vim modes are `Normal` / `Insert` / `Command` / `Search`** — no Visual
56
  mode. Insert mode is reused for both single-line field edits (`LineEdit`)
57
  and the body `TextArea`; `app.editing` discriminates which. `Search` is the
58
  `/` sidebar filter: it re-applies on every keystroke, and `app.filter` (the
59
  committed query) is deliberately separate from `app.search` (the live
60
  prompt buffer) so `Esc` can drop the prompt without touching the filter.
61
- **Sidebar labels are a view concern.** `SavedRequest` keeps `name` (user
62
  editable), `summary` and `operation_id` (verbatim from the spec);
63
  `Collection.label_mode` picks which one renders. `:rename-all` is the only
64
  thing that overwrites `name`. Import prefers `summary` over `operationId`
65
  for the initial name — most real specs put a generated controller method
66
  name in `operationId`.
67
- **Switching collections reassigns the whole `App`** (`switch_collection` does
68
  `*self = App::new(...)`). Everything view-related is derived from the
69
  collection, so there's nothing to migrate by hand — and dropping the mpsc
70
  channel and cached `OAuthToken` is a feature, not collateral: a response still
71
  in flight for the previous collection can no longer land in the new one, and
72
  the token belonged to the old `auth` config. It deliberately does *not* go
73
  through a `pending_*` field like `run_external_edit` does; that indirection
74
  only exists because the editor needs the `&mut Terminal` to suspend raw mode,
75
  and routing a switch through the run loop would put it out of reach of
76
  `input_tests.rs`.
77
- **Pasting a URL rewrites collection state.** `App::apply_url_input` +
78
  `http::url_input` split an absolute URL into origin / path / query: the origin
79
  is added to `servers` (deduped on `trim_end_matches('/')`, since `E`-added
80
  servers may carry a trailing slash) **and made active**, because the URL bar
81
  renders `base_url() + path` and would otherwise show something other than what
82
  was just pasted. Query rows are replaced only when the input actually contained
83
  a `?` — otherwise fixing a typo'd path would silently wipe the disabled
84
  optional params an import set up. Two traps the module exists to handle:
85
  `Url::parse("localhost:8080/x")` *succeeds* with scheme `localhost` (hence the
86
  http/https + host check), and `{`/`}` are in the crate's path encode set, so
87
  `/pets/{id}` comes back as `/pets/%7Bid%7D` and needs `restore_braces`.
88
- **`path_params` follows the path.** `SavedRequest::sync_path_params` derives
89
  the rows from `{placeholders}` in `path`, pruning ones that no longer appear —
90
  `build_url` ignores those anyway, so a stale row only makes the Params tab
91
  lie. `{{variables}}` are skipped by the scanner. Import does *not* call it:
92
  spec-declared rows are authoritative there.
93
- **`$EDITOR` integration** shells out synchronously, suspending raw mode
94
  around it (`app::run_external_edit`). It writes/reads a temp file rather
95
  than piping, so it works with any editor.
96
- **Highlighting is hand-rolled** (`highlight.rs`), line-oriented, and never
97
  drops input: every character comes back out in some span (there's a test).
98
  A `syntect`-class dependency would be larger than the rest of the binary,
99
  and JSON/XML/plain is all a request client shows.
100
- **The body has two renderers.** `tui-textarea` styles whole lines only, so
101
  the Body tab renders a highlighted `Paragraph` in Normal mode and the raw
102
  `TextArea` in Insert mode. The textarea stays the source of truth either
103
  way; the read-only view scrolls by moving *its* cursor, which is why `j`/`k`
104
  on the Body tab drive `CursorMove`.
105
- **Dynamic variables live in the `{{…}}` namespace**, not a second syntax:
106
  `{{uuid}}`, `{{randomInt(1,10)}}`, `{{isoTimestamp}}`. A collection variable
107
  shadows a dynamic one of the same name (`{{$name}}` forces the dynamic one),
108
  so a fixed `uuid` can be pinned for debugging. Randomness comes from UUID v4
109
  bytes and the RFC 3339 formatter is hand-written — both to avoid adding
110
  `rand`/`chrono` for a handful of values.
111
- **Collections carry a saved view.** `last_request` (request id),
112
  `last_focus` (pane `1`/`2`/`3`) and `last_tab` are written by
113
  `App::record_view` when `:w` runs, and replayed by `App::new` — it reopens
114
  the request (expanding its group if `groups_collapsed` would hide it), then
115
  restores the pane and tab. Two deliberate wrinkles: the view is recorded
116
  *during* `save` rather than by `select_request`, because marking the
117
  collection dirty just for moving the sidebar cursor would make `:q` nag
118
  after a read-only browse (the trade: navigate away, quit clean, and the old
119
  position stays); and a saved `Response` focus falls back to the editor,
120
  since responses aren't persisted and pane 3 is empty on open.
121
  `Focus`/`EditorTab` stay in `app.rs` and gained serde derives, so `model.rs`
122
  reaches back into `app` for those two types.
123
- **CLI names resolve by slug** (`store::match_name`): the file is named after
124
  `slugify(name)` anyway, so `delete "some api"` and `delete some-api` both hit
125
  `Some API`. Exact name wins first. `store::resolve_collection` is the entry
126
  point and produces the "Available: …" error every name-taking command shares.
127
- **`cielago edit` edits a temp copy, not the file.** It only writes back after
128
  the edited text parses as a `Collection`; on a parse error the temp file is
129
  left in place and its path printed, so a botched edit is recoverable. A `name`
130
  changed in the editor is a rename (file moves, `config.last_collection`
131
  follows), and it bails rather than overwriting a different collection whose
132
  name slugifies the same.
133
- **`FieldDoc`s are stored on the request**, not read from the spec on demand:
134
  a collection's `spec_source` is often a URL that has moved or needs auth by
135
  the time the collection is opened. Cost is a re-import to refresh them, and
136
  older collections having none.
137
138
## Before committing
139
140
```sh
141
cargo fmt
142
cargo clippy --all-targets   # keep this clean, no warnings
143
cargo test
144
```
145
146
## Known gaps (intentionally out of scope for v1)
147
148
Swagger 2.0, non-client-credentials OAuth flows (auth-code, API key),
149
collection folders beyond tag grouping, request history/response diffing.
150
The Docs tab covers request inputs only — response schemas and status codes
151
aren't imported.