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