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.
- kctl_linear-0.2.0/.gitignore +10 -0
- kctl_linear-0.2.0/PKG-INFO +17 -0
- kctl_linear-0.2.0/README.md +68 -0
- kctl_linear-0.2.0/pyproject.toml +45 -0
- kctl_linear-0.2.0/skills/linear-admin/SKILL.md +147 -0
- kctl_linear-0.2.0/src/kctl_linear/__init__.py +3 -0
- kctl_linear-0.2.0/src/kctl_linear/__main__.py +5 -0
- kctl_linear-0.2.0/src/kctl_linear/cli.py +131 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/__init__.py +0 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/config_cmd.py +201 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/cycles.py +205 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/dashboard.py +84 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/doctor_cmd.py +58 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/health.py +37 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/issues.py +305 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/labels.py +91 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/projects.py +110 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/skill_cmd.py +76 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/teams.py +95 -0
- kctl_linear-0.2.0/src/kctl_linear/commands/users.py +69 -0
- kctl_linear-0.2.0/src/kctl_linear/core/__init__.py +0 -0
- kctl_linear-0.2.0/src/kctl_linear/core/callbacks.py +44 -0
- kctl_linear-0.2.0/src/kctl_linear/core/client.py +533 -0
- kctl_linear-0.2.0/src/kctl_linear/core/config.py +54 -0
- kctl_linear-0.2.0/src/kctl_linear/core/exceptions.py +21 -0
- kctl_linear-0.2.0/src/kctl_linear/core/plugins.py +13 -0
- kctl_linear-0.2.0/tests/__init__.py +0 -0
- kctl_linear-0.2.0/tests/conftest.py +132 -0
- kctl_linear-0.2.0/tests/test_client.py +85 -0
- kctl_linear-0.2.0/tests/test_commands.py +47 -0
- kctl_linear-0.2.0/tests/test_cycles.py +24 -0
- kctl_linear-0.2.0/tests/test_cycles_full.py +274 -0
- kctl_linear-0.2.0/tests/test_health.py +17 -0
- kctl_linear-0.2.0/tests/test_issues.py +34 -0
- kctl_linear-0.2.0/tests/test_issues_full.py +445 -0
- kctl_linear-0.2.0/tests/test_projects_full.py +210 -0
- kctl_linear-0.2.0/tests/test_smoke.py +15 -0
|
@@ -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,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
|