insightfactory-cli 1.0.1.dev5__tar.gz → 1.0.1.dev9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/.github/workflows/release.yml +3 -1
  2. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/CLAUDE.md +9 -4
  3. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/PKG-INFO +39 -15
  4. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/README.md +38 -14
  5. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/pyproject.toml +1 -1
  6. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/api.py +22 -5
  7. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_api.py +40 -0
  8. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/.github/workflows/ci.yml +0 -0
  9. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/.github/workflows/claude.yml +0 -0
  10. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/.gitignore +0 -0
  11. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/.python-version +0 -0
  12. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/AGENTS.md +0 -0
  13. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/__init__.py +0 -0
  14. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/__main__.py +0 -0
  15. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/cache.py +0 -0
  16. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/cli.py +0 -0
  17. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/colour.py +0 -0
  18. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/__init__.py +0 -0
  19. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/config.py +0 -0
  20. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/login.py +0 -0
  21. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/logout.py +0 -0
  22. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/profiles.py +0 -0
  23. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/set_token.py +0 -0
  24. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/commands/token.py +0 -0
  25. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/config.py +0 -0
  26. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/constants.py +0 -0
  27. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/http.py +0 -0
  28. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/main.py +0 -0
  29. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/oauth.py +0 -0
  30. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/src/if_cli/runtime.py +0 -0
  31. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/__init__.py +0 -0
  32. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/cache_writer.py +0 -0
  33. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/conftest.py +0 -0
  34. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/helpers.py +0 -0
  35. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/servers.py +0 -0
  36. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_api_command.py +0 -0
  37. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_cache.py +0 -0
  38. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_cli.py +0 -0
  39. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_config.py +0 -0
  40. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_config_command.py +0 -0
  41. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_login.py +0 -0
  42. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_oauth.py +0 -0
  43. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_oauth_flow.py +0 -0
  44. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_profiles.py +0 -0
  45. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_programmatic_api.py +0 -0
  46. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_runtime.py +0 -0
  47. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_set_token.py +0 -0
  48. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/tests/test_token.py +0 -0
  49. {insightfactory_cli-1.0.1.dev5 → insightfactory_cli-1.0.1.dev9}/uv.lock +0 -0
@@ -1,6 +1,8 @@
1
1
  name: release
2
2
 
3
- # CD: merge to develop/main (or a v* tag) publishes to public PyPI.
3
+ # CD: a push to develop publishes a `.dev<run_number>` pre-release; a `v*` tag
4
+ # publishes the release. A merge to main runs CI and publishes nothing -- see
5
+ # if_s_insightfactory_cli.yml for why releasing is a deliberate tag push.
4
6
  # Verify + build live in insightfactory-ai/if_sre_github_actions
5
7
  # (if_s_insightfactory_cli.yml). The publish job stays here because PyPI
6
8
  # Trusted Publishing is registered for this repository, workflow `release.yml`,
@@ -9,6 +9,7 @@ the `if-cli` executable. It uses Python 3.10+, uv, pytest, ruff, and ty.
9
9
  uv sync
10
10
  uv run pytest
11
11
  uv run ruff check .
12
+ uv run ruff format --check .
12
13
  uv run ty check
13
14
  ```
14
15
 
@@ -22,7 +23,11 @@ uv run ty check
22
23
  Node CLI (`~/.insightfactory`, `INSIGHTFACTORY_CONFIG_DIR` override).
23
24
  - Do not commit credentials, access tokens, refresh tokens, or client secrets.
24
25
  - Test fixtures use only neutral hostnames (`factory.example`).
25
- - Do not publish to PyPI or create release tags from a developer machine;
26
- merges to `develop` / `main` (and optional `v*` tags) publish via the
27
- reusable workflow in `if_sre_github_actions` plus the OIDC job in
28
- `.github/workflows/release.yml`.
26
+ - Do not publish to PyPI from a developer machine. Publishing happens in CI: the
27
+ reusable workflow in `if_sre_github_actions` builds and stages, and the OIDC job
28
+ in `.github/workflows/release.yml` uploads.
29
+ - **A release is a pushed `v*` tag, not a merge.** Merging to `main` runs CI and
30
+ publishes nothing; pushing `v<version>` publishes that version, and the tag must
31
+ match `pyproject.toml`. Pushing `develop` publishes a `.dev{run_number}`
32
+ pre-release. Creating the release tag is a deliberate human act — do not create
33
+ or push one unless asked to cut a release.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: insightfactory-cli
3
- Version: 1.0.1.dev5
3
+ Version: 1.0.1.dev9
4
4
  Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
5
5
  Project-URL: Homepage, https://github.com/insightfactory-ai/insightfactory-cli
6
6
  Project-URL: Repository, https://github.com/insightfactory-ai/insightfactory-cli
@@ -102,6 +102,12 @@ prefixes.
102
102
  Override the config directory with `INSIGHTFACTORY_CONFIG_DIR` (useful in tests
103
103
  and CI). The default is `~/.insightfactory`.
104
104
 
105
+ The request method goes in `-X`; `if-cli api` takes exactly one positional
106
+ argument and reads it as the path. A `curl`-shaped `if-cli api GET /api/schedules`
107
+ therefore parses `GET` as the path — write `if-cli api -X GET /api/schedules`
108
+ instead. The CLI names the misplaced verb rather than reporting a problem with
109
+ `/api/schedules`.
110
+
105
111
  On Git Bash (MSYS2) on Windows, path conversion rewrites a leading-slash
106
112
  argument such as `/api/agent-projects` into a Windows path. Prefix the command
107
113
  with `MSYS_NO_PATHCONV=1`:
@@ -212,12 +218,16 @@ Python 3.10 or newer is required. Runtime code is stdlib-only.
212
218
 
213
219
  ## Release
214
220
 
215
- Publishing is automatic from GitHub Actions on merge. Verify and build live in
216
- the reusable workflow
221
+ **A release is cut by pushing a tag, not by merging.** Merging to `main` runs CI and
222
+ publishes nothing.
223
+
224
+ ```bash
225
+ git tag v1.0.1 && git push origin v1.0.1
226
+ ```
227
+
228
+ Verify and build live in the reusable workflow
217
229
  [`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
218
- in `insightfactory-ai/if_sre_github_actions`
219
- ([PR #300](https://github.com/insightfactory-ai/if_sre_github_actions/pull/300)
220
- must merge before these stubs go green). This repository's
230
+ in `insightfactory-ai/if_sre_github_actions`. This repository's
221
231
  `.github/workflows/release.yml` keeps only the OIDC publish job, because PyPI
222
232
  Trusted Publishing is registered for **this** repo, workflow `release.yml`, and
223
233
  environment `pypi` — a reusable workflow cannot be registered.
@@ -228,24 +238,38 @@ the reusable workflow and uploads it with `pypa/gh-action-pypi-publish`.
228
238
 
229
239
  | Trigger | Published version | PyPI role |
230
240
  |---|---|---|
231
- | Push / merge to `develop` | `1.0.0.dev{N}` | pre-release (`pip install --pre`) |
232
- | Push / merge to `main` | `1.0.0` (the version in `pyproject.toml`) | latest |
233
- | Push of a `v*` tag | same as `main` | retry / explicit cut of that version |
241
+ | Push / merge to `develop` | `1.0.1.dev{N}` | pre-release (`pip install --pre`) |
242
+ | Push of a `v*` tag | the version in `pyproject.toml`, which the tag must match | latest |
243
+ | Merge to `main` | nothing CI only | |
234
244
 
235
245
  `N` is `github.run_number` of the reusable workflow: a per-workflow integer
236
246
  that goes up on every run and stays the same across re-runs of that run (so a
237
247
  failed publish can be retried under the same version). It does not depend on
238
- git history or clone depth. After a successful `main` publish, bump `version`
239
- in `pyproject.toml` on `develop` so later `.devN` builds sort *after* what just
240
- landed; PEP 440 puts `1.0.0.devN` before `1.0.0`. Because of that ordering, a
248
+ git history or clone depth. After a successful release, bump `version` in
249
+ `pyproject.toml` on `develop` so later `.devN` builds sort *after* what just
250
+ landed; PEP 440 puts `1.0.1.devN` before `1.0.1`. Because of that ordering, a
241
251
  dependency spec of `insightfactory-cli>=1.0.0` does not match develop-channel
242
252
  `.devN` builds; an in-process consumer that wants them must specify a
243
253
  pre-release-bearing spec such as `insightfactory-cli>=1.0.0.dev0` (which also
244
254
  matches the final `1.0.0` release).
245
255
 
246
- A version that is already on PyPI is skipped rather than failing, so a second
247
- push to `main` (or a `v*` tag for the same version) is a no-op. A `v*` tag
248
- whose name does not match `pyproject.toml` fails before publishing.
256
+ A `v*` tag whose name does not match `pyproject.toml` fails before publishing.
257
+
258
+ The two channels treat an already-published version differently, on purpose:
259
+
260
+ - **Release channel (a tag):** publishing fails, naming the fix. A tag is a request to
261
+ release that version; if it cannot be honoured, the run must say so rather than report
262
+ success having published nothing. (git also refuses to push a tag that already exists,
263
+ so this is hard to reach.)
264
+ - **Dev channel (`develop`):** publishing is skipped quietly. `.dev{N}` uses
265
+ `github.run_number`, which is stable across re-runs of one run, so an already-present
266
+ version there can only mean a re-run of a run that already published — real idempotency,
267
+ not a swallowed mistake.
268
+
269
+ This split is why releasing moved off `main`. While a merge was the trigger, an
270
+ already-published version *had* to be tolerated as a no-op for merges to stay green — so a
271
+ merge that released nothing looked exactly like one that released, and a forgotten version
272
+ bump shipped nothing silently.
249
273
 
250
274
  ## Differences from the Node CLI
251
275
 
@@ -86,6 +86,12 @@ prefixes.
86
86
  Override the config directory with `INSIGHTFACTORY_CONFIG_DIR` (useful in tests
87
87
  and CI). The default is `~/.insightfactory`.
88
88
 
89
+ The request method goes in `-X`; `if-cli api` takes exactly one positional
90
+ argument and reads it as the path. A `curl`-shaped `if-cli api GET /api/schedules`
91
+ therefore parses `GET` as the path — write `if-cli api -X GET /api/schedules`
92
+ instead. The CLI names the misplaced verb rather than reporting a problem with
93
+ `/api/schedules`.
94
+
89
95
  On Git Bash (MSYS2) on Windows, path conversion rewrites a leading-slash
90
96
  argument such as `/api/agent-projects` into a Windows path. Prefix the command
91
97
  with `MSYS_NO_PATHCONV=1`:
@@ -196,12 +202,16 @@ Python 3.10 or newer is required. Runtime code is stdlib-only.
196
202
 
197
203
  ## Release
198
204
 
199
- Publishing is automatic from GitHub Actions on merge. Verify and build live in
200
- the reusable workflow
205
+ **A release is cut by pushing a tag, not by merging.** Merging to `main` runs CI and
206
+ publishes nothing.
207
+
208
+ ```bash
209
+ git tag v1.0.1 && git push origin v1.0.1
210
+ ```
211
+
212
+ Verify and build live in the reusable workflow
201
213
  [`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
202
- in `insightfactory-ai/if_sre_github_actions`
203
- ([PR #300](https://github.com/insightfactory-ai/if_sre_github_actions/pull/300)
204
- must merge before these stubs go green). This repository's
214
+ in `insightfactory-ai/if_sre_github_actions`. This repository's
205
215
  `.github/workflows/release.yml` keeps only the OIDC publish job, because PyPI
206
216
  Trusted Publishing is registered for **this** repo, workflow `release.yml`, and
207
217
  environment `pypi` — a reusable workflow cannot be registered.
@@ -212,24 +222,38 @@ the reusable workflow and uploads it with `pypa/gh-action-pypi-publish`.
212
222
 
213
223
  | Trigger | Published version | PyPI role |
214
224
  |---|---|---|
215
- | Push / merge to `develop` | `1.0.0.dev{N}` | pre-release (`pip install --pre`) |
216
- | Push / merge to `main` | `1.0.0` (the version in `pyproject.toml`) | latest |
217
- | Push of a `v*` tag | same as `main` | retry / explicit cut of that version |
225
+ | Push / merge to `develop` | `1.0.1.dev{N}` | pre-release (`pip install --pre`) |
226
+ | Push of a `v*` tag | the version in `pyproject.toml`, which the tag must match | latest |
227
+ | Merge to `main` | nothing CI only | |
218
228
 
219
229
  `N` is `github.run_number` of the reusable workflow: a per-workflow integer
220
230
  that goes up on every run and stays the same across re-runs of that run (so a
221
231
  failed publish can be retried under the same version). It does not depend on
222
- git history or clone depth. After a successful `main` publish, bump `version`
223
- in `pyproject.toml` on `develop` so later `.devN` builds sort *after* what just
224
- landed; PEP 440 puts `1.0.0.devN` before `1.0.0`. Because of that ordering, a
232
+ git history or clone depth. After a successful release, bump `version` in
233
+ `pyproject.toml` on `develop` so later `.devN` builds sort *after* what just
234
+ landed; PEP 440 puts `1.0.1.devN` before `1.0.1`. Because of that ordering, a
225
235
  dependency spec of `insightfactory-cli>=1.0.0` does not match develop-channel
226
236
  `.devN` builds; an in-process consumer that wants them must specify a
227
237
  pre-release-bearing spec such as `insightfactory-cli>=1.0.0.dev0` (which also
228
238
  matches the final `1.0.0` release).
229
239
 
230
- A version that is already on PyPI is skipped rather than failing, so a second
231
- push to `main` (or a `v*` tag for the same version) is a no-op. A `v*` tag
232
- whose name does not match `pyproject.toml` fails before publishing.
240
+ A `v*` tag whose name does not match `pyproject.toml` fails before publishing.
241
+
242
+ The two channels treat an already-published version differently, on purpose:
243
+
244
+ - **Release channel (a tag):** publishing fails, naming the fix. A tag is a request to
245
+ release that version; if it cannot be honoured, the run must say so rather than report
246
+ success having published nothing. (git also refuses to push a tag that already exists,
247
+ so this is hard to reach.)
248
+ - **Dev channel (`develop`):** publishing is skipped quietly. `.dev{N}` uses
249
+ `github.run_number`, which is stable across re-runs of one run, so an already-present
250
+ version there can only mean a re-run of a run that already published — real idempotency,
251
+ not a swallowed mistake.
252
+
253
+ This split is why releasing moved off `main`. While a merge was the trigger, an
254
+ already-published version *had* to be tolerated as a no-op for merges to stay green — so a
255
+ merge that released nothing looked exactly like one that released, and a forgotten version
256
+ bump shipped nothing silently.
233
257
 
234
258
  ## Differences from the Node CLI
235
259
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "insightfactory-cli"
3
- version = "1.0.1.dev5"
3
+ version = "1.0.1.dev9"
4
4
  description = "Profile-based authentication CLI for the InsightFactory Interfaces API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -61,6 +61,27 @@ def describe_route(document: dict[str, Any], method: str, path: str) -> dict[str
61
61
  return operation if isinstance(operation, dict) else None
62
62
 
63
63
 
64
+ def _is_absolute_http_url(api_path: str) -> bool:
65
+ parsed = parse_url(api_path)
66
+ return parsed.scheme in {"http", "https"} and bool(parsed.hostname)
67
+
68
+
69
+ def _die_on_misplaced_http_method(api_path: str, positionals: list[str]) -> None:
70
+ """Explain a `curl`-shaped `if-cli api GET /path`, where the verb is read as the path.
71
+
72
+ Recognition spans every verb a user might reach for, not just the ones -X accepts:
73
+ naming the misplaced verb is more useful than a path error even for TRACE, which
74
+ -X then rejects on its own terms.
75
+ """
76
+ method = api_path.lower()
77
+ if method not in HTTP_METHODS:
78
+ return
79
+ targets = [value for value in positionals[1:] if value.startswith("/") or _is_absolute_http_url(value)]
80
+ target = targets[0] if targets else "/api/path"
81
+ suggestion = f"if-cli api -X {method.upper()} {target}"
82
+ die(f"'{api_path}' looks like an HTTP method, not a path — did you mean: {suggestion} ?")
83
+
84
+
64
85
  def _die_on_msys_mangled_path(api_path: str) -> None:
65
86
  if not WINDOWS_DRIVE_PATH.search(api_path):
66
87
  return
@@ -73,11 +94,6 @@ def _die_on_msys_mangled_path(api_path: str) -> None:
73
94
  )
74
95
 
75
96
 
76
- def _is_absolute_http_url(api_path: str) -> bool:
77
- parsed = parse_url(api_path)
78
- return parsed.scheme in {"http", "https"} and bool(parsed.hostname)
79
-
80
-
81
97
  def resolve_api_url(host: str, api_path: str) -> ParseResult:
82
98
  if not api_path.startswith("/") and not _is_absolute_http_url(api_path):
83
99
  _die_on_msys_mangled_path(api_path)
@@ -145,6 +161,7 @@ def api_command(argv: list[str]) -> None:
145
161
  api_path = positionals[0] if positionals else None
146
162
  if not api_path:
147
163
  die(API_USAGE)
164
+ _die_on_misplaced_http_method(api_path, positionals)
148
165
 
149
166
  if values["method"] is not None:
150
167
  method = values["method"].upper()
@@ -37,6 +37,46 @@ def test_api_rejects_unsupported_request_methods_with_a_cli_error():
37
37
  assert "unsupported request method 'TRACE'" in result.stderr
38
38
 
39
39
 
40
+ def test_api_names_a_misplaced_http_method_instead_of_blaming_the_path():
41
+ result = run_cli(["api", "GET", "/api/schedules"])
42
+ assert result.returncode == 1
43
+ assert "'GET' looks like an HTTP method, not a path" in result.stderr
44
+ assert "did you mean: if-cli api -X GET /api/schedules ?" in result.stderr
45
+ assert "API path must begin with" not in result.stderr
46
+
47
+
48
+ def test_api_detects_a_misplaced_http_method_in_any_case_and_position():
49
+ lowercase = run_cli(["api", "put", "/api/orchestration/run"])
50
+ assert lowercase.returncode == 1
51
+ assert "'put' looks like an HTTP method, not a path" in lowercase.stderr
52
+ assert "did you mean: if-cli api -X PUT /api/orchestration/run ?" in lowercase.stderr
53
+
54
+ before_profile = run_cli(["api", "DELETE", "/api/schedules/1", "-p", "example-dev"])
55
+ assert before_profile.returncode == 1
56
+ assert "did you mean: if-cli api -X DELETE /api/schedules/1 ?" in before_profile.stderr
57
+
58
+
59
+ def test_api_keeps_a_same_origin_absolute_url_in_the_correction():
60
+ result = run_cli(["api", "GET", "https://factory.example/api/schedules"])
61
+ assert result.returncode == 1
62
+ assert "did you mean: if-cli api -X GET https://factory.example/api/schedules ?" in result.stderr
63
+
64
+
65
+ def test_api_names_a_misplaced_verb_that_x_would_not_accept():
66
+ result = run_cli(["api", "TRACE", "/api/schedules"])
67
+ assert result.returncode == 1
68
+ assert "'TRACE' looks like an HTTP method, not a path" in result.stderr
69
+ assert "did you mean: if-cli api -X TRACE /api/schedules ?" in result.stderr
70
+ assert "API path must begin with" not in result.stderr
71
+
72
+
73
+ def test_api_suggests_a_placeholder_path_when_only_a_method_was_supplied():
74
+ result = run_cli(["api", "POST"])
75
+ assert result.returncode == 1
76
+ assert "'POST' looks like an HTTP method, not a path" in result.stderr
77
+ assert "did you mean: if-cli api -X POST /api/path ?" in result.stderr
78
+
79
+
40
80
  def test_discovers_and_sorts_all_openapi_operations():
41
81
  assert discover_routes(OPENAPI) == [
42
82
  "DELETE /api/agent-projects/{id} Delete an agent project",