python-jcli 1.0.0__tar.gz → 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {python_jcli-1.0.0 → python_jcli-1.0.1}/PKG-INFO +10 -9
  2. {python_jcli-1.0.0 → python_jcli-1.0.1}/README.md +9 -8
  3. python_jcli-1.0.1/jcli/__init__.py +1 -0
  4. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/cli.py +30 -10
  5. python_jcli-1.0.1/jcli/cli_helpers.py +21 -0
  6. python_jcli-1.0.1/jcli/skills/jcli/build/SKILL.md +123 -0
  7. python_jcli-1.0.1/jcli/specs/_auth.yaml +15 -0
  8. python_jcli-1.0.0/jcli/plugins/skills.py → python_jcli-1.0.1/jcli/specs/plugins/jcli_commands.py +64 -46
  9. python_jcli-1.0.1/jcli/specs/plugins/jenkins_auth.py +75 -0
  10. python_jcli-1.0.1/jcli/specs/plugins/jenkins_methods.py +332 -0
  11. python_jcli-1.0.1/jcli/specs/plugins/jenkins_pipeline_validate.py +51 -0
  12. python_jcli-1.0.1/jcli/specs/plugins/jenkins_plugin.py +77 -0
  13. python_jcli-1.0.1/jcli/specs/plugins/jenkins_system.py +137 -0
  14. python_jcli-1.0.1/jcli/specs/resources/build.yaml +160 -0
  15. python_jcli-1.0.1/jcli/specs/resources/credential.yaml +97 -0
  16. python_jcli-1.0.1/jcli/specs/resources/job.yaml +184 -0
  17. python_jcli-1.0.1/jcli/specs/resources/node.yaml +69 -0
  18. python_jcli-1.0.1/jcli/specs/resources/pipeline.yaml +62 -0
  19. python_jcli-1.0.1/jcli/specs/resources/plugin.yaml +65 -0
  20. python_jcli-1.0.1/jcli/specs/resources/system.yaml +72 -0
  21. python_jcli-1.0.1/jcli/specs/resources/view.yaml +75 -0
  22. {python_jcli-1.0.0 → python_jcli-1.0.1}/pyproject.toml +1 -1
  23. {python_jcli-1.0.0 → python_jcli-1.0.1}/python_jcli.egg-info/PKG-INFO +10 -9
  24. {python_jcli-1.0.0 → python_jcli-1.0.1}/python_jcli.egg-info/SOURCES.txt +18 -29
  25. python_jcli-1.0.1/tests/test_build_log.py +197 -0
  26. {python_jcli-1.0.0 → python_jcli-1.0.1}/tests/test_cli.py +1 -1
  27. {python_jcli-1.0.0 → python_jcli-1.0.1}/tests/test_client.py +2 -2
  28. {python_jcli-1.0.0 → python_jcli-1.0.1}/tests/test_skills.py +24 -15
  29. python_jcli-1.0.0/jcli/__init__.py +0 -1
  30. python_jcli-1.0.0/jcli/cli_helpers.py +0 -58
  31. python_jcli-1.0.0/jcli/plugins/__init__.py +0 -28
  32. python_jcli-1.0.0/jcli/plugins/build.py +0 -197
  33. python_jcli-1.0.0/jcli/plugins/config.py +0 -268
  34. python_jcli-1.0.0/jcli/plugins/credential.py +0 -210
  35. python_jcli-1.0.0/jcli/plugins/job.py +0 -425
  36. python_jcli-1.0.0/jcli/plugins/node.py +0 -134
  37. python_jcli-1.0.0/jcli/plugins/pipeline.py +0 -108
  38. python_jcli-1.0.0/jcli/plugins/plugin.py +0 -173
  39. python_jcli-1.0.0/jcli/plugins/system.py +0 -173
  40. python_jcli-1.0.0/jcli/plugins/view.py +0 -148
  41. python_jcli-1.0.0/jcli/sdk/build.py +0 -182
  42. python_jcli-1.0.0/jcli/sdk/credential.py +0 -143
  43. python_jcli-1.0.0/jcli/sdk/job.py +0 -191
  44. python_jcli-1.0.0/jcli/sdk/job_templates.py +0 -128
  45. python_jcli-1.0.0/jcli/sdk/node.py +0 -139
  46. python_jcli-1.0.0/jcli/sdk/pipeline.py +0 -121
  47. python_jcli-1.0.0/jcli/sdk/plugin.py +0 -155
  48. python_jcli-1.0.0/jcli/sdk/system.py +0 -202
  49. python_jcli-1.0.0/jcli/sdk/view.py +0 -94
  50. python_jcli-1.0.0/tests/test_build.py +0 -915
  51. python_jcli-1.0.0/tests/test_credential.py +0 -774
  52. python_jcli-1.0.0/tests/test_job.py +0 -1375
  53. python_jcli-1.0.0/tests/test_node.py +0 -420
  54. python_jcli-1.0.0/tests/test_pipeline.py +0 -544
  55. python_jcli-1.0.0/tests/test_plugin.py +0 -824
  56. python_jcli-1.0.0/tests/test_system.py +0 -877
  57. python_jcli-1.0.0/tests/test_view.py +0 -679
  58. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/__main__.py +0 -0
  59. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/sdk/__init__.py +0 -0
  60. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/sdk/client.py +0 -0
  61. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/sdk/config.py +0 -0
  62. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/sdk/exceptions.py +0 -0
  63. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/sdk/output/__init__.py +0 -0
  64. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/sdk/output/formatter.py +0 -0
  65. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/SKILL.md +0 -0
  66. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/auth/SKILL.md +0 -0
  67. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/credential/SKILL.md +0 -0
  68. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/job/SKILL.md +0 -0
  69. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/node/SKILL.md +0 -0
  70. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/pipeline/SKILL.md +0 -0
  71. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/plugin/SKILL.md +0 -0
  72. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/system/SKILL.md +0 -0
  73. {python_jcli-1.0.0 → python_jcli-1.0.1}/jcli/skills/jcli/view/SKILL.md +0 -0
  74. {python_jcli-1.0.0 → python_jcli-1.0.1}/python_jcli.egg-info/dependency_links.txt +0 -0
  75. {python_jcli-1.0.0 → python_jcli-1.0.1}/python_jcli.egg-info/entry_points.txt +0 -0
  76. {python_jcli-1.0.0 → python_jcli-1.0.1}/python_jcli.egg-info/requires.txt +0 -0
  77. {python_jcli-1.0.0 → python_jcli-1.0.1}/python_jcli.egg-info/top_level.txt +0 -0
  78. {python_jcli-1.0.0 → python_jcli-1.0.1}/setup.cfg +0 -0
  79. {python_jcli-1.0.0 → python_jcli-1.0.1}/tests/test_cliyard_spike.py +0 -0
  80. {python_jcli-1.0.0 → python_jcli-1.0.1}/tests/test_config.py +0 -0
  81. {python_jcli-1.0.0 → python_jcli-1.0.1}/tests/test_exceptions.py +0 -0
  82. {python_jcli-1.0.0 → python_jcli-1.0.1}/tests/test_replay.py +0 -0
@@ -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, ...)
@@ -15,10 +15,10 @@ A command-line tool for managing Jenkins servers. jcli wraps the Jenkins REST AP
15
15
 
16
16
  ## Architecture
17
17
 
18
- 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.
18
+ 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.
19
19
 
20
20
  ```
21
- specs/
21
+ jcli/specs/
22
22
  _auth.yaml Server auth chain (basic auth + crumb, as cliyard plugins)
23
23
  resources/ One YAML spec per Jenkins domain
24
24
  job.yaml job list/get/create/config/copy/enable/disable/delete
@@ -30,11 +30,12 @@ specs/
30
30
  view.yaml view list/get/create/delete
31
31
  system.yaml system info/load/quiet-down/restart/script
32
32
  plugins/ Python method plugins for non-trivial logic
33
- jenkins_auth.py Basic + crumb authentication
34
- jenkins_methods.py Build trigger/replay, node create
33
+ jcli_commands.py Skills + completion commands
34
+ jenkins_auth.py Basic + crumb authentication
35
+ jenkins_methods.py Build trigger/replay, node create
35
36
  jenkins_pipeline_validate.py Jenkinsfile validation
36
- jenkins_plugin.py Plugin install/uninstall/update-center
37
- jenkins_system.py Groovy script, quiet-down, restart
37
+ jenkins_plugin.py Plugin install/uninstall/update-center
38
+ jenkins_system.py Groovy script, quiet-down, restart
38
39
  ```
39
40
 
40
41
  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.
@@ -298,15 +299,15 @@ pytest --cov=jcli --cov-report=term-missing
298
299
  ```
299
300
  jcli/
300
301
  cli.py Entry point: builds the Click CLI from cliyard YAML specs
301
- cli_helpers.py Global option injection and profile/format plumbing
302
+ cli_helpers.py Output-formatter helper used by the cliyard command plugins
302
303
  __init__.py Version
303
- plugins/ Legacy hand-written command modules (kept for SDK compatibility)
304
304
  sdk/ Shared libraries
305
305
  client.py Jenkins REST API client (HTTP, auth, crumb)
306
306
  config.py Configuration management (YAML, env vars)
307
307
  output/
308
308
  formatter.py Table/JSON/YAML output formatting
309
309
  exceptions.py Typed exceptions
310
+ skills/ Bundled skill documentation
310
311
  specs/ cliyard YAML specs (command definitions)
311
312
  _auth.yaml Auth chain: basic + crumb plugins
312
313
  resources/ One YAML per Jenkins domain (job, build, node, ...)
@@ -0,0 +1 @@
1
+ __version__ = "1.0.1"
@@ -48,21 +48,14 @@ SPEC_DIR_ENV = "JCLI_SPEC_DIR"
48
48
 
49
49
 
50
50
  def _default_spec_dir() -> Path:
51
- """Locate the cliyard YAML spec directory.
51
+ """Locate the cliyard YAML spec directory (ships inside the package).
52
52
 
53
- Priority: ``$JCLI_SPEC_DIR`` > package-local ``specs/`` > repo-root
54
- ``specs/`` (editable/dev layout).
53
+ Priority: ``$JCLI_SPEC_DIR`` env var > package-local ``jcli/specs/``.
55
54
  """
56
55
  env = os.environ.get(SPEC_DIR_ENV)
57
56
  if env:
58
57
  return Path(env)
59
- pkg_specs = Path(__file__).resolve().parent / "specs"
60
- if pkg_specs.is_dir():
61
- return pkg_specs
62
- repo_specs = Path(__file__).resolve().parent.parent / "specs"
63
- if repo_specs.is_dir():
64
- return repo_specs
65
- return pkg_specs
58
+ return Path(__file__).resolve().parent / "specs"
66
59
 
67
60
 
68
61
  def extract_profile_override(argv: list[str]) -> str | None:
@@ -213,6 +206,9 @@ def _make_format_injector(callback: Callable[..., Any]) -> Callable[..., Any]:
213
206
  ``Error:``/``错误:`` markers and re-emits them on stderr with a non-zero
214
207
  exit code.
215
208
 
209
+ Commands carrying a ``--follow`` flag (live log tailing) bypass the
210
+ stdout capture so the plugin can stream output in real time.
211
+
216
212
  Note: ``ctx.exit(1)`` (not ``return 1``) is required — Click >= 8.4
217
213
  ignores a plain integer return value from the top-level callback and
218
214
  always exits with ``ctx.exit_code`` (default 0).
@@ -231,6 +227,9 @@ def _make_format_injector(callback: Callable[..., Any]) -> Callable[..., Any]:
231
227
  if source in (None, ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP):
232
228
  kwargs["format"] = global_format
233
229
 
230
+ if kwargs.get("follow"):
231
+ return callback(**kwargs)
232
+
234
233
  buffer = io.StringIO()
235
234
  real_stdout = sys.stdout
236
235
  sys.stdout = buffer
@@ -498,6 +497,26 @@ def _add_jcli_auth_commands(cli: click.Group) -> None:
498
497
  cli.add_command(auth)
499
498
 
500
499
 
500
+ def _add_tail_short_options(cli: click.Group) -> None:
501
+ """Give ``build log`` tail-style short flags (``-f``/``-n``).
502
+
503
+ cliyard generates long-only options (``--follow``/``--lines``); add the
504
+ conventional ``tail`` short aliases on the ``build log`` command.
505
+ """
506
+ build = cli.commands.get("build")
507
+ if not isinstance(build, click.Group):
508
+ return
509
+ log = build.commands.get("log")
510
+ if not log:
511
+ return
512
+ for p in log.params:
513
+ if isinstance(p, click.Option):
514
+ if p.name == "follow" and "-f" not in p.opts:
515
+ p.opts = ("-f", "--follow")
516
+ elif p.name == "lines" and "-n" not in p.opts:
517
+ p.opts = ("-n", "--lines")
518
+
519
+
501
520
  def _build_cli(spec_dir: Path, server: str | None, profile: str | None) -> click.Group:
502
521
  """Create the cliyard CLI and apply the jcli wrapper layer."""
503
522
  base_url = resolve_base_url(server, profile)
@@ -509,6 +528,7 @@ def _build_cli(spec_dir: Path, server: str | None, profile: str | None) -> click
509
528
  wrap_subcommand_callbacks(cli)
510
529
  set_group_help(cli)
511
530
  _prune_non_jcli_commands(cli)
531
+ _add_tail_short_options(cli)
512
532
  return cli
513
533
 
514
534
 
@@ -0,0 +1,21 @@
1
+ """Centralized CLI helpers.
2
+
3
+ v2 only needs the output-formatter helper (used by the cliyard command
4
+ plugins, e.g. ``jcli/specs/plugins/jcli_commands.py``). The v1 ``get_client``
5
+ helper was removed together with the legacy hand-written command modules
6
+ (``jcli/plugins/``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import click
12
+
13
+ from jcli.sdk.output import OutputFormatter
14
+
15
+
16
+ def get_formatter(ctx: click.Context) -> OutputFormatter:
17
+ """Return an ``OutputFormatter`` matching the ``--format`` flag."""
18
+ obj = ctx.obj or {}
19
+ fmt = obj.get("format", "table")
20
+
21
+ return OutputFormatter(format_type=fmt)
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: jcli-build
3
+ version: 1.0.0
4
+ description: |
5
+ jcli 构建管理 - 管理 Jenkins 构建的触发、查看、停止等操作。
6
+ allowed-tools:
7
+ - Bash
8
+ - Read
9
+ ---
10
+
11
+ # jcli-build
12
+
13
+ 管理 Jenkins 构建的触发、查看、停止等操作。
14
+
15
+ ## 前提条件
16
+
17
+ - Python >= 3.10
18
+ - Jenkins 2.x 或更高版本
19
+ - 已安装 jcli:`pip install -e /data/git-project/jcli`
20
+
21
+ ## 命令参考
22
+
23
+ ```bash
24
+ jcli build list <job> # 列出构建历史
25
+ jcli build list <job> --limit 20 # 列出最近 20 次构建
26
+ jcli build get <job> <number> # 查看构建详情
27
+ jcli build log <job> <number> # 查看完整控制台日志
28
+ jcli build log <job> <number> -n 50 # 只显示末尾 50 行(tail -n)
29
+ jcli build log <job> <number> -f # 实时跟随(tail -f):先打印全部历史,再滚动新增,构建结束自动退出
30
+ jcli build log <job> <number> -f -n 50 # 显示末尾 50 行后实时跟随
31
+ jcli build artifacts <job> <number> # 查看构建产物
32
+ jcli build trigger <job> # 触发构建
33
+ jcli build trigger <job> --params BRANCH=main # 带参数触发构建
34
+ jcli build trigger <job> --params KEY1=VAL1 --params KEY2=VAL2 # 多个参数
35
+ jcli build rebuild <job> <number> # 重建(复用上次构建参数)
36
+ jcli build rebuild <job> <number> --set KEY=VAL # 重建并覆盖参数(可重复)
37
+ jcli build replay <job> <number> <jenkinsfile> # 回放(使用修改后的 Jenkinsfile)
38
+ jcli build stop <job> <number> # 停止构建
39
+ jcli build queue # 查看构建队列
40
+ ```
41
+
42
+ ## 常见用例
43
+
44
+ ### 查看构建失败原因
45
+
46
+ ```bash
47
+ # 1. 查看最近构建
48
+ jcli build list my-job
49
+
50
+ # 2. 查看失败构建的日志
51
+ jcli build log my-job 42
52
+ ```
53
+
54
+ ### 批量触发构建
55
+
56
+ ```bash
57
+ for job in job1 job2 job3; do
58
+ jcli build trigger $job
59
+ done
60
+ ```
61
+
62
+ ### 监控构建状态
63
+
64
+ ```bash
65
+ # 查看队列
66
+ jcli build queue
67
+
68
+ # 实时查看最新构建状态
69
+ watch -n 5 "jcli build list my-job --limit 1"
70
+
71
+ # 实时跟随构建日志(-f,构建结束自动退出)
72
+ jcli build log my-job 42 -f
73
+ ```
74
+
75
+ ### 带参数触发构建
76
+
77
+ ```bash
78
+ # 单个参数
79
+ jcli build trigger my-job --params BRANCH=develop
80
+
81
+ # 多个参数
82
+ jcli build trigger my-job --params BRANCH=develop --params DEPLOY=true --params ENV=staging
83
+ ```
84
+
85
+ ### 实时查看构建日志(tail 风格)
86
+
87
+ `jcli build log` 沿用 `tail` 命令的参数语义:
88
+
89
+ | 命令 | 效果 |
90
+ |------|------|
91
+ | `jcli build log <job> <n>` | 输出完整日志 |
92
+ | `jcli build log <job> <n> -n 50` | 只显示末尾 50 行 |
93
+ | `jcli build log <job> <n> -f` | 实时跟随:先输出全部历史,再滚动显示新日志 |
94
+ | `jcli build log <job> <n> -f -n 50` | 先看末尾 50 行,再实时跟随 |
95
+
96
+ - `-f` / `--follow`:实时输出,新日志逐行滚动(tail -f)
97
+ - `-n N` / `--lines N`:只显示末尾 N 行,默认全部
98
+ - 跟随模式在**构建结束后自动退出**,也可随时 `Ctrl+C` 中断
99
+
100
+ ```bash
101
+ # 构建失败排查:看末尾日志定位错误
102
+ jcli build log my-job 42 -n 100
103
+
104
+ # 实时观察正在构建的日志
105
+ jcli build log my-job 42 -f
106
+
107
+ # 触发构建后直接跟随
108
+ jcli build trigger my-job
109
+ jcli build log my-job 43 -f
110
+ ```
111
+
112
+ ### 重建与回放
113
+
114
+ ```bash
115
+ # 重建:复用上次构建的参数
116
+ jcli build rebuild my-job 42
117
+
118
+ # 重建并覆盖部分参数(--set 可重复)
119
+ jcli build rebuild my-job 42 --set DEPLOY=false
120
+
121
+ # 回放:使用修改后的 Jenkinsfile 文件重新运行 Pipeline
122
+ jcli build replay my-job 42 Jenkinsfile
123
+ ```
@@ -0,0 +1,15 @@
1
+ name: jcli
2
+ version: "1.0.1"
3
+ description: Jenkins CLI tool
4
+ output:
5
+ default: table
6
+ server:
7
+ base_url: http://localhost:8090
8
+ auth:
9
+ steps:
10
+ - name: basic
11
+ type: plugin:jenkins_basic
12
+ config: {}
13
+ - name: crumb
14
+ type: plugin:jenkins_crumb
15
+ config: {}
@@ -1,4 +1,14 @@
1
- """Jcli skills management commands."""
1
+ """Top-level command plugins for jcli 2.0: skills, completion.
2
+
3
+ Migrated verbatim from jcli v1:
4
+
5
+ * ``skills`` — jcli/plugins/skills.py
6
+ * ``completion`` — jcli/cli.py ``add_completion_command``
7
+
8
+ Each group is registered as a cliyard command plugin
9
+ (``@register_command``), so ``create_cli`` attaches it to the top-level
10
+ Click group. Command semantics and output formats are unchanged from v1.
11
+ """
2
12
 
3
13
  from __future__ import annotations
4
14
 
@@ -8,14 +18,20 @@ from typing import Any
8
18
 
9
19
  import click
10
20
 
21
+ from cliyard.plugin import register_command
22
+
11
23
  from jcli.cli_helpers import get_formatter
12
24
 
13
- # ------------------------------------------------------------------
14
- # Constants
15
- # ------------------------------------------------------------------
16
25
 
17
- # Bundled skills directory (ships with jcli package)
18
- BUNDLED_SKILLS_DIR = Path(__file__).parent.parent / "skills" / "jcli"
26
+ # =====================================================================
27
+ # Skills commands (verbatim from jcli/plugins/skills.py)
28
+ # =====================================================================
29
+
30
+ # Bundled skills directory (ships with jcli package):
31
+ # <repo-root>/jcli/skills/jcli/* — this plugin lives in <repo-root>/jcli/specs/plugins/
32
+ BUNDLED_SKILLS_DIR = (
33
+ Path(__file__).resolve().parent.parent.parent.parent / "jcli" / "skills" / "jcli"
34
+ )
19
35
 
20
36
  # Default install directory for opencode
21
37
  DEFAULT_INSTALL_DIR = Path.home() / ".config" / "opencode" / "skills"
@@ -24,11 +40,6 @@ DEFAULT_INSTALL_DIR = Path.home() / ".config" / "opencode" / "skills"
24
40
  FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
25
41
 
26
42
 
27
- # ------------------------------------------------------------------
28
- # Helpers
29
- # ------------------------------------------------------------------
30
-
31
-
32
43
  def parse_skill_metadata(skill_dir: Path) -> dict[str, Any]:
33
44
  """Parse SKILL.md frontmatter to extract metadata.
34
45
 
@@ -231,27 +242,17 @@ def uninstall_skill(name: str, install_dir: Path | None = None) -> bool:
231
242
  return True
232
243
 
233
244
 
234
- # ------------------------------------------------------------------
235
- # Click group
236
- # ------------------------------------------------------------------
237
-
238
-
239
245
  @click.group("skills", help="Manage jcli skills (list, install, uninstall).")
240
246
  def skills_group() -> None:
241
247
  """Skills management commands."""
242
248
 
243
249
 
244
- # ------------------------------------------------------------------
245
- # list
246
- # ------------------------------------------------------------------
247
-
248
-
249
250
  @skills_group.command("list")
250
251
  @click.option("--installed", "-i", is_flag=True, help="Show only installed skills.")
251
252
  @click.option("--bundled", "-b", is_flag=True, help="Show only bundled skills.")
252
253
  @click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
253
254
  @click.pass_context
254
- def list_cmd(ctx: click.Context, installed: bool, bundled: bool, install_dir: str | None) -> None:
255
+ def skills_list_cmd(ctx: click.Context, installed: bool, bundled: bool, install_dir: str | None) -> None:
255
256
  """List all available skills."""
256
257
  fmt = get_formatter(ctx)
257
258
  target_dir = Path(install_dir) if install_dir else DEFAULT_INSTALL_DIR
@@ -297,18 +298,13 @@ def list_cmd(ctx: click.Context, installed: bool, bundled: bool, install_dir: st
297
298
  fmt.print_table(headers, rows, title="Jcli Skills")
298
299
 
299
300
 
300
- # ------------------------------------------------------------------
301
- # install
302
- # ------------------------------------------------------------------
303
-
304
-
305
301
  @skills_group.command("install")
306
302
  @click.argument("name", required=False)
307
303
  @click.option("--all", "-a", "install_all", is_flag=True, help="Install all bundled skills.")
308
304
  @click.option("--force", "-f", is_flag=True, help="Force install (overwrite existing).")
309
305
  @click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
310
306
  @click.pass_context
311
- def install_cmd(ctx: click.Context, name: str | None, install_all: bool, force: bool, install_dir: str | None) -> None:
307
+ def skills_install_cmd(ctx: click.Context, name: str | None, install_all: bool, force: bool, install_dir: str | None) -> None:
312
308
  """Install a skill.
313
309
 
314
310
  NAME is the skill name to install (from bundled skills).
@@ -354,16 +350,11 @@ def install_cmd(ctx: click.Context, name: str | None, install_all: bool, force:
354
350
  fmt.print_success(f"Skill '{name}' installed to {target_dir / name}")
355
351
 
356
352
 
357
- # ------------------------------------------------------------------
358
- # uninstall
359
- # ------------------------------------------------------------------
360
-
361
-
362
353
  @skills_group.command("uninstall")
363
354
  @click.argument("name")
364
355
  @click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
365
356
  @click.pass_context
366
- def uninstall_cmd(ctx: click.Context, name: str, install_dir: str | None) -> None:
357
+ def skills_uninstall_cmd(ctx: click.Context, name: str, install_dir: str | None) -> None:
367
358
  """Uninstall a skill.
368
359
 
369
360
  NAME is the skill name to uninstall.
@@ -382,17 +373,12 @@ def uninstall_cmd(ctx: click.Context, name: str, install_dir: str | None) -> Non
382
373
  fmt.print_success(f"Skill '{name}' uninstalled from {target_dir}")
383
374
 
384
375
 
385
- # ------------------------------------------------------------------
386
- # get
387
- # ------------------------------------------------------------------
388
-
389
-
390
376
  @skills_group.command("get")
391
377
  @click.argument("name")
392
378
  @click.option("--installed", "-i", is_flag=True, help="Get from installed skills.")
393
379
  @click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
394
380
  @click.pass_context
395
- def get_cmd(ctx: click.Context, name: str, installed: bool, install_dir: str | None) -> None:
381
+ def skills_get_cmd(ctx: click.Context, name: str, installed: bool, install_dir: str | None) -> None:
396
382
  """Show skill details and SKILL.md content."""
397
383
  fmt = get_formatter(ctx)
398
384
  target_dir = Path(install_dir) if install_dir else DEFAULT_INSTALL_DIR
@@ -432,11 +418,43 @@ def get_cmd(ctx: click.Context, name: str, installed: bool, install_dir: str | N
432
418
  fmt.console.print(content)
433
419
 
434
420
 
435
- # ------------------------------------------------------------------
436
- # Registration
437
- # ------------------------------------------------------------------
421
+ # =====================================================================
422
+ # Completion command (verbatim from jcli/cli.py add_completion_command)
423
+ # =====================================================================
424
+
425
+
426
+ def build_completion_group(cli: click.Group) -> click.Group:
427
+ """Build the ``completion show`` group attached to *cli*."""
428
+ from click.shell_completion import BashComplete, FishComplete, ZshComplete
429
+
430
+ @click.group()
431
+ def completion() -> None:
432
+ """Shell completion support for jcli."""
433
+
434
+ @completion.command()
435
+ @click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]))
436
+ def show(shell: str) -> None:
437
+ """Output shell completion script for the specified shell."""
438
+ shell_cls = {"bash": BashComplete, "zsh": ZshComplete, "fish": FishComplete}
439
+ complete = shell_cls[shell](cli, {}, "jcli", "_JCLI_COMPLETE")
440
+ click.echo(complete.source(), nl=False)
441
+
442
+ cli.add_command(completion)
443
+ return completion
444
+
445
+
446
+ # =====================================================================
447
+ # cliyard command plugin registration
448
+ # =====================================================================
449
+
450
+
451
+ @register_command("skills")
452
+ def _register_skills(cli: click.Group, ctx: Any) -> None:
453
+ """Attach the skills command group to the top-level CLI."""
454
+ cli.add_command(skills_group)
438
455
 
439
456
 
440
- def register(parent_group: click.Group) -> None:
441
- """Register the skills subgroup under the parent Click group."""
442
- parent_group.add_command(skills_group)
457
+ @register_command("completion")
458
+ def _register_completion(cli: click.Group, ctx: Any) -> None:
459
+ """Attach the completion command group to the top-level CLI."""
460
+ build_completion_group(cli)
@@ -0,0 +1,75 @@
1
+ """Jenkins auth steps: Basic Auth + Crumb (CSRF)."""
2
+ import base64
3
+ import os
4
+
5
+ import requests
6
+
7
+ from cliyard.plugin import register_auth_step
8
+
9
+
10
+ def _config_path() -> str:
11
+ """Resolve the jcli config file path at call time.
12
+
13
+ Prefers ``jcli.sdk.config.DEFAULT_CONFIG_FILE`` (so tests can point it
14
+ elsewhere via monkeypatch) and falls back to ``~/.jcli/config.yaml``
15
+ when the SDK is not importable.
16
+ """
17
+ try:
18
+ from jcli.sdk.config import DEFAULT_CONFIG_FILE
19
+
20
+ return str(DEFAULT_CONFIG_FILE)
21
+ except Exception:
22
+ return os.path.expanduser("~/.jcli/config.yaml")
23
+
24
+
25
+ def _load_profile():
26
+ import yaml
27
+ config_path = _config_path()
28
+ if not os.path.exists(config_path):
29
+ raise RuntimeError(f"config not found: {config_path}")
30
+ with open(config_path, "r", encoding="utf-8") as f:
31
+ cfg = yaml.safe_load(f) or {}
32
+ name = os.environ.get("JCLI_PROFILE") or cfg.get("active_profile", "default")
33
+ profile = dict(cfg.get("profiles", {}).get(name, {}))
34
+ for env_key, field in (("JCLI_URL", "url"), ("JCLI_USERNAME", "username"), ("JCLI_API_TOKEN", "api_token")):
35
+ if os.environ.get(env_key):
36
+ profile[field] = os.environ[env_key]
37
+ return profile
38
+
39
+
40
+ @register_auth_step("jenkins_basic")
41
+ class JenkinsBasicAuth:
42
+ """Set Basic Auth header from ~/.jcli/config.yaml profile."""
43
+
44
+ def execute(self, auth_state, config, http_client):
45
+ profile = _load_profile()
46
+ username = profile.get("username", "")
47
+ token = profile.get("api_token", "")
48
+ if not username or not token:
49
+ raise RuntimeError("missing username/api_token in profile")
50
+ raw = f"{username}:{token}".encode()
51
+ http_client.default_headers["Authorization"] = "Basic " + base64.b64encode(raw).decode()
52
+ auth_state["username"] = username
53
+ return {"username": username}
54
+
55
+
56
+ @register_auth_step("jenkins_crumb")
57
+ class JenkinsCrumb:
58
+ """Fetch Jenkins crumb and inject as header. Tolerate 404 (CSRF disabled)."""
59
+
60
+ def execute(self, auth_state, config, http_client):
61
+ url = f"{http_client.base_url}/crumbIssuer/api/json"
62
+ try:
63
+ resp = http_client._session.get(url, headers=http_client.default_headers, timeout=10)
64
+ except requests.RequestException:
65
+ return {}
66
+ if resp.status_code == 404:
67
+ return {} # CSRF disabled
68
+ if resp.status_code != 200:
69
+ raise RuntimeError(f"crumb fetch failed: {resp.status_code}")
70
+ data = resp.json()
71
+ field = data.get("crumbRequestField")
72
+ value = data.get("crumb")
73
+ if field and value:
74
+ http_client.default_headers[field] = value
75
+ return {"crumb": value}