indico-cli 0.4.4__tar.gz → 0.5.1__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 (27) hide show
  1. indico_cli-0.5.1/.gitlab-ci.yml +23 -0
  2. {indico_cli-0.4.4 → indico_cli-0.5.1}/PKG-INFO +9 -1
  3. {indico_cli-0.4.4 → indico_cli-0.5.1}/README.md +8 -0
  4. indico_cli-0.5.1/docs/superpowers/plans/2026-06-04-secure-config-storage.md +543 -0
  5. indico_cli-0.5.1/docs/superpowers/specs/2026-06-04-secure-config-storage-design.md +137 -0
  6. {indico_cli-0.4.4 → indico_cli-0.5.1}/indico_cli/_version.py +2 -2
  7. {indico_cli-0.4.4 → indico_cli-0.5.1}/indico_cli/cli.py +56 -21
  8. indico_cli-0.5.1/indico_cli/config.py +72 -0
  9. {indico_cli-0.4.4 → indico_cli-0.5.1}/indico_cli/indico_api.py +27 -2
  10. {indico_cli-0.4.4 → indico_cli-0.5.1}/indico_cli/utils.py +14 -6
  11. {indico_cli-0.4.4 → indico_cli-0.5.1}/skills/indico-skill/SKILL.md +4 -0
  12. indico_cli-0.5.1/tests/test_cli_config.py +73 -0
  13. indico_cli-0.5.1/tests/test_config.py +74 -0
  14. {indico_cli-0.4.4 → indico_cli-0.5.1}/tests/test_write_commands.py +130 -0
  15. indico_cli-0.4.4/.gitlab-ci.yml +0 -50
  16. {indico_cli-0.4.4 → indico_cli-0.5.1}/.gitignore +0 -0
  17. {indico_cli-0.4.4 → indico_cli-0.5.1}/CLAUDE.md +0 -0
  18. {indico_cli-0.4.4 → indico_cli-0.5.1}/LICENSE +0 -0
  19. {indico_cli-0.4.4 → indico_cli-0.5.1}/docs/superpowers/plans/2026-03-24-search-results-and-error-messages.md +0 -0
  20. {indico_cli-0.4.4 → indico_cli-0.5.1}/docs/superpowers/specs/2026-03-24-search-results-and-error-messages-design.md +0 -0
  21. {indico_cli-0.4.4 → indico_cli-0.5.1}/indico_cli/__init__.py +0 -0
  22. {indico_cli-0.4.4 → indico_cli-0.5.1}/pyproject.toml +0 -0
  23. {indico_cli-0.4.4 → indico_cli-0.5.1}/tests/__init__.py +0 -0
  24. {indico_cli-0.4.4 → indico_cli-0.5.1}/tests/test_error_messages.py +0 -0
  25. {indico_cli-0.4.4 → indico_cli-0.5.1}/tests/test_format_search_result.py +0 -0
  26. {indico_cli-0.4.4 → indico_cli-0.5.1}/tests/test_get_files.py +0 -0
  27. {indico_cli-0.4.4 → indico_cli-0.5.1}/uv.lock +0 -0
@@ -0,0 +1,23 @@
1
+ stages:
2
+ - publish
3
+
4
+ # Publish to PyPI on git tag. The acc-py index (acc-py-repo.cern.ch) is a Nexus
5
+ # group that proxies PyPI, so it serves this package from PyPI automatically
6
+ # once its metadata cache refreshes. Publishing to the acc-py hosted repo is not
7
+ # possible for a package that exists on PyPI (Nexus returns 409 Conflict), so
8
+ # PyPI is the single source of truth.
9
+ publish-pypi:
10
+ stage: publish
11
+ image: python:3.12-slim
12
+ rules:
13
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/
14
+ script:
15
+ - apt-get update && apt-get install -y git
16
+ - git fetch --tags
17
+ - pip install build twine
18
+ - python -m build
19
+ - twine upload --skip-existing dist/*
20
+ variables:
21
+ TWINE_USERNAME: __token__
22
+ TWINE_PASSWORD: $PYPI_API_TOKEN
23
+ GIT_DEPTH: 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indico-cli
3
- Version: 0.4.4
3
+ Version: 0.5.1
4
4
  Summary: CLI tool for interacting with Indico event management systems
5
5
  Project-URL: Repository, https://gitlab.cern.ch/cern-agent-skills/indico-cli
6
6
  License: MIT
@@ -50,6 +50,11 @@ Or use the `configure` command to save credentials:
50
50
  indico-cli configure --base-url https://indico.cern.ch --token indp_...
51
51
  ```
52
52
 
53
+ Saved credentials live in `$XDG_CONFIG_HOME/indico-cli/config.json` (or
54
+ `~/.config/indico-cli/config.json` when `XDG_CONFIG_HOME` is unset), written with
55
+ owner-only permissions (`0600`). Inspect or reset them with `indico-cli config
56
+ show`, `indico-cli config path`, and `indico-cli config clear`.
57
+
53
58
  API tokens can be generated at `<your-indico-url>/user/preferences/api`. Required scopes: `read:legacy_api`, `write:legacy_api` (for create operations), `read:user` (optional).
54
59
 
55
60
  ## Usage
@@ -117,6 +122,9 @@ indico-cli create-event-with-agenda --title "Workshop" --category-id 4648 \
117
122
  | Command | Description |
118
123
  |---------|-------------|
119
124
  | `configure` | Save Indico base URL and API token, and verify the connection. |
125
+ | `config show` | Show the saved base URL and a masked token, plus the config file path. |
126
+ | `config path` | Print the path to the config file. |
127
+ | `config clear` | Delete the saved configuration (use `--yes` to skip the prompt). |
120
128
  | `user-info` | Show the current user's name, email, and admin status. |
121
129
  | `search-events` | List events in a category, with optional date range and public-only filter. |
122
130
  | `search-by-term` | Full-text search across categories, events, contributions, attachments, and notes. |
@@ -27,6 +27,11 @@ Or use the `configure` command to save credentials:
27
27
  indico-cli configure --base-url https://indico.cern.ch --token indp_...
28
28
  ```
29
29
 
30
+ Saved credentials live in `$XDG_CONFIG_HOME/indico-cli/config.json` (or
31
+ `~/.config/indico-cli/config.json` when `XDG_CONFIG_HOME` is unset), written with
32
+ owner-only permissions (`0600`). Inspect or reset them with `indico-cli config
33
+ show`, `indico-cli config path`, and `indico-cli config clear`.
34
+
30
35
  API tokens can be generated at `<your-indico-url>/user/preferences/api`. Required scopes: `read:legacy_api`, `write:legacy_api` (for create operations), `read:user` (optional).
31
36
 
32
37
  ## Usage
@@ -94,6 +99,9 @@ indico-cli create-event-with-agenda --title "Workshop" --category-id 4648 \
94
99
  | Command | Description |
95
100
  |---------|-------------|
96
101
  | `configure` | Save Indico base URL and API token, and verify the connection. |
102
+ | `config show` | Show the saved base URL and a masked token, plus the config file path. |
103
+ | `config path` | Print the path to the config file. |
104
+ | `config clear` | Delete the saved configuration (use `--yes` to skip the prompt). |
97
105
  | `user-info` | Show the current user's name, email, and admin status. |
98
106
  | `search-events` | List events in a category, with optional date range and public-only filter. |
99
107
  | `search-by-term` | Full-text search across categories, events, contributions, attachments, and notes. |
@@ -0,0 +1,543 @@
1
+ # Secure Config Storage + `config` Commands Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Store the Indico token+URL in a generic config file with owner-only permissions, and add `config show/path/clear` commands to inspect and manage it.
6
+
7
+ **Architecture:** Extract on-disk config persistence from `cli.py` into a new focused module `indico_cli/config.py` that honors `XDG_CONFIG_HOME` and writes the file as `0600` (dir `0700`). `cli.py` imports from it and gains a `config` Click group. Credential resolution (args -> env -> file) is unchanged.
8
+
9
+ **Tech Stack:** Python 3.12, Click 8, pytest, Click `CliRunner` for CLI tests.
10
+
11
+ **Linting:** Before every Python commit, run `uvx ruff check indico_cli tests` and fix anything it reports. The repo is not typed (no `[tool.mypy]`, no type-checking config), so mypy is skipped. Docs-only commits (Task 4) need no lint.
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ - **Create** `indico_cli/config.py` — on-disk credential config: path resolution, load/save (with permission hardening), clear, token masking. Single responsibility.
18
+ - **Create** `tests/test_config.py` — unit tests for `config.py` (tmp `XDG_CONFIG_HOME`).
19
+ - **Create** `tests/test_cli_config.py` — Click `CliRunner` tests for the `config` group.
20
+ - **Modify** `indico_cli/cli.py` — import from `config.py`, delete the inline config code, add the `config` command group.
21
+ - **Modify** `README.md` — document the `config` commands and file permissions.
22
+ - **Modify** `skills/indico-skill/SKILL.md` — mention `config show/path/clear`.
23
+
24
+ ---
25
+
26
+ ## Task 1: Create the `config` module (with tests)
27
+
28
+ **Files:**
29
+ - Create: `indico_cli/config.py`
30
+ - Test: `tests/test_config.py`
31
+
32
+ - [ ] **Step 1: Write the failing tests**
33
+
34
+ Create `tests/test_config.py`:
35
+
36
+ ```python
37
+ """Unit tests for indico_cli.config on-disk credential storage."""
38
+
39
+ import os
40
+ import stat
41
+ from pathlib import Path
42
+
43
+ import pytest
44
+
45
+ from indico_cli import config
46
+
47
+
48
+ @pytest.fixture
49
+ def tmp_xdg(tmp_path, monkeypatch):
50
+ """Point config storage at a temp XDG_CONFIG_HOME."""
51
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
52
+ return tmp_path
53
+
54
+
55
+ def test_get_config_path_honors_xdg(tmp_xdg):
56
+ assert config.get_config_path() == tmp_xdg / "indico-cli" / "config.json"
57
+
58
+
59
+ def test_get_config_path_defaults_to_home(monkeypatch):
60
+ monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
61
+ assert config.get_config_path() == Path.home() / ".config" / "indico-cli" / "config.json"
62
+
63
+
64
+ def test_save_then_load_roundtrips(tmp_xdg):
65
+ config.save_config("https://indico.cern.ch", "indp_secrettoken")
66
+ assert config.load_config() == {
67
+ "base_url": "https://indico.cern.ch",
68
+ "token": "indp_secrettoken",
69
+ }
70
+
71
+
72
+ @pytest.mark.skipif(os.name == "nt", reason="POSIX permissions only")
73
+ def test_save_sets_owner_only_permissions(tmp_xdg):
74
+ config.save_config("https://indico.cern.ch", "indp_secrettoken")
75
+ config_file = config.get_config_path()
76
+ assert stat.S_IMODE(config_file.stat().st_mode) == 0o600
77
+ assert stat.S_IMODE(config_file.parent.stat().st_mode) == 0o700
78
+
79
+
80
+ def test_load_returns_empty_when_missing(tmp_xdg):
81
+ assert config.load_config() == {}
82
+
83
+
84
+ def test_load_returns_empty_on_corrupt_json(tmp_xdg):
85
+ config_file = config.get_config_path()
86
+ config_file.parent.mkdir(parents=True, exist_ok=True)
87
+ config_file.write_text("{not valid json")
88
+ assert config.load_config() == {}
89
+
90
+
91
+ def test_clear_removes_file_and_returns_true(tmp_xdg):
92
+ config.save_config("https://indico.cern.ch", "indp_secrettoken")
93
+ assert config.clear_config() is True
94
+ assert not config.get_config_path().exists()
95
+
96
+
97
+ def test_clear_returns_false_when_nothing(tmp_xdg):
98
+ assert config.clear_config() is False
99
+
100
+
101
+ def test_mask_token_empty():
102
+ assert config.mask_token("") == "(none)"
103
+
104
+
105
+ def test_mask_token_short():
106
+ assert config.mask_token("indp_123") == "****"
107
+
108
+
109
+ def test_mask_token_normal():
110
+ assert config.mask_token("indp_abcdefgh1234") == "indp_...1234"
111
+ ```
112
+
113
+ - [ ] **Step 2: Run tests to verify they fail**
114
+
115
+ Run: `uv run pytest tests/test_config.py -v`
116
+ Expected: FAIL — `ModuleNotFoundError: No module named 'indico_cli.config'` (or `AttributeError`).
117
+
118
+ - [ ] **Step 3: Write the module**
119
+
120
+ Create `indico_cli/config.py`:
121
+
122
+ ```python
123
+ #!/usr/bin/env python3
124
+ """On-disk credential configuration for indico-cli.
125
+
126
+ Stores the Indico base URL and API token in a generic config file
127
+ (``$XDG_CONFIG_HOME/indico-cli/config.json`` or
128
+ ``~/.config/indico-cli/config.json``). The file holds a token, so it is written
129
+ with owner-only permissions (0600).
130
+ """
131
+
132
+ import json
133
+ import os
134
+ from pathlib import Path
135
+
136
+ APP_DIR_NAME = "indico-cli"
137
+ CONFIG_FILENAME = "config.json"
138
+
139
+
140
+ def get_config_path() -> Path:
141
+ """Return the config file path, honoring XDG_CONFIG_HOME.
142
+
143
+ Computed on each call so runtime/test environment changes are respected.
144
+ Falls back to ~/.config when XDG_CONFIG_HOME is unset (backward compatible).
145
+ """
146
+ base = os.environ.get("XDG_CONFIG_HOME")
147
+ config_home = Path(base) if base else Path.home() / ".config"
148
+ return config_home / APP_DIR_NAME / CONFIG_FILENAME
149
+
150
+
151
+ def load_config() -> dict:
152
+ """Load saved configuration, returning an empty dict on any failure."""
153
+ config_file = get_config_path()
154
+ if config_file.exists():
155
+ try:
156
+ return json.loads(config_file.read_text())
157
+ except (json.JSONDecodeError, OSError):
158
+ return {}
159
+ return {}
160
+
161
+
162
+ def save_config(base_url: str, token: str) -> None:
163
+ """Persist configuration with owner-only permissions (dir 0700, file 0600)."""
164
+ config_file = get_config_path()
165
+ config_dir = config_file.parent
166
+ config_dir.mkdir(parents=True, exist_ok=True)
167
+ config_file.write_text(
168
+ json.dumps({"base_url": base_url, "token": token}, indent=2)
169
+ )
170
+ # Best-effort permission hardening. A filesystem without POSIX permission
171
+ # bits (e.g. some Windows setups) must not crash the save.
172
+ try:
173
+ os.chmod(config_dir, 0o700)
174
+ os.chmod(config_file, 0o600)
175
+ except OSError:
176
+ pass
177
+
178
+
179
+ def clear_config() -> bool:
180
+ """Delete the config file. Return True if a file was removed."""
181
+ config_file = get_config_path()
182
+ if config_file.exists():
183
+ config_file.unlink()
184
+ return True
185
+ return False
186
+
187
+
188
+ def mask_token(token: str) -> str:
189
+ """Return a masked representation of a token, safe for display."""
190
+ if not token:
191
+ return "(none)"
192
+ if len(token) <= 8:
193
+ return "****"
194
+ return f"{token[:5]}...{token[-4:]}"
195
+ ```
196
+
197
+ - [ ] **Step 4: Run tests to verify they pass**
198
+
199
+ Run: `uv run pytest tests/test_config.py -v`
200
+ Expected: PASS (all tests; the permissions test is skipped only on Windows).
201
+
202
+ - [ ] **Step 5: Lint, then commit**
203
+
204
+ Run: `uvx ruff check indico_cli tests` (fix any reported issues, then re-run until clean).
205
+
206
+ ```bash
207
+ git add indico_cli/config.py tests/test_config.py
208
+ git commit -m "feat: add config module with XDG path and 0600 permissions"
209
+ ```
210
+
211
+ ---
212
+
213
+ ## Task 2: Wire `cli.py` to the config module
214
+
215
+ Replace the inline config code in `cli.py` with imports from `indico_cli.config`, keeping `configure` and `get_client` behavior identical. This is a refactor verified by the existing test suite.
216
+
217
+ **Files:**
218
+ - Modify: `indico_cli/cli.py` (lines ~8, 15-32, 94-95)
219
+
220
+ - [ ] **Step 1: Remove the now-unused `pathlib` import**
221
+
222
+ In `indico_cli/cli.py`, delete this line (Task 1's module owns path handling now; `click.Path` is unrelated):
223
+
224
+ ```python
225
+ from pathlib import Path
226
+ ```
227
+
228
+ - [ ] **Step 2: Replace the inline config block with an import**
229
+
230
+ Delete this entire block (currently lines 15-32):
231
+
232
+ ```python
233
+ CONFIG_DIR = Path.home() / ".config" / "indico-cli"
234
+ CONFIG_FILE = CONFIG_DIR / "config.json"
235
+
236
+
237
+ def load_config() -> dict:
238
+ """Load saved configuration from disk, returning empty dict on failure."""
239
+ if CONFIG_FILE.exists():
240
+ try:
241
+ return json.loads(CONFIG_FILE.read_text())
242
+ except (json.JSONDecodeError, OSError):
243
+ return {}
244
+ return {}
245
+
246
+
247
+ def save_config(base_url: str, token: str) -> None:
248
+ """Persist configuration to ~/.config/indico-cli/config.json."""
249
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
250
+ CONFIG_FILE.write_text(json.dumps({"base_url": base_url, "token": token}, indent=2))
251
+ ```
252
+
253
+ Replace it with this import (place it just below `from indico_cli.indico_api import IndicoAPI, IndicoClient`):
254
+
255
+ ```python
256
+ from indico_cli.config import (
257
+ clear_config,
258
+ get_config_path,
259
+ load_config,
260
+ mask_token,
261
+ save_config,
262
+ )
263
+ ```
264
+
265
+ - [ ] **Step 3: Update the `configure` command's saved-path message**
266
+
267
+ In the `configure` command, replace:
268
+
269
+ ```python
270
+ save_config(base_url, token)
271
+ click.echo(f"Configuration saved to {CONFIG_FILE}")
272
+ ```
273
+
274
+ with:
275
+
276
+ ```python
277
+ save_config(base_url, token)
278
+ click.echo(f"Configuration saved to {get_config_path()}")
279
+ ```
280
+
281
+ - [ ] **Step 4: Run the full test suite**
282
+
283
+ Run: `uv run pytest -v`
284
+ Expected: PASS — all existing tests plus `tests/test_config.py`. No import errors.
285
+
286
+ - [ ] **Step 5: Smoke-test the CLI loads**
287
+
288
+ Run: `uv run indico-cli --help`
289
+ Expected: the help text lists existing commands and exits 0 (no traceback).
290
+
291
+ - [ ] **Step 6: Lint, then commit**
292
+
293
+ Run: `uvx ruff check indico_cli tests` (fix any reported issues, then re-run until clean).
294
+
295
+ ```bash
296
+ git add indico_cli/cli.py
297
+ git commit -m "refactor: use indico_cli.config for credential persistence"
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Task 3: Add the `config` command group (with tests)
303
+
304
+ **Files:**
305
+ - Modify: `indico_cli/cli.py` (add group after the `configure` command, before `user-info`)
306
+ - Test: `tests/test_cli_config.py`
307
+
308
+ - [ ] **Step 1: Write the failing CLI tests**
309
+
310
+ Create `tests/test_cli_config.py`:
311
+
312
+ ```python
313
+ """CLI tests for the `config` command group."""
314
+
315
+ import json
316
+
317
+ import pytest
318
+ from click.testing import CliRunner
319
+
320
+ from indico_cli.cli import cli
321
+
322
+
323
+ @pytest.fixture
324
+ def tmp_xdg(tmp_path, monkeypatch):
325
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
326
+ monkeypatch.delenv("INDICO_BASE_URL", raising=False)
327
+ monkeypatch.delenv("INDICO_API_TOKEN", raising=False)
328
+ return tmp_path
329
+
330
+
331
+ def _write_config(tmp_path, base_url="https://indico.cern.ch", token="indp_abcdefgh1234"):
332
+ config_file = tmp_path / "indico-cli" / "config.json"
333
+ config_file.parent.mkdir(parents=True, exist_ok=True)
334
+ config_file.write_text(json.dumps({"base_url": base_url, "token": token}))
335
+ return config_file
336
+
337
+
338
+ def test_config_show_masks_token(tmp_xdg):
339
+ _write_config(tmp_xdg)
340
+ result = CliRunner().invoke(cli, ["config", "show"])
341
+ assert result.exit_code == 0
342
+ assert "indp_...1234" in result.output
343
+ assert "indp_abcdefgh1234" not in result.output
344
+ assert "https://indico.cern.ch" in result.output
345
+
346
+
347
+ def test_config_show_no_config(tmp_xdg):
348
+ result = CliRunner().invoke(cli, ["config", "show"])
349
+ assert result.exit_code == 0
350
+ assert "No saved configuration" in result.output
351
+
352
+
353
+ def test_config_show_notes_env_override(tmp_xdg, monkeypatch):
354
+ _write_config(tmp_xdg)
355
+ monkeypatch.setenv("INDICO_API_TOKEN", "indp_envtoken")
356
+ result = CliRunner().invoke(cli, ["config", "show"])
357
+ assert result.exit_code == 0
358
+ assert "INDICO_API_TOKEN" in result.output
359
+ assert "override" in result.output
360
+
361
+
362
+ def test_config_path_prints_path(tmp_xdg):
363
+ result = CliRunner().invoke(cli, ["config", "path"])
364
+ assert result.exit_code == 0
365
+ assert str(tmp_xdg / "indico-cli" / "config.json") in result.output
366
+
367
+
368
+ def test_config_clear_with_yes(tmp_xdg):
369
+ _write_config(tmp_xdg)
370
+ result = CliRunner().invoke(cli, ["config", "clear", "--yes"])
371
+ assert result.exit_code == 0
372
+ assert not (tmp_xdg / "indico-cli" / "config.json").exists()
373
+
374
+
375
+ def test_config_clear_nothing(tmp_xdg):
376
+ result = CliRunner().invoke(cli, ["config", "clear", "--yes"])
377
+ assert result.exit_code == 0
378
+ assert "Nothing to clear" in result.output
379
+
380
+
381
+ def test_config_clear_aborts_without_confirmation(tmp_xdg):
382
+ _write_config(tmp_xdg)
383
+ result = CliRunner().invoke(cli, ["config", "clear"], input="n\n")
384
+ assert result.exit_code != 0 # click confirm abort -> non-zero exit
385
+ assert (tmp_xdg / "indico-cli" / "config.json").exists()
386
+ ```
387
+
388
+ - [ ] **Step 2: Run tests to verify they fail**
389
+
390
+ Run: `uv run pytest tests/test_cli_config.py -v`
391
+ Expected: FAIL — `config` is not a command (Click reports "No such command 'config'").
392
+
393
+ - [ ] **Step 3: Expand the config import, then add the `config` group to `cli.py`**
394
+
395
+ First, widen the existing import line in `indico_cli/cli.py` (Task 2 left it importing only the three names it used) to also bring in `clear_config` and `mask_token`:
396
+
397
+ ```python
398
+ from indico_cli.config import (
399
+ clear_config,
400
+ get_config_path,
401
+ load_config,
402
+ mask_token,
403
+ save_config,
404
+ )
405
+ ```
406
+
407
+ Then insert this block immediately after the `configure` command function (after its final line, before the `user-info` command). It uses `load_config`, `get_config_path`, `clear_config`, `mask_token` and the already-imported `os`/`click`:
408
+
409
+ ```python
410
+ @cli.group("config")
411
+ def config_group():
412
+ """Inspect and manage saved Indico CLI configuration."""
413
+
414
+
415
+ @config_group.command("show")
416
+ def config_show():
417
+ """Show saved configuration (token masked)."""
418
+ cfg = load_config()
419
+ path = get_config_path()
420
+ if not cfg:
421
+ click.echo(f"No saved configuration at {path}")
422
+ else:
423
+ click.echo(f"Config file: {path}")
424
+ click.echo(f"Base URL: {cfg.get('base_url', '(none)')}")
425
+ click.echo(f"Token: {mask_token(cfg.get('token', ''))}")
426
+ overrides = [
427
+ name for name in ("INDICO_BASE_URL", "INDICO_API_TOKEN") if os.getenv(name)
428
+ ]
429
+ if overrides:
430
+ click.echo(
431
+ f"Note: {' and '.join(overrides)} set in environment; "
432
+ "these override the config file."
433
+ )
434
+
435
+
436
+ @config_group.command("path")
437
+ def config_path():
438
+ """Print the path to the config file."""
439
+ path = get_config_path()
440
+ state = "exists" if path.exists() else "does not exist"
441
+ click.echo(f"{path} ({state})")
442
+
443
+
444
+ @config_group.command("clear")
445
+ @click.option("--yes", "-y", is_flag=True, default=False, help="Skip the confirmation prompt")
446
+ def config_clear(yes):
447
+ """Delete the saved configuration file."""
448
+ path = get_config_path()
449
+ if not path.exists():
450
+ click.echo("Nothing to clear")
451
+ return
452
+ if not yes:
453
+ click.confirm(f"Delete saved configuration at {path}?", abort=True)
454
+ clear_config()
455
+ click.echo(f"Removed {path}")
456
+ ```
457
+
458
+ - [ ] **Step 4: Run the CLI tests to verify they pass**
459
+
460
+ Run: `uv run pytest tests/test_cli_config.py -v`
461
+ Expected: PASS (all 7 tests).
462
+
463
+ - [ ] **Step 5: Run the full suite**
464
+
465
+ Run: `uv run pytest -v`
466
+ Expected: PASS — everything green.
467
+
468
+ - [ ] **Step 6: Lint, then commit**
469
+
470
+ Run: `uvx ruff check indico_cli tests` (fix any reported issues, then re-run until clean).
471
+
472
+ ```bash
473
+ git add indico_cli/cli.py tests/test_cli_config.py
474
+ git commit -m "feat: add config show/path/clear commands"
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Task 4: Documentation
480
+
481
+ **Files:**
482
+ - Modify: `README.md`
483
+ - Modify: `skills/indico-skill/SKILL.md`
484
+
485
+ - [ ] **Step 1: Update the README commands table**
486
+
487
+ In `README.md`, in the "All Commands" table, add these three rows immediately after the `configure` row:
488
+
489
+ ```markdown
490
+ | `config show` | Show the saved base URL and a masked token, plus the config file path. |
491
+ | `config path` | Print the path to the config file. |
492
+ | `config clear` | Delete the saved configuration (use `--yes` to skip the prompt). |
493
+ ```
494
+
495
+ - [ ] **Step 2: Add a permissions/location note to the README Configuration section**
496
+
497
+ In `README.md`, append this paragraph to the "Configuration" section (after the `configure` example block, before the "API tokens can be generated..." line):
498
+
499
+ ```markdown
500
+ Saved credentials live in `$XDG_CONFIG_HOME/indico-cli/config.json` (or
501
+ `~/.config/indico-cli/config.json` when `XDG_CONFIG_HOME` is unset), written with
502
+ owner-only permissions (`0600`). Inspect or reset them with `indico-cli config
503
+ show`, `indico-cli config path`, and `indico-cli config clear`.
504
+ ```
505
+
506
+ - [ ] **Step 3: Update SKILL.md Configure section**
507
+
508
+ In `skills/indico-skill/SKILL.md`, immediately after the line
509
+ `Environment variables \`INDICO_BASE_URL\` and \`INDICO_API_TOKEN\` override the config file.`,
510
+ add:
511
+
512
+ ```markdown
513
+
514
+ Inspect or reset saved credentials with `uvx indico-cli config show` (token
515
+ masked), `uvx indico-cli config path`, or `uvx indico-cli config clear`. The
516
+ config file is written with `0600` permissions.
517
+ ```
518
+
519
+ - [ ] **Step 4: Commit**
520
+
521
+ ```bash
522
+ git add README.md skills/indico-skill/SKILL.md
523
+ git commit -m "docs: document config show/path/clear and file permissions"
524
+ ```
525
+
526
+ ---
527
+
528
+ ## Self-Review
529
+
530
+ **Spec coverage:**
531
+ - Harden permissions (dir 0700, file 0600) -> Task 1 `save_config` + permission test.
532
+ - `config` group `show`/`path`/`clear` -> Task 3.
533
+ - Extract dedicated module -> Task 1 (`config.py`) + Task 2 (wire-in).
534
+ - Honor `XDG_CONFIG_HOME` -> Task 1 `get_config_path` + test.
535
+ - `mask_token` format -> Task 1 + tests.
536
+ - `config show` env-override note -> Task 3 + `test_config_show_notes_env_override`.
537
+ - No-file / nothing-to-clear exit-0 states -> Task 3 + tests.
538
+ - Docs (README + SKILL.md) -> Task 4.
539
+ - Resolution unchanged (args -> env -> file) -> Task 2 verified by full suite.
540
+
541
+ **Placeholder scan:** none — every code/test step shows complete content.
542
+
543
+ **Type/name consistency:** `get_config_path`, `load_config`, `save_config`, `clear_config`, `mask_token` are defined in Task 1 and used with identical names in Tasks 2-3. Click group function `config_group` (command name `config`) does not shadow the imported names. `--yes/-y` flag matches between command and test.