python-jcli 0.1.0__py3-none-any.whl
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.
- jcli/__init__.py +1 -0
- jcli/__main__.py +3 -0
- jcli/cli.py +79 -0
- jcli/cli_helpers.py +58 -0
- jcli/plugins/__init__.py +28 -0
- jcli/plugins/build.py +197 -0
- jcli/plugins/config.py +268 -0
- jcli/plugins/credential.py +210 -0
- jcli/plugins/job.py +425 -0
- jcli/plugins/node.py +134 -0
- jcli/plugins/pipeline.py +108 -0
- jcli/plugins/plugin.py +173 -0
- jcli/plugins/skills.py +399 -0
- jcli/plugins/system.py +173 -0
- jcli/plugins/view.py +148 -0
- jcli/sdk/__init__.py +0 -0
- jcli/sdk/build.py +182 -0
- jcli/sdk/client.py +264 -0
- jcli/sdk/config.py +227 -0
- jcli/sdk/credential.py +143 -0
- jcli/sdk/exceptions.py +59 -0
- jcli/sdk/job.py +191 -0
- jcli/sdk/job_templates.py +128 -0
- jcli/sdk/node.py +139 -0
- jcli/sdk/output/__init__.py +3 -0
- jcli/sdk/output/formatter.py +131 -0
- jcli/sdk/pipeline.py +121 -0
- jcli/sdk/plugin.py +155 -0
- jcli/sdk/system.py +202 -0
- jcli/sdk/view.py +94 -0
- jcli/skills/jcli/SKILL.md +73 -0
- jcli/skills/jcli/config/SKILL.md +81 -0
- jcli/skills/jcli/credential/SKILL.md +47 -0
- jcli/skills/jcli/job/SKILL.md +55 -0
- jcli/skills/jcli/node/SKILL.md +51 -0
- jcli/skills/jcli/pipeline/SKILL.md +58 -0
- jcli/skills/jcli/plugin/SKILL.md +51 -0
- jcli/skills/jcli/system/SKILL.md +70 -0
- jcli/skills/jcli/view/SKILL.md +47 -0
- python_jcli-0.1.0.dist-info/METADATA +311 -0
- python_jcli-0.1.0.dist-info/RECORD +44 -0
- python_jcli-0.1.0.dist-info/WHEEL +5 -0
- python_jcli-0.1.0.dist-info/entry_points.txt +2 -0
- python_jcli-0.1.0.dist-info/top_level.txt +1 -0
jcli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
jcli/__main__.py
ADDED
jcli/cli.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Jenkins CLI - Command line interface for managing Jenkins."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from jcli import __version__
|
|
9
|
+
from jcli.plugins import register_commands
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("jcli")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
@click.version_option(version=__version__, prog_name="jcli")
|
|
16
|
+
@click.option(
|
|
17
|
+
"-f",
|
|
18
|
+
"--format",
|
|
19
|
+
"output_format",
|
|
20
|
+
default="table",
|
|
21
|
+
type=click.Choice(["table", "json", "yaml"]),
|
|
22
|
+
help="Output format.",
|
|
23
|
+
)
|
|
24
|
+
@click.option("-p", "--profile", default=None, help="Configuration profile name.")
|
|
25
|
+
@click.option("-s", "--server", default=None, help="Jenkins server URL.")
|
|
26
|
+
@click.option("-d", "--debug", is_flag=True, default=False, help="Enable debug output.")
|
|
27
|
+
@click.pass_context
|
|
28
|
+
def cli(ctx, output_format, profile, server, debug):
|
|
29
|
+
"""Jenkins CLI - Manage Jenkins from the command line."""
|
|
30
|
+
ctx.ensure_object(dict)
|
|
31
|
+
ctx.obj["format"] = output_format
|
|
32
|
+
ctx.obj["profile"] = profile
|
|
33
|
+
ctx.obj["server"] = server
|
|
34
|
+
ctx.obj["debug"] = debug
|
|
35
|
+
|
|
36
|
+
# Configure debug logging when --debug / -d is set
|
|
37
|
+
if debug:
|
|
38
|
+
logging.basicConfig(
|
|
39
|
+
level=logging.DEBUG,
|
|
40
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
41
|
+
stream=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
logger.debug("Debug logging enabled")
|
|
44
|
+
else:
|
|
45
|
+
# Ensure WARNING+ level when not in debug mode
|
|
46
|
+
logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Register all plugin subcommands
|
|
50
|
+
register_commands(cli)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.group()
|
|
54
|
+
def completion():
|
|
55
|
+
"""Shell completion support for jcli."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@completion.command()
|
|
60
|
+
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]))
|
|
61
|
+
def show(shell):
|
|
62
|
+
"""Output shell completion script for the specified shell."""
|
|
63
|
+
from click.shell_completion import BashComplete, ZshComplete, FishComplete
|
|
64
|
+
|
|
65
|
+
shell_cls = {"bash": BashComplete, "zsh": ZshComplete, "fish": FishComplete}
|
|
66
|
+
complete = shell_cls[shell](cli, {}, "jcli", "_JCLI_COMPLETE")
|
|
67
|
+
click.echo(complete.source(), nl=False)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
cli.add_command(completion)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
"""CLI entry point."""
|
|
75
|
+
cli()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
jcli/cli_helpers.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Centralized CLI helpers for creating JenkinsClient and OutputFormatter.
|
|
2
|
+
|
|
3
|
+
All plugin modules should import ``get_client`` and ``get_formatter`` from here
|
|
4
|
+
instead of implementing their own versions. This ensures consistent behaviour
|
|
5
|
+
for ``--profile``, ``--server``, and ``--format`` across every subcommand.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from jcli.sdk.client import JenkinsClient
|
|
15
|
+
from jcli.sdk.config import Config
|
|
16
|
+
from jcli.sdk.output import OutputFormatter
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_client(ctx: click.Context) -> JenkinsClient:
|
|
22
|
+
"""Return a cached ``JenkinsClient`` from the Click context.
|
|
23
|
+
|
|
24
|
+
Resolution order:
|
|
25
|
+
1. Re-use ``ctx.obj["client"]`` if already present (tests / caching).
|
|
26
|
+
2. Load the config profile selected by ``--profile`` / ``$JCLI_PROFILE``.
|
|
27
|
+
3. Override the profile URL with ``--server`` when provided.
|
|
28
|
+
"""
|
|
29
|
+
obj = ctx.obj or {}
|
|
30
|
+
|
|
31
|
+
# Allow pre-created client (used by tests and internal callers)
|
|
32
|
+
if "client" in obj:
|
|
33
|
+
return obj["client"]
|
|
34
|
+
|
|
35
|
+
config = Config()
|
|
36
|
+
config.load()
|
|
37
|
+
|
|
38
|
+
profile_name = obj.get("profile") or config.get_active_profile_name()
|
|
39
|
+
profile = config.get_profile(profile_name)
|
|
40
|
+
|
|
41
|
+
url = obj.get("server") or profile["url"]
|
|
42
|
+
username = profile["username"]
|
|
43
|
+
token = profile["api_token"]
|
|
44
|
+
|
|
45
|
+
client = JenkinsClient(base_url=url, username=username, token=token)
|
|
46
|
+
logger.debug("Created JenkinsClient for %s (profile=%s)", url, profile_name)
|
|
47
|
+
|
|
48
|
+
# Cache for subsequent calls within the same invocation
|
|
49
|
+
obj["client"] = client
|
|
50
|
+
return client
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_formatter(ctx: click.Context) -> OutputFormatter:
|
|
54
|
+
"""Return an ``OutputFormatter`` matching the ``--format`` flag."""
|
|
55
|
+
obj = ctx.obj or {}
|
|
56
|
+
fmt = obj.get("format", "table")
|
|
57
|
+
|
|
58
|
+
return OutputFormatter(format_type=fmt)
|
jcli/plugins/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Jenkins CLI plugins - register all subcommands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from jcli.plugins.build import register as register_build
|
|
6
|
+
from jcli.plugins.config import register as register_config
|
|
7
|
+
from jcli.plugins.credential import register as register_credential
|
|
8
|
+
from jcli.plugins.job import register as register_job
|
|
9
|
+
from jcli.plugins.node import register as register_node
|
|
10
|
+
from jcli.plugins.pipeline import register as register_pipeline
|
|
11
|
+
from jcli.plugins.plugin import register as register_plugin
|
|
12
|
+
from jcli.plugins.skills import register as register_skills
|
|
13
|
+
from jcli.plugins.system import register as register_system
|
|
14
|
+
from jcli.plugins.view import register as register_view
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_commands(group: click.Group) -> None:
|
|
18
|
+
"""Register all plugin subcommands under the main CLI group."""
|
|
19
|
+
register_build(group)
|
|
20
|
+
register_config(group)
|
|
21
|
+
register_credential(group)
|
|
22
|
+
register_job(group)
|
|
23
|
+
register_node(group)
|
|
24
|
+
register_pipeline(group)
|
|
25
|
+
register_plugin(group)
|
|
26
|
+
register_skills(group)
|
|
27
|
+
register_system(group)
|
|
28
|
+
register_view(group)
|
jcli/plugins/build.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Jenkins Build management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from jcli.cli_helpers import get_client, get_formatter
|
|
8
|
+
from jcli.sdk.build import (
|
|
9
|
+
cancel_queue_item,
|
|
10
|
+
get_build,
|
|
11
|
+
get_build_artifacts,
|
|
12
|
+
get_build_log,
|
|
13
|
+
get_builds,
|
|
14
|
+
get_queue,
|
|
15
|
+
stop_build,
|
|
16
|
+
trigger_build,
|
|
17
|
+
)
|
|
18
|
+
from jcli.sdk.exceptions import JenkinsError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_params(raw_params: tuple[str, ...]) -> dict[str, str]:
|
|
22
|
+
"""Parse ``KEY=VAL`` strings into a dict."""
|
|
23
|
+
params: dict[str, str] = {}
|
|
24
|
+
for item in raw_params:
|
|
25
|
+
if "=" not in item:
|
|
26
|
+
raise click.BadParameter(f"Invalid parameter format: {item} (expected KEY=VAL)")
|
|
27
|
+
key, _, value = item.partition("=")
|
|
28
|
+
params[key.strip()] = value.strip()
|
|
29
|
+
return params
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
# Build group
|
|
34
|
+
# ------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.group("build", help="Manage Jenkins builds.")
|
|
38
|
+
def build_group():
|
|
39
|
+
"""Build management commands."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@build_group.command("trigger")
|
|
43
|
+
@click.argument("job_name")
|
|
44
|
+
@click.option(
|
|
45
|
+
"--params", "-p",
|
|
46
|
+
multiple=True,
|
|
47
|
+
help="Build parameters as KEY=VAL pairs. Repeatable.",
|
|
48
|
+
)
|
|
49
|
+
@click.pass_context
|
|
50
|
+
def trigger(ctx: click.Context, job_name: str, params: tuple[str, ...]):
|
|
51
|
+
"""Trigger a new build for JOB_NAME.
|
|
52
|
+
|
|
53
|
+
Use --params/-p KEY=VAL to pass build parameters (repeat for multiple).
|
|
54
|
+
"""
|
|
55
|
+
client = get_client(ctx)
|
|
56
|
+
fmt = get_formatter(ctx)
|
|
57
|
+
try:
|
|
58
|
+
param_dict = _parse_params(params) if params else None
|
|
59
|
+
result = trigger_build(client, job_name, param_dict)
|
|
60
|
+
fmt.print_success(f"Build triggered for {job_name}")
|
|
61
|
+
fmt.print_json(result)
|
|
62
|
+
except JenkinsError as exc:
|
|
63
|
+
fmt.print_error(str(exc))
|
|
64
|
+
raise SystemExit(1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@build_group.command("list")
|
|
68
|
+
@click.argument("job_name")
|
|
69
|
+
@click.option("--limit", "-n", default=10, type=int, help="Maximum builds to show.")
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def list_builds(ctx: click.Context, job_name: str, limit: int):
|
|
72
|
+
"""List recent builds for JOB_NAME."""
|
|
73
|
+
client = get_client(ctx)
|
|
74
|
+
fmt = get_formatter(ctx)
|
|
75
|
+
try:
|
|
76
|
+
builds = get_builds(client, job_name, limit=limit)
|
|
77
|
+
if not builds:
|
|
78
|
+
fmt.print_info(f"No builds found for {job_name}")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
headers = ["Number", "Result", "Timestamp", "Duration", "Building"]
|
|
82
|
+
rows = [
|
|
83
|
+
[
|
|
84
|
+
b.get("number", ""),
|
|
85
|
+
str(b.get("result") or "IN PROGRESS"),
|
|
86
|
+
str(b.get("timestamp", "")),
|
|
87
|
+
str(b.get("duration", "")),
|
|
88
|
+
str(b.get("building", False)),
|
|
89
|
+
]
|
|
90
|
+
for b in builds
|
|
91
|
+
]
|
|
92
|
+
fmt.print_table(headers, rows, title=f"Builds for {job_name}")
|
|
93
|
+
except JenkinsError as exc:
|
|
94
|
+
fmt.print_error(str(exc))
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@build_group.command("get")
|
|
99
|
+
@click.argument("job_name")
|
|
100
|
+
@click.argument("number", type=int)
|
|
101
|
+
@click.pass_context
|
|
102
|
+
def get_build_cmd(ctx: click.Context, job_name: str, number: int):
|
|
103
|
+
"""Show details for a specific build."""
|
|
104
|
+
client = get_client(ctx)
|
|
105
|
+
fmt = get_formatter(ctx)
|
|
106
|
+
try:
|
|
107
|
+
build = get_build(client, job_name, number)
|
|
108
|
+
fmt.print_json(build)
|
|
109
|
+
except JenkinsError as exc:
|
|
110
|
+
fmt.print_error(str(exc))
|
|
111
|
+
raise SystemExit(1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@build_group.command("log")
|
|
115
|
+
@click.argument("job_name")
|
|
116
|
+
@click.argument("number", type=int)
|
|
117
|
+
@click.pass_context
|
|
118
|
+
def log_cmd(ctx: click.Context, job_name: str, number: int):
|
|
119
|
+
"""Show console log for a build."""
|
|
120
|
+
client = get_client(ctx)
|
|
121
|
+
fmt = get_formatter(ctx)
|
|
122
|
+
try:
|
|
123
|
+
log_text = get_build_log(client, job_name, number)
|
|
124
|
+
fmt.console.print(log_text)
|
|
125
|
+
except JenkinsError as exc:
|
|
126
|
+
fmt.print_error(str(exc))
|
|
127
|
+
raise SystemExit(1)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@build_group.command("stop")
|
|
131
|
+
@click.argument("job_name")
|
|
132
|
+
@click.argument("number", type=int)
|
|
133
|
+
@click.pass_context
|
|
134
|
+
def stop_cmd(ctx: click.Context, job_name: str, number: int):
|
|
135
|
+
"""Stop a running build."""
|
|
136
|
+
client = get_client(ctx)
|
|
137
|
+
fmt = get_formatter(ctx)
|
|
138
|
+
try:
|
|
139
|
+
stop_build(client, job_name, number)
|
|
140
|
+
fmt.print_success(f"Build {job_name}#{number} stopped")
|
|
141
|
+
except JenkinsError as exc:
|
|
142
|
+
fmt.print_error(str(exc))
|
|
143
|
+
raise SystemExit(1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@build_group.command("queue")
|
|
147
|
+
@click.pass_context
|
|
148
|
+
def queue_cmd(ctx: click.Context):
|
|
149
|
+
"""Show the current Jenkins build queue."""
|
|
150
|
+
client = get_client(ctx)
|
|
151
|
+
fmt = get_formatter(ctx)
|
|
152
|
+
try:
|
|
153
|
+
items = get_queue(client)
|
|
154
|
+
if not items:
|
|
155
|
+
fmt.print_info("Build queue is empty")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
headers = ["ID", "Task", "Why", "In Queue Since"]
|
|
159
|
+
rows = [
|
|
160
|
+
[
|
|
161
|
+
str(item.get("id", "")),
|
|
162
|
+
str(item.get("task", {}).get("name", "")),
|
|
163
|
+
str(item.get("why", "")),
|
|
164
|
+
str(item.get("inQueueSince", "")),
|
|
165
|
+
]
|
|
166
|
+
for item in items
|
|
167
|
+
]
|
|
168
|
+
fmt.print_table(headers, rows, title="Build Queue")
|
|
169
|
+
except JenkinsError as exc:
|
|
170
|
+
fmt.print_error(str(exc))
|
|
171
|
+
raise SystemExit(1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@build_group.command("artifacts")
|
|
175
|
+
@click.argument("job_name")
|
|
176
|
+
@click.argument("build_number")
|
|
177
|
+
@click.pass_context
|
|
178
|
+
def artifacts_cmd(ctx: click.Context, job_name: str, build_number: str):
|
|
179
|
+
"""List artifacts for a build."""
|
|
180
|
+
client = get_client(ctx)
|
|
181
|
+
fmt = get_formatter(ctx)
|
|
182
|
+
try:
|
|
183
|
+
data = get_build_artifacts(client, job_name, build_number)
|
|
184
|
+
artifacts = data.get("artifacts", [])
|
|
185
|
+
if not artifacts:
|
|
186
|
+
fmt.print_info("No artifacts found.")
|
|
187
|
+
return
|
|
188
|
+
headers = ["File Name", "Relative Path"]
|
|
189
|
+
rows = [[a["fileName"], a["relativePath"]] for a in artifacts]
|
|
190
|
+
fmt.print_table(headers, rows, title=f"Artifacts: {job_name} #{build_number}")
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
fmt.print_error(str(exc))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def register(parent_group: click.Group):
|
|
196
|
+
"""Register the build subgroup under the parent Click group."""
|
|
197
|
+
parent_group.add_command(build_group)
|
jcli/plugins/config.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Configuration management CLI commands for jcli."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from jcli.sdk.config import Config, ProfileNotFoundError
|
|
6
|
+
from jcli.sdk.output.formatter import OutputFormatter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_config(ctx: click.Context) -> Config:
|
|
10
|
+
"""Get or create Config from Click context."""
|
|
11
|
+
if "config" not in ctx.obj:
|
|
12
|
+
ctx.obj["config"] = Config().load()
|
|
13
|
+
return ctx.obj["config"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_formatter(ctx: click.Context) -> OutputFormatter:
|
|
17
|
+
"""Get or create OutputFormatter from Click context."""
|
|
18
|
+
if "formatter" not in ctx.obj:
|
|
19
|
+
fmt = ctx.obj.get("format", "table")
|
|
20
|
+
ctx.obj["formatter"] = OutputFormatter(fmt)
|
|
21
|
+
return ctx.obj["formatter"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
def config_group():
|
|
26
|
+
"""Manage jcli configuration."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@config_group.command("init")
|
|
31
|
+
@click.option("--force", is_flag=True, help="Overwrite existing config file")
|
|
32
|
+
@click.pass_context
|
|
33
|
+
def config_init(ctx: click.Context, force: bool):
|
|
34
|
+
"""Initialize configuration file with default template."""
|
|
35
|
+
cfg = _get_config(ctx)
|
|
36
|
+
fmt = _get_formatter(ctx)
|
|
37
|
+
|
|
38
|
+
if cfg.config_path.exists() and not force:
|
|
39
|
+
fmt.print_error(f"Config file already exists: {cfg.config_path}")
|
|
40
|
+
fmt.print_info("Use --force to overwrite")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Create fresh config with template
|
|
44
|
+
cfg = Config()
|
|
45
|
+
cfg.load()
|
|
46
|
+
cfg.save()
|
|
47
|
+
|
|
48
|
+
fmt.print_success(f"Config file created: {cfg.config_path}")
|
|
49
|
+
fmt.print_info("Edit it with your Jenkins server details:")
|
|
50
|
+
fmt.print_info(f" url: https://jenkins.example.com")
|
|
51
|
+
fmt.print_info(f" username: admin")
|
|
52
|
+
fmt.print_info(f" api_token: your-api-token-here")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@config_group.command("show")
|
|
56
|
+
@click.option("--profile", "-p", help="Show specific profile (default: active)")
|
|
57
|
+
@click.pass_context
|
|
58
|
+
def config_show(ctx: click.Context, profile: str | None):
|
|
59
|
+
"""Show configuration details."""
|
|
60
|
+
cfg = _get_config(ctx)
|
|
61
|
+
fmt = _get_formatter(ctx)
|
|
62
|
+
|
|
63
|
+
if profile:
|
|
64
|
+
try:
|
|
65
|
+
data = cfg.get_profile(profile)
|
|
66
|
+
except ProfileNotFoundError:
|
|
67
|
+
fmt.print_error(f"Profile '{profile}' not found")
|
|
68
|
+
return
|
|
69
|
+
active_name = cfg.get_active_profile_name()
|
|
70
|
+
is_active = " (active)" if profile == active_name else ""
|
|
71
|
+
fmt.print_info(f"Profile: {profile}{is_active}")
|
|
72
|
+
_show_profile(fmt, data)
|
|
73
|
+
else:
|
|
74
|
+
active_name = cfg.get_active_profile_name()
|
|
75
|
+
fmt.print_info(f"Config file: {cfg.config_path}")
|
|
76
|
+
fmt.print_info(f"Active profile: {active_name}")
|
|
77
|
+
fmt.print_info("")
|
|
78
|
+
try:
|
|
79
|
+
data = cfg.get_active_profile()
|
|
80
|
+
_show_profile(fmt, data)
|
|
81
|
+
except ProfileNotFoundError:
|
|
82
|
+
fmt.print_error(f"Active profile '{active_name}' not found")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _show_profile(fmt: OutputFormatter, data: dict):
|
|
86
|
+
"""Display profile fields."""
|
|
87
|
+
fields = [
|
|
88
|
+
("url", "URL"),
|
|
89
|
+
("username", "Username"),
|
|
90
|
+
("api_token", "API Token"),
|
|
91
|
+
("description", "Description"),
|
|
92
|
+
]
|
|
93
|
+
for key, label in fields:
|
|
94
|
+
value = data.get(key, "")
|
|
95
|
+
# Mask token for display
|
|
96
|
+
if key == "api_token" and value and len(value) > 8:
|
|
97
|
+
display = value[:4] + "****" + value[-4:]
|
|
98
|
+
else:
|
|
99
|
+
display = value
|
|
100
|
+
fmt.print_info(f" {label}: {display}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@config_group.command("list")
|
|
104
|
+
@click.pass_context
|
|
105
|
+
def config_list(ctx: click.Context):
|
|
106
|
+
"""List all configured profiles."""
|
|
107
|
+
cfg = _get_config(ctx)
|
|
108
|
+
fmt = _get_formatter(ctx)
|
|
109
|
+
|
|
110
|
+
profiles = cfg.list_profiles()
|
|
111
|
+
active_name = cfg.get_active_profile_name()
|
|
112
|
+
|
|
113
|
+
if not profiles:
|
|
114
|
+
fmt.print_info("No profiles configured. Run 'jcli config init' to create one.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
headers = ["Name", "URL", "Username", "Active"]
|
|
118
|
+
rows = []
|
|
119
|
+
for name, data in profiles.items():
|
|
120
|
+
active_mark = "✓" if name == active_name else ""
|
|
121
|
+
rows.append([
|
|
122
|
+
name,
|
|
123
|
+
data.get("url", ""),
|
|
124
|
+
data.get("username", ""),
|
|
125
|
+
active_mark,
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
fmt.print_table(headers, rows, title="Profiles")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@config_group.command("set")
|
|
132
|
+
@click.argument("profile")
|
|
133
|
+
@click.argument("field", type=click.Choice(["url", "username", "api_token", "description"]))
|
|
134
|
+
@click.argument("value")
|
|
135
|
+
@click.pass_context
|
|
136
|
+
def config_set(ctx: click.Context, profile: str, field: str, value: str):
|
|
137
|
+
"""Set a configuration field for a profile.
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
jcli config set dev url https://jenkins-dev.example.com
|
|
141
|
+
jcli config set dev username admin
|
|
142
|
+
jcli config set dev api_token abc123
|
|
143
|
+
jcli config set dev description "Development Jenkins"
|
|
144
|
+
"""
|
|
145
|
+
cfg = _get_config(ctx)
|
|
146
|
+
fmt = _get_formatter(ctx)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
data = cfg.get_profile(profile)
|
|
150
|
+
except ProfileNotFoundError:
|
|
151
|
+
fmt.print_error(f"Profile '{profile}' not found")
|
|
152
|
+
fmt.print_info(f"Create it first with: jcli config add {profile}")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# Update the field
|
|
156
|
+
data[field] = value
|
|
157
|
+
cfg.add_profile(
|
|
158
|
+
name=profile,
|
|
159
|
+
url=data.get("url", ""),
|
|
160
|
+
username=data.get("username", ""),
|
|
161
|
+
api_token=data.get("api_token", ""),
|
|
162
|
+
description=data.get("description", ""),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
fmt.print_success(f"Updated profile '{profile}': {field} = {value}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@config_group.command("add")
|
|
169
|
+
@click.argument("profile")
|
|
170
|
+
@click.option("--url", "-u", default="", help="Jenkins server URL")
|
|
171
|
+
@click.option("--username", default="", help="Jenkins username")
|
|
172
|
+
@click.option("--api-token", default="", help="Jenkins API token")
|
|
173
|
+
@click.option("--description", "-d", default="", help="Profile description")
|
|
174
|
+
@click.pass_context
|
|
175
|
+
def config_add(
|
|
176
|
+
ctx: click.Context,
|
|
177
|
+
profile: str,
|
|
178
|
+
url: str,
|
|
179
|
+
username: str,
|
|
180
|
+
api_token: str,
|
|
181
|
+
description: str,
|
|
182
|
+
):
|
|
183
|
+
"""Add a new configuration profile.
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
jcli config add dev --url https://jenkins-dev.example.com --username admin
|
|
187
|
+
jcli config add prod -u https://jenkins-prod.example.com -d "Production"
|
|
188
|
+
"""
|
|
189
|
+
cfg = _get_config(ctx)
|
|
190
|
+
fmt = _get_formatter(ctx)
|
|
191
|
+
|
|
192
|
+
# Check if profile already exists
|
|
193
|
+
profiles = cfg.list_profiles()
|
|
194
|
+
if profile in profiles:
|
|
195
|
+
fmt.print_error(f"Profile '{profile}' already exists")
|
|
196
|
+
fmt.print_info(f"Use 'jcli config set {profile} <field> <value>' to update")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
cfg.add_profile(
|
|
200
|
+
name=profile,
|
|
201
|
+
url=url or "https://jenkins.example.com",
|
|
202
|
+
username=username or "admin",
|
|
203
|
+
api_token=api_token or "",
|
|
204
|
+
description=description or f"{profile} Jenkins instance",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
fmt.print_success(f"Profile '{profile}' added")
|
|
208
|
+
fmt.print_info("Configure it with:")
|
|
209
|
+
fmt.print_info(f" jcli config set {profile} url https://your-jenkins.com")
|
|
210
|
+
fmt.print_info(f" jcli config set {profile} api_token your-token")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@config_group.command("delete")
|
|
214
|
+
@click.argument("profile")
|
|
215
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
216
|
+
@click.pass_context
|
|
217
|
+
def config_delete(ctx: click.Context, profile: str, yes: bool):
|
|
218
|
+
"""Delete a configuration profile.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
jcli config delete dev
|
|
222
|
+
jcli config delete old-server --yes
|
|
223
|
+
"""
|
|
224
|
+
cfg = _get_config(ctx)
|
|
225
|
+
fmt = _get_formatter(ctx)
|
|
226
|
+
|
|
227
|
+
profiles = cfg.list_profiles()
|
|
228
|
+
if profile not in profiles:
|
|
229
|
+
fmt.print_error(f"Profile '{profile}' not found")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
if not yes:
|
|
233
|
+
if not click.confirm(f"Delete profile '{profile}'?"):
|
|
234
|
+
fmt.print_info("Cancelled")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
cfg.remove_profile(profile)
|
|
238
|
+
fmt.print_success(f"Profile '{profile}' deleted")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@config_group.command("use")
|
|
242
|
+
@click.argument("profile")
|
|
243
|
+
@click.pass_context
|
|
244
|
+
def config_use(ctx: click.Context, profile: str):
|
|
245
|
+
"""Switch active profile.
|
|
246
|
+
|
|
247
|
+
Examples:
|
|
248
|
+
jcli config use prod
|
|
249
|
+
jcli config use dev
|
|
250
|
+
"""
|
|
251
|
+
cfg = _get_config(ctx)
|
|
252
|
+
fmt = _get_formatter(ctx)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
cfg.set_active_profile(profile)
|
|
256
|
+
except ProfileNotFoundError:
|
|
257
|
+
fmt.print_error(f"Profile '{profile}' not found")
|
|
258
|
+
profiles = cfg.list_profiles()
|
|
259
|
+
if profiles:
|
|
260
|
+
fmt.print_info(f"Available profiles: {', '.join(profiles.keys())}")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
fmt.print_success(f"Active profile set to '{profile}'")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def register(group: click.Group) -> None:
|
|
267
|
+
"""Register config commands with the main CLI group."""
|
|
268
|
+
group.add_command(config_group, "config")
|