kctl-linear 0.2.0__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 (37) hide show
  1. kctl_linear-0.2.0/.gitignore +10 -0
  2. kctl_linear-0.2.0/PKG-INFO +17 -0
  3. kctl_linear-0.2.0/README.md +68 -0
  4. kctl_linear-0.2.0/pyproject.toml +45 -0
  5. kctl_linear-0.2.0/skills/linear-admin/SKILL.md +147 -0
  6. kctl_linear-0.2.0/src/kctl_linear/__init__.py +3 -0
  7. kctl_linear-0.2.0/src/kctl_linear/__main__.py +5 -0
  8. kctl_linear-0.2.0/src/kctl_linear/cli.py +131 -0
  9. kctl_linear-0.2.0/src/kctl_linear/commands/__init__.py +0 -0
  10. kctl_linear-0.2.0/src/kctl_linear/commands/config_cmd.py +201 -0
  11. kctl_linear-0.2.0/src/kctl_linear/commands/cycles.py +205 -0
  12. kctl_linear-0.2.0/src/kctl_linear/commands/dashboard.py +84 -0
  13. kctl_linear-0.2.0/src/kctl_linear/commands/doctor_cmd.py +58 -0
  14. kctl_linear-0.2.0/src/kctl_linear/commands/health.py +37 -0
  15. kctl_linear-0.2.0/src/kctl_linear/commands/issues.py +305 -0
  16. kctl_linear-0.2.0/src/kctl_linear/commands/labels.py +91 -0
  17. kctl_linear-0.2.0/src/kctl_linear/commands/projects.py +110 -0
  18. kctl_linear-0.2.0/src/kctl_linear/commands/skill_cmd.py +76 -0
  19. kctl_linear-0.2.0/src/kctl_linear/commands/teams.py +95 -0
  20. kctl_linear-0.2.0/src/kctl_linear/commands/users.py +69 -0
  21. kctl_linear-0.2.0/src/kctl_linear/core/__init__.py +0 -0
  22. kctl_linear-0.2.0/src/kctl_linear/core/callbacks.py +44 -0
  23. kctl_linear-0.2.0/src/kctl_linear/core/client.py +533 -0
  24. kctl_linear-0.2.0/src/kctl_linear/core/config.py +54 -0
  25. kctl_linear-0.2.0/src/kctl_linear/core/exceptions.py +21 -0
  26. kctl_linear-0.2.0/src/kctl_linear/core/plugins.py +13 -0
  27. kctl_linear-0.2.0/tests/__init__.py +0 -0
  28. kctl_linear-0.2.0/tests/conftest.py +132 -0
  29. kctl_linear-0.2.0/tests/test_client.py +85 -0
  30. kctl_linear-0.2.0/tests/test_commands.py +47 -0
  31. kctl_linear-0.2.0/tests/test_cycles.py +24 -0
  32. kctl_linear-0.2.0/tests/test_cycles_full.py +274 -0
  33. kctl_linear-0.2.0/tests/test_health.py +17 -0
  34. kctl_linear-0.2.0/tests/test_issues.py +34 -0
  35. kctl_linear-0.2.0/tests/test_issues_full.py +445 -0
  36. kctl_linear-0.2.0/tests/test_projects_full.py +210 -0
  37. kctl_linear-0.2.0/tests/test_smoke.py +15 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .ruff_cache/
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .venv/
10
+ .env
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: kctl-linear
3
+ Version: 0.2.0
4
+ Summary: Kodemeio Linear CLI — project and sprint tracking
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: httpx>=0.28.0
7
+ Requires-Dist: kctl-lib>=0.4.0
8
+ Requires-Dist: pydantic>=2.10.0
9
+ Requires-Dist: pyyaml>=6.0.2
10
+ Requires-Dist: rich>=13.9.0
11
+ Requires-Dist: typer>=0.15.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: mypy>=1.14.0; extra == 'dev'
14
+ Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
15
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
16
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
17
+ Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
@@ -0,0 +1,68 @@
1
+ # kctl-linear
2
+
3
+ Linear project and sprint tracking CLI. Uses the Linear GraphQL API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv tool install kctl-linear
9
+ # or editable install for development
10
+ uv tool install --editable packages/kctl-linear
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Configure
17
+ kctl-linear config init
18
+ kctl-linear config show
19
+
20
+ # Common workflows
21
+ kctl-linear dashboard # Overview: issues, cycles, projects
22
+ kctl-linear issues list # List all issues
23
+ kctl-linear issues search "bug login" # Search issues by keyword
24
+ kctl-linear issues create # Create a new issue
25
+ kctl-linear cycles current # Active sprint details
26
+ kctl-linear projects list # List all projects
27
+ ```
28
+
29
+ ## Command Groups
30
+
31
+ | Group | Subcommands | Description |
32
+ |-------------|--------------------------------------|------------------------------------|
33
+ | `config` | init, add, use, show, validate, ... | Profile and API key management |
34
+ | `dashboard` | (root) | Summary view of issues and sprints |
35
+ | `issues` | list, show, create, update, comment, search | Issue lifecycle management |
36
+ | `cycles` | list, show, current, stats | Sprint / cycle tracking |
37
+ | `projects` | list, show | Project listing and details |
38
+ | `teams` | list, show | Team membership and info |
39
+ | `labels` | list, create | Issue label management |
40
+ | `users` | list, me | Workspace user listing |
41
+ | `health` | check | API connectivity check |
42
+
43
+ ## Configuration
44
+
45
+ Config is stored in `~/.config/kodemeio/config.yaml` under the `linear` key.
46
+
47
+ ```bash
48
+ kctl-linear config init # Interactive setup (API key, workspace)
49
+ kctl-linear config add myprofile # Add a named profile
50
+ kctl-linear config use myprofile # Switch active profile
51
+ kctl-linear config show # Display current config (secrets masked)
52
+ ```
53
+
54
+ Required fields: `api_key`, `team_id` (optional default team).
55
+
56
+ ## Global Options
57
+
58
+ `--json`, `--format/-f` (pretty/json/csv/yaml), `--quiet/-q`, `--profile/-p`, `--no-header`, `--version/-V`
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ cd packages/kctl-linear
64
+ uv sync --all-extras
65
+ uv run pytest tests/ -v
66
+ uv run mypy src/
67
+ uv run ruff check src/
68
+ ```
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kctl-linear"
7
+ version = "0.2.0"
8
+ description = "Kodemeio Linear CLI — project and sprint tracking"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "kctl-lib>=0.4.0",
12
+ "typer>=0.15.0",
13
+ "rich>=13.9.0",
14
+ "pydantic>=2.10.0",
15
+ "pyyaml>=6.0.2",
16
+ "httpx>=0.28.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=8.3.0",
22
+ "pytest-httpx>=0.35.0",
23
+ "ruff>=0.9.0",
24
+ "mypy>=1.14.0",
25
+ "types-PyYAML>=6.0.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ kctl-linear = "kctl_linear.cli:_run"
30
+
31
+ [tool.uv.sources]
32
+ kctl-lib = { workspace = true }
33
+
34
+ [project.entry-points."kctl_linear.plugins"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/kctl_linear"]
38
+
39
+ [tool.ruff]
40
+ target-version = "py312"
41
+ line-length = 120
42
+
43
+ [tool.mypy]
44
+ python_version = "3.12"
45
+ strict = true
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: linear-admin
3
+ description: >
4
+ Linear project tracking administration via kctl-linear CLI (10 groups, ~31 commands).
5
+ MUST use for ANY kctl-linear operation.
6
+ Triggers on: "check", "comment", "config", "current", "cycles", "dashboard", "generate", "health", "init", "issues", "kctl-linear", "labels", "profile", "profiles", "projects", "remove", "search", "skill", "stats", "teams", "test", "users", "validate".
7
+ Auto-generated: 2026-04-05
8
+ registry_hash: 1da478e9bb45
9
+ ---
10
+
11
+ # linear-admin — kctl-linear CLI Reference
12
+
13
+ > Auto-generated from `kctl-linear` command registry. Do not edit manually.
14
+ > To regenerate: `kctl-linear skill generate`
15
+ > To add custom content: edit `SKILL.extra.md` in the same directory.
16
+
17
+ ## Overview
18
+
19
+ **CLI:** `kctl-linear`
20
+ **Command groups:** 10
21
+ **Total commands:** ~31
22
+ **Install:** `cd cli && uv tool install --editable .`
23
+
24
+ ## Global Options
25
+
26
+ | Flag | Description |
27
+ |------|-------------|
28
+ | `--json` | JSON output |
29
+ | `--quiet`, `-q` | Suppress info messages |
30
+ | `--format`, `-f` | Output format: pretty/json/csv/yaml |
31
+ | `--no-header` | Omit CSV header row |
32
+ | `--profile`, `-p` | Config profile name |
33
+ | `--version`, `-V` | Show version |
34
+
35
+ ## Command Reference
36
+
37
+ ### `kctl-linear config`
38
+
39
+ Profile and configuration management.
40
+
41
+ | Command | Description |
42
+ |---------|-------------|
43
+ | `config add <name>` | Add a new config profile. |
44
+ | `config current` | Show active profile and resolved context. |
45
+ | `config init` | Interactive config setup. |
46
+ | `config profiles` | List all config profiles. |
47
+ | `config remove <name>` | Remove a config profile. |
48
+ | `config set <key> <value>` | Set a single config value. |
49
+ | `config show` | Show current configuration. |
50
+ | `config test` | Test API connection with current configuration. |
51
+ | `config use <name>` | Switch active config profile. |
52
+ | `config validate` | Validate current config completeness. |
53
+
54
+ ### `kctl-linear cycles`
55
+
56
+ Cycle (sprint) management.
57
+
58
+ | Command | Description |
59
+ |---------|-------------|
60
+ | `cycles current [--team]` | Show current active cycle with progress and issues. |
61
+ | `cycles list [--team] [--limit]` | List past and upcoming cycles. |
62
+ | `cycles show <cycle_id>` | Show cycle details: scope, completed, remaining issues. |
63
+ | `cycles stats [--team]` | Show velocity trends across recent cycles. |
64
+
65
+ ### `kctl-linear dashboard`
66
+
67
+ Quick overview dashboard.
68
+
69
+ ### `kctl-linear health`
70
+
71
+ API health check.
72
+
73
+ ### `kctl-linear issues`
74
+
75
+ Issue management.
76
+
77
+ | Command | Description |
78
+ |---------|-------------|
79
+ | `issues comment <issue_id> <body>` | Add a comment to an issue. |
80
+ | `issues create <title> [--team] [--description] [--priority] [--assignee]` | Create a new issue. |
81
+ | `issues list [--team] [--state] [--assignee] [--limit]` | List issues with optional filters. |
82
+ | `issues search <query> [--limit]` | Full-text search for issues. |
83
+ | `issues show <issue_id>` | Show issue details, comments, and history. |
84
+ | `issues update <issue_id> [--state] [--assignee] [--priority] [--title] [--description]` | Update an existing issue. |
85
+
86
+ ### `kctl-linear labels`
87
+
88
+ Label management.
89
+
90
+ | Command | Description |
91
+ |---------|-------------|
92
+ | `labels create <name> [--color] [--team]` | Create a new label. |
93
+ | `labels list [--team]` | List all labels, optionally filtered by team. |
94
+
95
+ ### `kctl-linear projects`
96
+
97
+ Project tracking.
98
+
99
+ | Command | Description |
100
+ |---------|-------------|
101
+ | `projects list` | List active projects with progress. |
102
+ | `projects show <project_id>` | Show project details, milestones, and member issues. |
103
+
104
+ ### `kctl-linear skill`
105
+
106
+ Claude Code skill management.
107
+
108
+ | Command | Description |
109
+ |---------|-------------|
110
+ | `skill generate [--output] [--install] [--check]` | Auto-generate SKILL.md from CLI command registry. |
111
+
112
+ **Examples:**
113
+ ```bash
114
+ kctl-linear skill generate
115
+ kctl-linear skill generate --install
116
+ kctl-linear skill generate --check
117
+ ```
118
+
119
+ ### `kctl-linear teams`
120
+
121
+ Team information.
122
+
123
+ | Command | Description |
124
+ |---------|-------------|
125
+ | `teams list` | List all teams with member counts. |
126
+ | `teams show <team_id>` | Show team members, workflow states, and labels. |
127
+
128
+ ### `kctl-linear users`
129
+
130
+ User information.
131
+
132
+ | Command | Description |
133
+ |---------|-------------|
134
+ | `users list` | List all workspace members. |
135
+ | `users me` | Show current authenticated user. |
136
+
137
+ ## Configuration
138
+
139
+ Shared config: `~/.config/kodemeio/config.yaml`
140
+
141
+ ```bash
142
+ kctl-linear config init # Interactive setup
143
+ kctl-linear config show # Show current config
144
+ kctl-linear config profiles # List profiles
145
+ kctl-linear config current # Show active profile
146
+ kctl-linear config validate # Verify config
147
+ ```
@@ -0,0 +1,3 @@
1
+ """kctl-linear: Linear project management."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ """Allow running as python -m kctl_linear."""
2
+
3
+ from kctl_linear.cli import _run
4
+
5
+ _run()
@@ -0,0 +1,131 @@
1
+ """Main CLI entry point for kctl-linear."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from kctl_lib import KctlError, handle_cli_error
10
+
11
+ from kctl_linear import __version__
12
+ from kctl_linear.commands.config_cmd import app as config_app
13
+ from kctl_linear.commands.cycles import app as cycles_app
14
+ from kctl_linear.commands.dashboard import app as dashboard_app
15
+ from kctl_linear.commands.health import app as health_app
16
+ from kctl_linear.commands.issues import app as issues_app
17
+ from kctl_linear.commands.labels import app as labels_app
18
+ from kctl_linear.commands.projects import app as projects_app
19
+ from kctl_linear.commands.teams import app as teams_app
20
+ from kctl_linear.commands.users import app as users_app
21
+ from kctl_linear.core.callbacks import AppContext
22
+ from kctl_linear.core.plugins import discover_and_load_plugins
23
+ from kctl_linear.commands.skill_cmd import app as skill_app
24
+ from kctl_linear.commands.doctor_cmd import app as doctor_app
25
+ from kctl_lib.self_update import notify_if_outdated
26
+
27
+
28
+ def version_callback(value: bool) -> None:
29
+ if value:
30
+ typer.echo(f"kctl-linear {__version__}")
31
+ raise typer.Exit()
32
+
33
+
34
+ app = typer.Typer(
35
+ name="kctl-linear",
36
+ help="Linear project management",
37
+ no_args_is_help=True,
38
+ rich_markup_mode="rich",
39
+ pretty_exceptions_enable=False,
40
+ )
41
+
42
+
43
+ @app.callback()
44
+ def main(
45
+ ctx: typer.Context,
46
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
47
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
48
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
49
+ format: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")] = "pretty",
50
+ no_header: Annotated[bool, typer.Option("--no-header", help="Omit header row in CSV output")] = False,
51
+ version: Annotated[
52
+ bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
53
+ ] = False,
54
+ ) -> None:
55
+ """Linear project management."""
56
+ ctx.ensure_object(dict)
57
+ ctx.obj = AppContext(
58
+ json_mode=json_output,
59
+ quiet=quiet,
60
+ profile=profile,
61
+ format=format,
62
+ no_header=no_header,
63
+ )
64
+ notify_if_outdated(ctx.obj.output, "kctl-linear", __version__)
65
+
66
+
67
+ # Command group registration
68
+ app.add_typer(config_app, name="config")
69
+ app.add_typer(health_app, name="health")
70
+ app.add_typer(dashboard_app, name="dashboard")
71
+ app.add_typer(issues_app, name="issues")
72
+ app.add_typer(cycles_app, name="cycles")
73
+ app.add_typer(projects_app, name="projects")
74
+ app.add_typer(teams_app, name="teams")
75
+ app.add_typer(labels_app, name="labels")
76
+ app.add_typer(users_app, name="users")
77
+ app.add_typer(skill_app, name="skill", hidden=True)
78
+ app.add_typer(doctor_app, name="doctor")
79
+
80
+ # Load third-party plugins via entry points
81
+ discover_and_load_plugins(app)
82
+
83
+
84
+ @app.command("self-update")
85
+ def self_update_cmd(ctx: typer.Context) -> None:
86
+ """Check for updates and upgrade kctl-linear."""
87
+ actx = ctx.obj
88
+ out = actx.output
89
+
90
+ from kctl_lib.self_update import check_update
91
+ from kctl_lib.self_update import update as do_update
92
+
93
+ latest = check_update("kctl-linear", __version__)
94
+ if latest:
95
+ out.info(f"Updating to {latest}...")
96
+ do_update("kctl-linear")
97
+ out.success(f"Updated to {latest}")
98
+ else:
99
+ out.success("Already up to date")
100
+
101
+
102
+ @app.command()
103
+ def completions(
104
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
105
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
106
+ ) -> None:
107
+ """Generate or install shell completions."""
108
+ from kctl_lib.completions import get_completion_script, install_completions
109
+
110
+ if install:
111
+ path = install_completions("kctl-linear", shell)
112
+ if path:
113
+ typer.echo(f"Completions installed to {path}")
114
+ else:
115
+ typer.echo(f"Could not install completions for {shell}", err=True)
116
+ raise typer.Exit(code=1)
117
+ else:
118
+ script = get_completion_script("kctl-linear", shell)
119
+ typer.echo(script)
120
+
121
+
122
+ def _run() -> None:
123
+ """Entry point with error handling."""
124
+ try:
125
+ app()
126
+ except KctlError as e:
127
+ handle_cli_error(e)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ _run()
File without changes
@@ -0,0 +1,201 @@
1
+ """Config profile management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_linear.core.callbacks import AppContext
10
+ from kctl_linear.core.config import (
11
+ SERVICE_KEY,
12
+ ServiceConfig,
13
+ get_all_services_in_profile,
14
+ get_profile_names,
15
+ get_service_config,
16
+ remove_profile,
17
+ resolve_active_profile_name,
18
+ set_default_profile,
19
+ set_service_config,
20
+ )
21
+
22
+ app = typer.Typer(help="Profile and configuration management.")
23
+
24
+
25
+ @app.command()
26
+ def init(ctx: typer.Context) -> None:
27
+ """Interactive config setup."""
28
+ actx: AppContext = ctx.obj
29
+ out = actx.output
30
+ profile_name = typer.prompt("Profile name", default="default")
31
+ api_key = typer.prompt("Linear API key", hide_input=True)
32
+ default_team = typer.prompt("Default team key (e.g., KOD)", default="")
33
+ svc = ServiceConfig(api_key=api_key, default_team=default_team)
34
+ set_service_config(profile_name, svc)
35
+ set_default_profile(profile_name)
36
+ out.success(f"Config saved to profile '{profile_name}'")
37
+
38
+
39
+ @app.command()
40
+ def add(
41
+ ctx: typer.Context,
42
+ name: Annotated[str, typer.Argument(help="Profile name")],
43
+ ) -> None:
44
+ """Add a new config profile."""
45
+ actx: AppContext = ctx.obj
46
+ out = actx.output
47
+ api_key = typer.prompt("Linear API key", hide_input=True)
48
+ default_team = typer.prompt("Default team key (e.g., KOD)", default="")
49
+ svc = ServiceConfig(api_key=api_key, default_team=default_team)
50
+ set_service_config(name, svc)
51
+ out.success(f"Profile '{name}' added")
52
+
53
+
54
+ @app.command()
55
+ def use(
56
+ ctx: typer.Context,
57
+ name: Annotated[str, typer.Argument(help="Profile name to activate")],
58
+ ) -> None:
59
+ """Switch active config profile."""
60
+ actx: AppContext = ctx.obj
61
+ out = actx.output
62
+ available = get_profile_names()
63
+ if name not in available:
64
+ out.error(f"Profile '{name}' not found. Available: {', '.join(available)}")
65
+ raise typer.Exit(1)
66
+ set_default_profile(name)
67
+ out.success(f"Switched to profile '{name}'")
68
+
69
+
70
+ @app.command()
71
+ def show(ctx: typer.Context) -> None:
72
+ """Show current configuration."""
73
+ actx: AppContext = ctx.obj
74
+ out = actx.output
75
+ active = resolve_active_profile_name(actx.profile)
76
+ profiles = get_profile_names()
77
+ if out.json_mode:
78
+ out.raw_json({"active_profile": active, "profiles": {n: get_all_services_in_profile(n) for n in profiles}})
79
+ return
80
+ out.header("Configuration")
81
+ out.kv("Active profile", active)
82
+ for name in profiles:
83
+ marker = " (active)" if name == active else ""
84
+ out.header(f"Profile: {name}{marker}")
85
+ services = get_all_services_in_profile(name)
86
+ for svc_name, svc_data in services.items():
87
+ out.text(f" [bold]{svc_name}:[/bold]")
88
+ if isinstance(svc_data, dict):
89
+ for k, v in svc_data.items():
90
+ out.kv(f" {k}", str(v))
91
+
92
+
93
+ @app.command()
94
+ def validate(ctx: typer.Context) -> None:
95
+ """Validate current config completeness."""
96
+ actx: AppContext = ctx.obj
97
+ out = actx.output
98
+ active = resolve_active_profile_name(actx.profile)
99
+ svc = get_service_config(active)
100
+ issues: list[str] = []
101
+ if not svc.api_key:
102
+ issues.append("api_key is not set")
103
+ if not svc.default_team:
104
+ issues.append("default_team is not set (optional but recommended)")
105
+ if out.json_mode:
106
+ out.raw_json({"profile": active, "valid": not any("api_key" in i for i in issues), "issues": issues})
107
+ return
108
+ if any("api_key" in i for i in issues):
109
+ out.error(f"Profile '{active}' has issues:")
110
+ for issue in issues:
111
+ out.text(f" - {issue}")
112
+ raise typer.Exit(1)
113
+ if issues:
114
+ out.warn(f"Profile '{active}' has warnings:")
115
+ for issue in issues:
116
+ out.text(f" - {issue}")
117
+ else:
118
+ out.success(f"Profile '{active}' is valid")
119
+
120
+
121
+ @app.command()
122
+ def remove(
123
+ ctx: typer.Context,
124
+ name: Annotated[str, typer.Argument(help="Profile name to remove")],
125
+ ) -> None:
126
+ """Remove a config profile."""
127
+ actx: AppContext = ctx.obj
128
+ out = actx.output
129
+ available = get_profile_names()
130
+ if name not in available:
131
+ out.error(f"Profile '{name}' not found")
132
+ raise typer.Exit(1)
133
+ remove_profile(name)
134
+ out.success(f"Profile '{name}' removed")
135
+
136
+
137
+ @app.command("set")
138
+ def set_(
139
+ ctx: typer.Context,
140
+ key: Annotated[str, typer.Argument(help="Config key")],
141
+ value: Annotated[str, typer.Argument(help="Config value")],
142
+ ) -> None:
143
+ """Set a single config value."""
144
+ actx: AppContext = ctx.obj
145
+ out = actx.output
146
+ active = resolve_active_profile_name(actx.profile)
147
+ svc = get_service_config(active)
148
+ if key not in ServiceConfig.model_fields:
149
+ out.error(f"Unknown key '{key}'. Valid: {', '.join(ServiceConfig.model_fields)}")
150
+ raise typer.Exit(1)
151
+ data = svc.model_dump()
152
+ data[key] = value
153
+ set_service_config(active, ServiceConfig(**data))
154
+ out.success(f"Set {key}={value} in profile '{active}'")
155
+
156
+
157
+ @app.command()
158
+ def profiles(ctx: typer.Context) -> None:
159
+ """List all config profiles."""
160
+ actx: AppContext = ctx.obj
161
+ out = actx.output
162
+ active = resolve_active_profile_name(actx.profile)
163
+ names = get_profile_names()
164
+ if out.json_mode:
165
+ out.raw_json({"profiles": names, "active": active})
166
+ return
167
+ rows = [[name, "active" if name == active else ""] for name in names]
168
+ out.table("Profiles", [("Name", "cyan"), ("Status", "green")], rows)
169
+
170
+
171
+ @app.command()
172
+ def current(ctx: typer.Context) -> None:
173
+ """Show active profile and resolved context."""
174
+ actx: AppContext = ctx.obj
175
+ out = actx.output
176
+ active = resolve_active_profile_name(actx.profile)
177
+ svc = get_service_config(active)
178
+ if out.json_mode:
179
+ out.raw_json({"profile": active, **svc.model_dump()})
180
+ return
181
+ fields = [(k, str(v) or "(not set)") for k, v in svc.model_dump().items()]
182
+ sections = [("Active Profile", [("Name", active)] + fields)]
183
+ out.detail("Current Config", sections)
184
+
185
+
186
+ @app.command()
187
+ def test(ctx: typer.Context) -> None:
188
+ """Test API connection with current configuration."""
189
+ actx: AppContext = ctx.obj
190
+ out = actx.output
191
+ active = resolve_active_profile_name(actx.profile)
192
+ out.info(f"Testing profile '{active}' \u2192 {SERVICE_KEY}")
193
+ try:
194
+ data = actx.client.query("{ viewer { id name email } }")
195
+ viewer = data.get("viewer", {})
196
+ name = viewer.get("name", "unknown")
197
+ email = viewer.get("email", "")
198
+ out.success(f"Connected \u2014 authenticated as '{name}' ({email})")
199
+ except Exception as e:
200
+ out.error(f"Connection failed: {e}")
201
+ raise typer.Exit(1) from e