python-jcli 1.0.0__py3-none-any.whl → 1.0.1__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.
Files changed (44) hide show
  1. jcli/__init__.py +1 -1
  2. jcli/cli.py +30 -10
  3. jcli/cli_helpers.py +5 -42
  4. jcli/skills/jcli/build/SKILL.md +123 -0
  5. jcli/specs/_auth.yaml +15 -0
  6. jcli/{plugins/skills.py → specs/plugins/jcli_commands.py} +64 -46
  7. jcli/specs/plugins/jenkins_auth.py +75 -0
  8. jcli/specs/plugins/jenkins_methods.py +332 -0
  9. jcli/specs/plugins/jenkins_pipeline_validate.py +51 -0
  10. jcli/specs/plugins/jenkins_plugin.py +77 -0
  11. jcli/specs/plugins/jenkins_system.py +137 -0
  12. jcli/specs/resources/build.yaml +160 -0
  13. jcli/specs/resources/credential.yaml +97 -0
  14. jcli/specs/resources/job.yaml +184 -0
  15. jcli/specs/resources/node.yaml +69 -0
  16. jcli/specs/resources/pipeline.yaml +62 -0
  17. jcli/specs/resources/plugin.yaml +65 -0
  18. jcli/specs/resources/system.yaml +72 -0
  19. jcli/specs/resources/view.yaml +75 -0
  20. {python_jcli-1.0.0.dist-info → python_jcli-1.0.1.dist-info}/METADATA +10 -9
  21. python_jcli-1.0.1.dist-info/RECORD +40 -0
  22. jcli/plugins/__init__.py +0 -28
  23. jcli/plugins/build.py +0 -197
  24. jcli/plugins/config.py +0 -268
  25. jcli/plugins/credential.py +0 -210
  26. jcli/plugins/job.py +0 -425
  27. jcli/plugins/node.py +0 -134
  28. jcli/plugins/pipeline.py +0 -108
  29. jcli/plugins/plugin.py +0 -173
  30. jcli/plugins/system.py +0 -173
  31. jcli/plugins/view.py +0 -148
  32. jcli/sdk/build.py +0 -182
  33. jcli/sdk/credential.py +0 -143
  34. jcli/sdk/job.py +0 -191
  35. jcli/sdk/job_templates.py +0 -128
  36. jcli/sdk/node.py +0 -139
  37. jcli/sdk/pipeline.py +0 -121
  38. jcli/sdk/plugin.py +0 -155
  39. jcli/sdk/system.py +0 -202
  40. jcli/sdk/view.py +0 -94
  41. python_jcli-1.0.0.dist-info/RECORD +0 -44
  42. {python_jcli-1.0.0.dist-info → python_jcli-1.0.1.dist-info}/WHEEL +0 -0
  43. {python_jcli-1.0.0.dist-info → python_jcli-1.0.1.dist-info}/entry_points.txt +0 -0
  44. {python_jcli-1.0.0.dist-info → python_jcli-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,75 @@
1
+ name: view
2
+ description: Manage Jenkins views
3
+ methods:
4
+ list:
5
+ description: List all Jenkins views
6
+ http:
7
+ method: GET
8
+ path: api/json
9
+ query:
10
+ tree: "views[name,url,jobs[name]]"
11
+ output:
12
+ items_path: $.views
13
+ fields:
14
+ - name: name
15
+ alias: Name
16
+ - name: url
17
+ alias: URL
18
+ - name: jobs
19
+ alias: Jobs
20
+ get:
21
+ description: Get details for a view
22
+ http:
23
+ method: GET
24
+ path: "view/{{ name }}/api/json"
25
+ params:
26
+ path:
27
+ - name: name
28
+ type: string
29
+ argument: true
30
+ create:
31
+ description: Create a view from XML config
32
+ http:
33
+ method: POST
34
+ path: createView
35
+ query:
36
+ name: '{{ name }}'
37
+ headers:
38
+ Content-Type: application/xml
39
+ body: '{{ config_xml }}'
40
+ params:
41
+ query:
42
+ - name: name
43
+ type: string
44
+ argument: true
45
+ body:
46
+ - name: config_xml
47
+ type: file
48
+ argument: true
49
+ update:
50
+ description: Update a view's configuration
51
+ http:
52
+ method: POST
53
+ path: "view/{{ name }}/config.xml"
54
+ headers:
55
+ Content-Type: application/xml
56
+ body: '{{ config_xml }}'
57
+ params:
58
+ path:
59
+ - name: name
60
+ type: string
61
+ argument: true
62
+ body:
63
+ - name: config_xml
64
+ type: file
65
+ argument: true
66
+ delete:
67
+ description: Delete a view
68
+ http:
69
+ method: POST
70
+ path: "view/{{ name }}/doDelete"
71
+ params:
72
+ path:
73
+ - name: name
74
+ type: string
75
+ argument: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jcli
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Python Jenkins CLI tool
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -32,10 +32,10 @@ A command-line tool for managing Jenkins servers. jcli wraps the Jenkins REST AP
32
32
 
33
33
  ## Architecture
34
34
 
35
- jcli 2.0 is driven by [cliyard](https://pypi.org/project/cliyard/) YAML specs instead of hand-written Click command groups. At startup, `jcli/cli.py` locates the spec directory (`JCLI_SPEC_DIR` env var > package-local `specs/` > repo-root `specs/`) and calls `cliyard.runtime.create_cli()` to build the whole command tree. Commands stay declarative: adding or changing a command is an edit to a YAML file, not Python plumbing.
35
+ jcli 2.0 is driven by [cliyard](https://pypi.org/project/cliyard/) YAML specs instead of hand-written Click command groups. At startup, `jcli/cli.py` locates the spec directory (`JCLI_SPEC_DIR` env var > package-local `jcli/specs/`) and calls `cliyard.runtime.create_cli()` to build the whole command tree. Commands stay declarative: adding or changing a command is an edit to a YAML file, not Python plumbing.
36
36
 
37
37
  ```
38
- specs/
38
+ jcli/specs/
39
39
  _auth.yaml Server auth chain (basic auth + crumb, as cliyard plugins)
40
40
  resources/ One YAML spec per Jenkins domain
41
41
  job.yaml job list/get/create/config/copy/enable/disable/delete
@@ -47,11 +47,12 @@ specs/
47
47
  view.yaml view list/get/create/delete
48
48
  system.yaml system info/load/quiet-down/restart/script
49
49
  plugins/ Python method plugins for non-trivial logic
50
- jenkins_auth.py Basic + crumb authentication
51
- jenkins_methods.py Build trigger/replay, node create
50
+ jcli_commands.py Skills + completion commands
51
+ jenkins_auth.py Basic + crumb authentication
52
+ jenkins_methods.py Build trigger/replay, node create
52
53
  jenkins_pipeline_validate.py Jenkinsfile validation
53
- jenkins_plugin.py Plugin install/uninstall/update-center
54
- jenkins_system.py Groovy script, quiet-down, restart
54
+ jenkins_plugin.py Plugin install/uninstall/update-center
55
+ jenkins_system.py Groovy script, quiet-down, restart
55
56
  ```
56
57
 
57
58
  A resource spec maps a command name to either a plain HTTP call (method, path, params, output mapping) or a `plugin:` reference. The auth chain in `_auth.yaml` injects `Authorization` and crumb headers into every request.
@@ -315,15 +316,15 @@ pytest --cov=jcli --cov-report=term-missing
315
316
  ```
316
317
  jcli/
317
318
  cli.py Entry point: builds the Click CLI from cliyard YAML specs
318
- cli_helpers.py Global option injection and profile/format plumbing
319
+ cli_helpers.py Output-formatter helper used by the cliyard command plugins
319
320
  __init__.py Version
320
- plugins/ Legacy hand-written command modules (kept for SDK compatibility)
321
321
  sdk/ Shared libraries
322
322
  client.py Jenkins REST API client (HTTP, auth, crumb)
323
323
  config.py Configuration management (YAML, env vars)
324
324
  output/
325
325
  formatter.py Table/JSON/YAML output formatting
326
326
  exceptions.py Typed exceptions
327
+ skills/ Bundled skill documentation
327
328
  specs/ cliyard YAML specs (command definitions)
328
329
  _auth.yaml Auth chain: basic + crumb plugins
329
330
  resources/ One YAML per Jenkins domain (job, build, node, ...)
@@ -0,0 +1,40 @@
1
+ jcli/__init__.py,sha256=d4QHYmS_30j0hPN8NmNPnQ_Z0TphDRbu4MtQj9cT9e8,22
2
+ jcli/__main__.py,sha256=ARL8XYr45T4pIf0gnyCCbK8uljcn1LF7TKcFapxHt_k,34
3
+ jcli/cli.py,sha256=krV91DUIFEQnO_OzS08JahY5GFIQB5Omwvj2YcQVX_Y,21921
4
+ jcli/cli_helpers.py,sha256=xtMiNfoicBnc4g4qi56PJT3S8KwS56up5tOANNE-SmY,610
5
+ jcli/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ jcli/sdk/client.py,sha256=hnBZcS82ziPyHepPWq7femUqQppw8QCtwbdMrWYVrxc,8723
7
+ jcli/sdk/config.py,sha256=fSBTWqE0i8jpzmtGfo2M-Ln2Y5eQRQD0StS76glyCsI,6992
8
+ jcli/sdk/exceptions.py,sha256=zv5oYOr_t0rma4lVudLp3gLdL5idSzlKGc7g2MowcL0,1737
9
+ jcli/sdk/output/__init__.py,sha256=Rt0F77oq24tqHvTDV4HyWKAjDtOyPOh9m7ucFy7B77s,70
10
+ jcli/sdk/output/formatter.py,sha256=OUjQxeD-2iSNncuuRaLa01qLi899ux3F39KNkc8ik2Q,3876
11
+ jcli/skills/jcli/SKILL.md,sha256=X9xHxRvNa4F3hUN8bhckBXAJZx2MWY58hGqqd0y158M,3040
12
+ jcli/skills/jcli/auth/SKILL.md,sha256=4_DLD51SwKTXh2Xi0UiQbqG4uXw7qIKv2SOOopS2Qn0,1996
13
+ jcli/skills/jcli/build/SKILL.md,sha256=J2UbuAAuAHiIrMbnzLH2Q8mQcRvy3JkuTS9GyhMfR64,3557
14
+ jcli/skills/jcli/credential/SKILL.md,sha256=3VsVdGalsIySvGfFO79IdL0b5OjF7kJBS9_4P56Wt2A,1244
15
+ jcli/skills/jcli/job/SKILL.md,sha256=bmGyiyLXS2n395bA31gehcMRe094SUBq-FzUlUDvpQM,2066
16
+ jcli/skills/jcli/node/SKILL.md,sha256=k4qi6ApspdIlBcxb0v2ItPrPwT7d9AnFq4N-vGhGcqY,1247
17
+ jcli/skills/jcli/pipeline/SKILL.md,sha256=kUwnxBCUx53zi7efVgtJg9ainO-YuX3griaCqoUyzxU,1231
18
+ jcli/skills/jcli/plugin/SKILL.md,sha256=--1t5LekIHAtnCWUnsQ9BKkPIgAQ7n-p7_QGd8H2jbI,1178
19
+ jcli/skills/jcli/system/SKILL.md,sha256=SHdcLbaJyifjt4KJlmdcHdYLsFGwpF-6lqiWdYpw3co,1932
20
+ jcli/skills/jcli/view/SKILL.md,sha256=f2QZEzvFocIlR7Yh0RKnp5y4sijwUJU_1fxiW7gKJcA,1047
21
+ jcli/specs/_auth.yaml,sha256=h4wfmdjIetxCAQGpPHmp89FIRPXHP9N5vwDkw6PIPcc,276
22
+ jcli/specs/plugins/jcli_commands.py,sha256=6d9HEmPeyjwTaY6z9dFUbD7TYjkOn2C4WkUb_J2U1Wc,15878
23
+ jcli/specs/plugins/jenkins_auth.py,sha256=CbjWOHCEUoKpbV4WkRFTUCoPCLF5srHnWIkcIG1TUAA,2714
24
+ jcli/specs/plugins/jenkins_methods.py,sha256=9Rd8xxqXQl8ikKSlB15kgp61Y0UQDgSL7XsY5u5L2-Q,11793
25
+ jcli/specs/plugins/jenkins_pipeline_validate.py,sha256=N7DuQgtam_idKQaPWSsmxa4G4ohf-c8YUn6hLfF-znQ,1868
26
+ jcli/specs/plugins/jenkins_plugin.py,sha256=Bdg1CZI6kHsa1Po3KaR6057ON2t1SmuRt17MvRLc63U,2533
27
+ jcli/specs/plugins/jenkins_system.py,sha256=n_m_c8gHfIXmqUyDSiGZytblDM7H5QQCoDUEBhI30NQ,4289
28
+ jcli/specs/resources/build.yaml,sha256=niDH8L8HSHrIa_u5BMP9FJXgd3i2UWiTXUXOjKzSsM0,3994
29
+ jcli/specs/resources/credential.yaml,sha256=SUTOmYN_lafMAzuhH8noZj3HJgUvwG4FkXytoTcxA-M,2402
30
+ jcli/specs/resources/job.yaml,sha256=jvy-ARhPKCTaEpSyU1bJ7C1_HHo7l_Kb0b8yOku3FHs,4075
31
+ jcli/specs/resources/node.yaml,sha256=FhMEQ1kxucP6XyoTiVys7yoy8y3o-stbBdKv_SgIckA,1594
32
+ jcli/specs/resources/pipeline.yaml,sha256=kk-0B3adC8gA6YaFOOfQ5uAZQ11X3wCfvO-nLTx8hoE,1646
33
+ jcli/specs/resources/plugin.yaml,sha256=pqR5GUi1FHAL8iFsPbGCEr3sKSmR1UwK9CcZR5dLvJI,1561
34
+ jcli/specs/resources/system.yaml,sha256=ZhRcG5FTrKqIz-QNTxhPdO-y0WE29QVo3-U8oiMXYcc,1529
35
+ jcli/specs/resources/view.yaml,sha256=ELWZFZy667gh7SzwXCpxTacaduiqH7n2PAKdqzgnDLw,1596
36
+ python_jcli-1.0.1.dist-info/METADATA,sha256=4sTjfkW2xI6-6Ghz6RacmyZEvO41jBxAULceIVA_xZc,12519
37
+ python_jcli-1.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
38
+ python_jcli-1.0.1.dist-info/entry_points.txt,sha256=g-Gb8voGH3Ru4NEoCUMJXJiMZMapdAfYi6Xt1phyFi4,39
39
+ python_jcli-1.0.1.dist-info/top_level.txt,sha256=WFqrbWSEfhvQfbN2m-_Wi5KKCwv1_23LhzPUqo7AdQc,5
40
+ python_jcli-1.0.1.dist-info/RECORD,,
jcli/plugins/__init__.py DELETED
@@ -1,28 +0,0 @@
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 DELETED
@@ -1,197 +0,0 @@
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 DELETED
@@ -1,268 +0,0 @@
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")