python-jcli 0.1.0__py3-none-any.whl → 1.0.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 -1
- jcli/cli.py +546 -52
- jcli/plugins/skills.py +60 -17
- jcli/skills/jcli/SKILL.md +15 -2
- jcli/skills/jcli/auth/SKILL.md +86 -0
- jcli/skills/jcli/credential/SKILL.md +14 -5
- jcli/skills/jcli/job/SKILL.md +20 -4
- jcli/skills/jcli/node/SKILL.md +4 -2
- jcli/skills/jcli/pipeline/SKILL.md +3 -3
- jcli/skills/jcli/plugin/SKILL.md +3 -0
- jcli/skills/jcli/system/SKILL.md +15 -2
- jcli/skills/jcli/view/SKILL.md +4 -3
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.0.dist-info}/METADATA +56 -12
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.0.dist-info}/RECORD +17 -17
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.0.dist-info}/WHEEL +1 -1
- jcli/skills/jcli/config/SKILL.md +0 -81
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.0.dist-info}/entry_points.txt +0 -0
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.0.dist-info}/top_level.txt +0 -0
jcli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "1.0.0"
|
jcli/cli.py
CHANGED
|
@@ -1,78 +1,572 @@
|
|
|
1
|
-
"""Jenkins CLI - Command line interface for managing Jenkins.
|
|
1
|
+
"""Jenkins CLI - Command line interface for managing Jenkins.
|
|
2
|
+
|
|
3
|
+
jcli 2.0 wrapper: the Click CLI is generated from the cliyard YAML specs
|
|
4
|
+
(``specs/``) and this module layers the global options on top:
|
|
5
|
+
|
|
6
|
+
* ``-f/--format`` — table / json / yaml / csv (default ``table``)
|
|
7
|
+
* ``-p/--profile`` — pick a profile from ``~/.jcli/config.yaml``
|
|
8
|
+
* ``-d/--debug`` — enable DEBUG logging on stderr
|
|
9
|
+
* ``-s/--server`` — handled by cliyard's runner pre-extraction
|
|
10
|
+
(:func:`cliyard.runtime.runner.extract_server_override`); it is *not*
|
|
11
|
+
redefined here to avoid conflicting with cliyard.
|
|
12
|
+
|
|
13
|
+
The global ``-f`` is injected into each subcommand callback only when the
|
|
14
|
+
subcommand did not explicitly set its own ``--format`` on the command line
|
|
15
|
+
(cliyard's ``_make_callback`` pops ``format`` from kwargs, so a plain default
|
|
16
|
+
would shadow the global override).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
2
20
|
|
|
3
21
|
import logging
|
|
22
|
+
import os
|
|
4
23
|
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Callable
|
|
5
26
|
|
|
6
27
|
import click
|
|
28
|
+
from click.core import ParameterSource
|
|
29
|
+
|
|
30
|
+
from cliyard.runtime import create_cli
|
|
31
|
+
from cliyard.runtime.runner import extract_server_override
|
|
7
32
|
|
|
8
33
|
from jcli import __version__
|
|
9
|
-
from jcli.plugins import register_commands
|
|
10
34
|
|
|
11
35
|
logger = logging.getLogger("jcli")
|
|
12
36
|
|
|
37
|
+
#: Formats accepted by the global ``-f/--format`` option (superset of
|
|
38
|
+
#: cliyard's built-ins, adding ``yaml``).
|
|
39
|
+
GLOBAL_FORMATS = ("table", "json", "yaml", "csv")
|
|
13
40
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
#: Env var to override the cliyard spec directory (mainly for tests/dev).
|
|
42
|
+
SPEC_DIR_ENV = "JCLI_SPEC_DIR"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Spec directory & base_url resolution
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _default_spec_dir() -> Path:
|
|
51
|
+
"""Locate the cliyard YAML spec directory.
|
|
52
|
+
|
|
53
|
+
Priority: ``$JCLI_SPEC_DIR`` > package-local ``specs/`` > repo-root
|
|
54
|
+
``specs/`` (editable/dev layout).
|
|
55
|
+
"""
|
|
56
|
+
env = os.environ.get(SPEC_DIR_ENV)
|
|
57
|
+
if env:
|
|
58
|
+
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
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_profile_override(argv: list[str]) -> str | None:
|
|
69
|
+
"""Read a ``--profile``/``-p`` value from *argv* without removing it.
|
|
70
|
+
|
|
71
|
+
The top-level ``-p/--profile`` Click option parses the flag normally; this
|
|
72
|
+
only previews the value so the base_url can be resolved before the CLI tree
|
|
73
|
+
is built. ``--profile X``, ``--profile=X``, ``-p X`` and ``-p=X`` forms
|
|
74
|
+
are supported.
|
|
75
|
+
"""
|
|
76
|
+
profile: str | None = None
|
|
77
|
+
i = 0
|
|
78
|
+
while i < len(argv):
|
|
79
|
+
arg = argv[i]
|
|
80
|
+
if arg in ("--profile", "-p") and i + 1 < len(argv) and not argv[i + 1].startswith("-"):
|
|
81
|
+
profile = argv[i + 1]
|
|
82
|
+
i += 2
|
|
83
|
+
continue
|
|
84
|
+
if arg.startswith("--profile="):
|
|
85
|
+
profile = arg.split("=", 1)[1]
|
|
86
|
+
i += 1
|
|
87
|
+
continue
|
|
88
|
+
if arg.startswith("-p=") and len(arg) > 3:
|
|
89
|
+
profile = arg[3:]
|
|
90
|
+
i += 1
|
|
91
|
+
continue
|
|
92
|
+
i += 1
|
|
93
|
+
return profile
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _read_profile_url(profile_name: str | None) -> str | None:
|
|
97
|
+
"""Read a profile URL from ``~/.jcli/config.yaml`` (read-only, no creation)."""
|
|
98
|
+
import yaml as _yaml
|
|
99
|
+
|
|
100
|
+
from jcli.sdk.config import DEFAULT_CONFIG_FILE, ENV_PROFILE
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
if not DEFAULT_CONFIG_FILE.exists():
|
|
104
|
+
return None
|
|
105
|
+
cfg = _yaml.safe_load(DEFAULT_CONFIG_FILE.read_text(encoding="utf-8"))
|
|
106
|
+
except Exception:
|
|
107
|
+
return None
|
|
108
|
+
if not isinstance(cfg, dict):
|
|
109
|
+
return None
|
|
110
|
+
name = profile_name or os.environ.get(ENV_PROFILE) or cfg.get("active_profile") or "default"
|
|
111
|
+
profiles = cfg.get("profiles")
|
|
112
|
+
if not isinstance(profiles, dict):
|
|
113
|
+
return None
|
|
114
|
+
profile = profiles.get(name)
|
|
115
|
+
if not isinstance(profile, dict):
|
|
116
|
+
return None
|
|
117
|
+
url = profile.get("url")
|
|
118
|
+
return url if isinstance(url, str) and url.strip() else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def resolve_base_url(server_override: str | None, profile_name: str | None) -> str | None:
|
|
122
|
+
"""Resolve the base_url handed to ``create_cli(base_url_override=...)``.
|
|
123
|
+
|
|
124
|
+
Precedence: ``-s/--server`` > ``JCLI_URL`` env > profile ``url`` from
|
|
125
|
+
``~/.jcli/config.yaml``. ``None`` lets cliyard fall back to the spec's
|
|
126
|
+
``server.base_url``.
|
|
127
|
+
"""
|
|
128
|
+
if server_override:
|
|
129
|
+
return server_override
|
|
130
|
+
env_url = os.environ.get("JCLI_URL")
|
|
131
|
+
if env_url:
|
|
132
|
+
return env_url
|
|
133
|
+
return _read_profile_url(profile_name)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Global options & format injection
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def add_global_options(cli: click.Group) -> None:
|
|
142
|
+
"""Attach global options (``-f/--format``, ``-p/--profile``, ``-d/--debug``).
|
|
143
|
+
|
|
144
|
+
Values are stored in ``ctx.obj`` so wrapped subcommand callbacks can read
|
|
145
|
+
them. ``-s/--server`` is intentionally *not* added here — cliyard's
|
|
146
|
+
``create_cli`` already registers it on the top-level group (with a root
|
|
147
|
+
callback that stores the override into ``ctx.obj["server"]``); that
|
|
148
|
+
behavior is preserved by the combined callback below.
|
|
149
|
+
|
|
150
|
+
Note: ``Group.callback`` is a plain attribute in Click 8.4 (the
|
|
151
|
+
``@cli.callback`` decorator form is gone), so the callback and its options
|
|
152
|
+
are assigned explicitly.
|
|
153
|
+
"""
|
|
154
|
+
cli.params.extend(
|
|
155
|
+
[
|
|
156
|
+
click.Option(
|
|
157
|
+
["-f", "--format", "output_format"],
|
|
158
|
+
default="table",
|
|
159
|
+
type=click.Choice(GLOBAL_FORMATS),
|
|
160
|
+
help="Output format.",
|
|
161
|
+
show_default=True,
|
|
162
|
+
),
|
|
163
|
+
click.Option(["-p", "--profile"], default=None, help="Configuration profile name."),
|
|
164
|
+
click.Option(["-d", "--debug"], is_flag=True, default=False, help="Enable debug output."),
|
|
165
|
+
]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@click.pass_context
|
|
169
|
+
def _global_options(
|
|
170
|
+
ctx: click.Context,
|
|
171
|
+
output_format: str,
|
|
172
|
+
profile: str | None,
|
|
173
|
+
debug: bool,
|
|
174
|
+
**extra: Any,
|
|
175
|
+
) -> None:
|
|
176
|
+
ctx.ensure_object(dict)
|
|
177
|
+
|
|
178
|
+
# Preserve cliyard's root-callback behavior: the native --server/-s
|
|
179
|
+
# option (registered by create_cli) is stored for subcommands.
|
|
180
|
+
server = extra.get("server")
|
|
181
|
+
if server:
|
|
182
|
+
ctx.obj["server"] = server
|
|
183
|
+
|
|
184
|
+
ctx.obj["format"] = output_format
|
|
185
|
+
ctx.obj["profile"] = profile
|
|
186
|
+
ctx.obj["debug"] = debug
|
|
187
|
+
|
|
188
|
+
if debug:
|
|
189
|
+
logging.basicConfig(
|
|
190
|
+
level=logging.DEBUG,
|
|
191
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
192
|
+
stream=sys.stderr,
|
|
193
|
+
force=True,
|
|
194
|
+
)
|
|
195
|
+
logger.debug("Debug logging enabled")
|
|
196
|
+
else:
|
|
197
|
+
logging.basicConfig(level=logging.WARNING, stream=sys.stderr, force=True)
|
|
198
|
+
|
|
199
|
+
cli.callback = _global_options
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _make_format_injector(callback: Callable[..., Any]) -> Callable[..., Any]:
|
|
203
|
+
"""Wrap a subcommand callback so the global ``-f/--format`` is applied.
|
|
204
|
+
|
|
205
|
+
cliyard's ``_make_callback`` pops ``format`` from kwargs and falls back to
|
|
206
|
+
the method's own default — a default therefore shadows a global override.
|
|
207
|
+
We inspect the parameter source and only inject the global format when the
|
|
208
|
+
subcommand did not explicitly pass ``--format`` on the command line.
|
|
209
|
+
|
|
210
|
+
cliyard's callbacks also swallow every exception and print the message to
|
|
211
|
+
the console without re-raising, so connection failures / API errors would
|
|
212
|
+
otherwise exit 0. The wrapped callback captures stdout, recognises the
|
|
213
|
+
``Error:``/``错误:`` markers and re-emits them on stderr with a non-zero
|
|
214
|
+
exit code.
|
|
215
|
+
|
|
216
|
+
Note: ``ctx.exit(1)`` (not ``return 1``) is required — Click >= 8.4
|
|
217
|
+
ignores a plain integer return value from the top-level callback and
|
|
218
|
+
always exits with ``ctx.exit_code`` (default 0).
|
|
219
|
+
"""
|
|
220
|
+
import io
|
|
221
|
+
|
|
222
|
+
def wrapped(**kwargs: Any) -> Any:
|
|
223
|
+
ctx = click.get_current_context()
|
|
224
|
+
root = ctx.find_root()
|
|
225
|
+
global_format = (root.obj or {}).get("format")
|
|
226
|
+
if global_format:
|
|
227
|
+
try:
|
|
228
|
+
source = ctx.get_parameter_source("format")
|
|
229
|
+
except Exception:
|
|
230
|
+
source = ParameterSource.DEFAULT
|
|
231
|
+
if source in (None, ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP):
|
|
232
|
+
kwargs["format"] = global_format
|
|
233
|
+
|
|
234
|
+
buffer = io.StringIO()
|
|
235
|
+
real_stdout = sys.stdout
|
|
236
|
+
sys.stdout = buffer
|
|
237
|
+
try:
|
|
238
|
+
result = callback(**kwargs)
|
|
239
|
+
finally:
|
|
240
|
+
sys.stdout = real_stdout
|
|
241
|
+
|
|
242
|
+
output = buffer.getvalue()
|
|
243
|
+
if output.startswith(("错误:", "Error:")):
|
|
244
|
+
click.echo(output, err=True, nl=False)
|
|
245
|
+
ctx.exit(1)
|
|
246
|
+
if output:
|
|
247
|
+
click.echo(output, nl=False)
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
wrapped.__name__ = getattr(callback, "__name__", "callback")
|
|
251
|
+
wrapped.__doc__ = getattr(callback, "__doc__", None)
|
|
252
|
+
return wrapped
|
|
47
253
|
|
|
48
254
|
|
|
49
|
-
|
|
50
|
-
|
|
255
|
+
def wrap_subcommand_callbacks(command: click.Command) -> None:
|
|
256
|
+
"""Recursively wrap leaf-command callbacks to inherit the global format."""
|
|
257
|
+
if isinstance(command, click.Group):
|
|
258
|
+
for sub in command.commands.values():
|
|
259
|
+
wrap_subcommand_callbacks(sub)
|
|
260
|
+
return
|
|
261
|
+
if any(getattr(p, "name", None) == "format" for p in command.params):
|
|
262
|
+
command.callback = _make_format_injector(command.callback)
|
|
51
263
|
|
|
52
264
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"""Shell completion support for jcli."""
|
|
56
|
-
pass
|
|
265
|
+
def add_completion_command(cli: click.Group) -> None:
|
|
266
|
+
"""Attach the ``completion show`` command (kept from jcli v1)."""
|
|
57
267
|
|
|
268
|
+
@click.group()
|
|
269
|
+
def completion() -> None:
|
|
270
|
+
"""Shell completion support for jcli."""
|
|
58
271
|
|
|
59
|
-
@completion.command()
|
|
60
|
-
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]))
|
|
61
|
-
def show(shell):
|
|
62
|
-
|
|
63
|
-
|
|
272
|
+
@completion.command()
|
|
273
|
+
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]))
|
|
274
|
+
def show(shell: str) -> None:
|
|
275
|
+
"""Output shell completion script for the specified shell."""
|
|
276
|
+
from click.shell_completion import BashComplete, FishComplete, ZshComplete
|
|
64
277
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
278
|
+
shell_cls = {"bash": BashComplete, "zsh": ZshComplete, "fish": FishComplete}
|
|
279
|
+
complete = shell_cls[shell](cli, {}, "jcli", "_JCLI_COMPLETE")
|
|
280
|
+
click.echo(complete.source(), nl=False)
|
|
68
281
|
|
|
282
|
+
cli.add_command(completion)
|
|
69
283
|
|
|
70
|
-
cli.add_command(completion)
|
|
71
284
|
|
|
285
|
+
def set_group_help(cli: click.Group) -> None:
|
|
286
|
+
"""Propagate each command's ``short_help`` to its ``help`` text.
|
|
72
287
|
|
|
73
|
-
|
|
288
|
+
cliyard only sets ``short_help`` (from the YAML ``description``), so
|
|
289
|
+
``jcli job --help`` / ``jcli plugin list --help`` would otherwise render
|
|
290
|
+
without the description line. jcli v1 printed it, so keep that
|
|
291
|
+
behaviour: the top-level help lists commands via ``short_help``
|
|
292
|
+
(unchanged), while per-command ``--help`` pages show the description.
|
|
293
|
+
"""
|
|
294
|
+
stack = list(cli.commands.values())
|
|
295
|
+
while stack:
|
|
296
|
+
command = stack.pop()
|
|
297
|
+
if isinstance(command, click.Group):
|
|
298
|
+
stack.extend(command.commands.values())
|
|
299
|
+
if command.short_help and not command.help:
|
|
300
|
+
command.help = command.short_help
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# CLI construction & entry point
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
# Commands belonging to jcli. cliyard auto-registers ``auth`` (built-in)
|
|
308
|
+
# and any global plugins found in ~/.cliyard/plugins/ (other projects), so
|
|
309
|
+
# only whitelisted top-level commands are kept on the jcli command surface.
|
|
310
|
+
_ALLOWED_TOP_LEVEL_COMMANDS = {
|
|
311
|
+
"job", "build", "node", "plugin", "credential", "pipeline", "view",
|
|
312
|
+
"system", "skills", "completion", "auth",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _prune_non_jcli_commands(cli: click.Group) -> None:
|
|
317
|
+
"""Drop top-level commands that are not part of jcli."""
|
|
318
|
+
for name in list(cli.commands.keys()):
|
|
319
|
+
if name not in _ALLOWED_TOP_LEVEL_COMMANDS:
|
|
320
|
+
cli.commands.pop(name)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _mask_token(token: str) -> str:
|
|
324
|
+
"""Mask an API token for display (``abcd****wxyz``)."""
|
|
325
|
+
if len(token) > 8:
|
|
326
|
+
return f"{token[:4]}****{token[-4:]}"
|
|
327
|
+
return "****" if token else "-"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _add_jcli_auth_commands(cli: click.Group) -> None:
|
|
331
|
+
"""Register the jcli-native ``auth`` command group.
|
|
332
|
+
|
|
333
|
+
Replaces cliyard's built-in ``auth``, which writes ``~/.cliyard/
|
|
334
|
+
credentials.yaml`` and is unrelated to jcli's actual authentication
|
|
335
|
+
source. This group reads/writes ``~/.jcli/config.yaml`` through the
|
|
336
|
+
same :class:`jcli.sdk.config.Config` class as ``jcli config``, so the
|
|
337
|
+
two command families share a single configuration source.
|
|
338
|
+
"""
|
|
339
|
+
import copy
|
|
340
|
+
|
|
341
|
+
from jcli.sdk.config import (
|
|
342
|
+
DEFAULT_CONFIG_TEMPLATE,
|
|
343
|
+
DEFAULT_PROFILE_NAME,
|
|
344
|
+
ProfileNotFoundError,
|
|
345
|
+
Config,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@click.group()
|
|
349
|
+
def auth() -> None:
|
|
350
|
+
"""Manage Jenkins authentication profiles."""
|
|
351
|
+
|
|
352
|
+
def _get_config() -> Config:
|
|
353
|
+
return Config().load()
|
|
354
|
+
|
|
355
|
+
@auth.command("add")
|
|
356
|
+
@click.option("-n", "--name", default=DEFAULT_PROFILE_NAME, help="Profile name.")
|
|
357
|
+
@click.option("-u", "--username", required=True, help="Jenkins username.")
|
|
358
|
+
@click.option(
|
|
359
|
+
"-p",
|
|
360
|
+
"--password",
|
|
361
|
+
"api_token",
|
|
362
|
+
required=True,
|
|
363
|
+
help="Jenkins API token (Jenkins Basic Auth = username:token).",
|
|
364
|
+
)
|
|
365
|
+
@click.option("-e", "--endpoint", "url", required=True, help="Jenkins server URL.")
|
|
366
|
+
@click.option("--default", "set_default", is_flag=True, help="Set as the active profile.")
|
|
367
|
+
def auth_add(name: str, username: str, api_token: str, url: str, set_default: bool) -> None:
|
|
368
|
+
"""Add (or update) an authentication profile in ~/.jcli/config.yaml."""
|
|
369
|
+
cfg = _get_config()
|
|
370
|
+
has_custom = any(p != DEFAULT_PROFILE_NAME for p in cfg.list_profiles())
|
|
371
|
+
cfg.add_profile(name=name, url=url, username=username, api_token=api_token)
|
|
372
|
+
if set_default or not has_custom:
|
|
373
|
+
cfg.set_active_profile(name)
|
|
374
|
+
click.echo(f"Profile '{name}' added ({'active' if cfg.get_active_profile_name() == name else 'inactive'}).")
|
|
375
|
+
|
|
376
|
+
@auth.command("status")
|
|
377
|
+
@click.pass_context
|
|
378
|
+
def auth_status(ctx: click.Context) -> None:
|
|
379
|
+
"""List configured authentication profiles (tokens masked)."""
|
|
380
|
+
from jcli.sdk.output.formatter import get_formatter
|
|
381
|
+
|
|
382
|
+
fmt = get_formatter(ctx.find_root())
|
|
383
|
+
cfg = _get_config()
|
|
384
|
+
profiles = cfg.list_profiles()
|
|
385
|
+
active_name = cfg.get_active_profile_name()
|
|
386
|
+
|
|
387
|
+
if not profiles:
|
|
388
|
+
fmt.print_info("No profiles configured. Run 'jcli auth add' to create one.")
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
headers = ["Name", "URL", "Username", "Token", "Active"]
|
|
392
|
+
rows = []
|
|
393
|
+
for name, data in profiles.items():
|
|
394
|
+
rows.append(
|
|
395
|
+
[
|
|
396
|
+
name,
|
|
397
|
+
data.get("url", ""),
|
|
398
|
+
data.get("username", ""),
|
|
399
|
+
_mask_token(data.get("api_token", "")),
|
|
400
|
+
"✓" if name == active_name else "",
|
|
401
|
+
]
|
|
402
|
+
)
|
|
403
|
+
fmt.print_table(headers, rows, title="Profiles")
|
|
404
|
+
|
|
405
|
+
def _auth_use(profile: str) -> None:
|
|
406
|
+
cfg = _get_config()
|
|
407
|
+
try:
|
|
408
|
+
cfg.set_active_profile(profile)
|
|
409
|
+
except ProfileNotFoundError:
|
|
410
|
+
click.echo(f"Error: Profile '{profile}' not found", err=True)
|
|
411
|
+
available = ", ".join(cfg.list_profiles())
|
|
412
|
+
if available:
|
|
413
|
+
click.echo(f"Available profiles: {available}", err=True)
|
|
414
|
+
raise click.exceptions.Exit(1)
|
|
415
|
+
click.echo(f"Active profile set to '{profile}'")
|
|
416
|
+
|
|
417
|
+
@auth.command("use")
|
|
418
|
+
@click.argument("profile")
|
|
419
|
+
def auth_use(profile: str) -> None:
|
|
420
|
+
"""Switch the active authentication profile."""
|
|
421
|
+
_auth_use(profile)
|
|
422
|
+
|
|
423
|
+
@auth.command("switch", hidden=True)
|
|
424
|
+
@click.argument("profile")
|
|
425
|
+
def auth_switch(profile: str) -> None:
|
|
426
|
+
"""Alias of ``auth use``."""
|
|
427
|
+
_auth_use(profile)
|
|
428
|
+
|
|
429
|
+
@auth.command("rm")
|
|
430
|
+
@click.argument("name", required=False)
|
|
431
|
+
@click.option("--all", "clear_all", is_flag=True, help="Remove all profiles and reset to the default template.")
|
|
432
|
+
def auth_rm(name: str | None, clear_all: bool) -> None:
|
|
433
|
+
"""Remove an authentication profile (or all with --all)."""
|
|
434
|
+
cfg = _get_config()
|
|
435
|
+
if name:
|
|
436
|
+
try:
|
|
437
|
+
cfg.remove_profile(name)
|
|
438
|
+
except ProfileNotFoundError:
|
|
439
|
+
click.echo(f"Error: Profile '{name}' not found", err=True)
|
|
440
|
+
raise click.exceptions.Exit(1)
|
|
441
|
+
click.echo(f"Profile '{name}' removed")
|
|
442
|
+
elif clear_all:
|
|
443
|
+
cfg._data = copy.deepcopy(DEFAULT_CONFIG_TEMPLATE)
|
|
444
|
+
cfg.save()
|
|
445
|
+
click.echo("All profiles removed, config reset to the default template.")
|
|
446
|
+
else:
|
|
447
|
+
active = cfg.get_active_profile_name()
|
|
448
|
+
click.echo(f"Active profile: {active} (use 'jcli auth rm NAME' or 'jcli auth rm --all')")
|
|
449
|
+
|
|
450
|
+
@auth.command("set")
|
|
451
|
+
@click.argument("name")
|
|
452
|
+
@click.argument("field", type=click.Choice(["url", "username", "api_token", "description"]))
|
|
453
|
+
@click.argument("value")
|
|
454
|
+
def auth_set(name: str, field: str, value: str) -> None:
|
|
455
|
+
"""Set a configuration field (url/username/api_token/description) for a profile."""
|
|
456
|
+
cfg = _get_config()
|
|
457
|
+
try:
|
|
458
|
+
data = cfg.get_profile(name)
|
|
459
|
+
except ProfileNotFoundError:
|
|
460
|
+
click.echo(f"Error: Profile '{name}' not found", err=True)
|
|
461
|
+
raise click.exceptions.Exit(1)
|
|
462
|
+
data[field] = value
|
|
463
|
+
cfg.add_profile(
|
|
464
|
+
name=name,
|
|
465
|
+
url=data.get("url", ""),
|
|
466
|
+
username=data.get("username", ""),
|
|
467
|
+
api_token=data.get("api_token", ""),
|
|
468
|
+
description=data.get("description", ""),
|
|
469
|
+
)
|
|
470
|
+
click.echo(f"Updated profile '{name}': {field} = {value}")
|
|
471
|
+
|
|
472
|
+
@auth.command("show")
|
|
473
|
+
@click.argument("name", required=False)
|
|
474
|
+
def auth_show(name: str | None) -> None:
|
|
475
|
+
"""Show profile details (default: active profile, token masked)."""
|
|
476
|
+
cfg = _get_config()
|
|
477
|
+
if name is None:
|
|
478
|
+
name = cfg.get_active_profile_name()
|
|
479
|
+
try:
|
|
480
|
+
data = cfg.get_profile(name)
|
|
481
|
+
except ProfileNotFoundError:
|
|
482
|
+
click.echo(f"Error: Profile '{name}' not found", err=True)
|
|
483
|
+
raise click.exceptions.Exit(1)
|
|
484
|
+
active_name = cfg.get_active_profile_name()
|
|
485
|
+
is_active = " (active)" if name == active_name else ""
|
|
486
|
+
click.echo(f"Profile: {name}{is_active}")
|
|
487
|
+
fields = [
|
|
488
|
+
("url", "URL"),
|
|
489
|
+
("username", "Username"),
|
|
490
|
+
("api_token", "API Token"),
|
|
491
|
+
("description", "Description"),
|
|
492
|
+
]
|
|
493
|
+
for key, label in fields:
|
|
494
|
+
value = data.get(key, "")
|
|
495
|
+
display = _mask_token(value) if key == "api_token" else value
|
|
496
|
+
click.echo(f" {label}: {display}")
|
|
497
|
+
|
|
498
|
+
cli.add_command(auth)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _build_cli(spec_dir: Path, server: str | None, profile: str | None) -> click.Group:
|
|
502
|
+
"""Create the cliyard CLI and apply the jcli wrapper layer."""
|
|
503
|
+
base_url = resolve_base_url(server, profile)
|
|
504
|
+
cli = create_cli(str(spec_dir), version=__version__, base_url_override=base_url)
|
|
505
|
+
cli.commands.pop("auth", None)
|
|
506
|
+
_add_jcli_auth_commands(cli)
|
|
507
|
+
add_global_options(cli)
|
|
508
|
+
add_completion_command(cli)
|
|
509
|
+
wrap_subcommand_callbacks(cli)
|
|
510
|
+
set_group_help(cli)
|
|
511
|
+
_prune_non_jcli_commands(cli)
|
|
512
|
+
return cli
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def create_jcli_cli(argv: list[str] | None = None) -> click.Group:
|
|
516
|
+
"""Build the jcli Click CLI from the YAML specs (wrapper included).
|
|
517
|
+
|
|
518
|
+
With *argv* left ``None`` the real ``sys.argv`` is used and
|
|
519
|
+
``-s/--server`` is stripped from it (cliyard's runner pre-extraction).
|
|
520
|
+
Tests pass an explicit argv to avoid touching ``sys.argv``.
|
|
521
|
+
"""
|
|
522
|
+
if argv is None:
|
|
523
|
+
argv = sys.argv[1:]
|
|
524
|
+
cleaned, server = extract_server_override(argv)
|
|
525
|
+
if cleaned != argv:
|
|
526
|
+
sys.argv = [sys.argv[0]] + cleaned
|
|
527
|
+
profile = extract_profile_override(cleaned)
|
|
528
|
+
else:
|
|
529
|
+
cleaned, server = extract_server_override(list(argv))
|
|
530
|
+
profile = extract_profile_override(argv)
|
|
531
|
+
return _build_cli(_default_spec_dir(), server, profile)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def main() -> None:
|
|
74
535
|
"""CLI entry point."""
|
|
75
|
-
|
|
536
|
+
try:
|
|
537
|
+
cli = create_jcli_cli()
|
|
538
|
+
except Exception as exc:
|
|
539
|
+
click.echo(f"Error: {exc}", err=True)
|
|
540
|
+
sys.exit(1)
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
code = cli(standalone_mode=False)
|
|
544
|
+
sys.exit(code if code is not None else 0)
|
|
545
|
+
except SystemExit as e:
|
|
546
|
+
sys.exit(int(e.code) if e.code is not None else 0)
|
|
547
|
+
except click.exceptions.ClickException as exc:
|
|
548
|
+
click.echo(exc.format_message(), err=True)
|
|
549
|
+
sys.exit(exc.exit_code)
|
|
550
|
+
except Exception as exc:
|
|
551
|
+
click.echo(f"Error: {exc}", err=True)
|
|
552
|
+
sys.exit(1)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class _LazyCLI:
|
|
556
|
+
"""Lazy proxy so ``from jcli.cli import cli`` keeps working (tests/scripts).
|
|
557
|
+
|
|
558
|
+
Each access rebuilds the CLI from the current specs, so no stale state is
|
|
559
|
+
cached. The real entry point is :func:`main` (console script target).
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
563
|
+
return create_jcli_cli(sys.argv[1:])(*args, **kwargs)
|
|
564
|
+
|
|
565
|
+
def __getattr__(self, name: str) -> Any:
|
|
566
|
+
return getattr(create_jcli_cli(sys.argv[1:]), name)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
cli = _LazyCLI()
|
|
76
570
|
|
|
77
571
|
|
|
78
572
|
if __name__ == "__main__":
|
jcli/plugins/skills.py
CHANGED
|
@@ -59,9 +59,12 @@ def parse_skill_metadata(skill_dir: Path) -> dict[str, Any]:
|
|
|
59
59
|
frontmatter = match.group(1)
|
|
60
60
|
|
|
61
61
|
# Simple YAML parsing without pyyaml dependency
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
lines = frontmatter.split("\n")
|
|
63
|
+
i = 0
|
|
64
|
+
while i < len(lines):
|
|
65
|
+
line = lines[i].strip()
|
|
64
66
|
if not line or line.startswith("#"):
|
|
67
|
+
i += 1
|
|
65
68
|
continue
|
|
66
69
|
|
|
67
70
|
if line.startswith("name:"):
|
|
@@ -70,14 +73,45 @@ def parse_skill_metadata(skill_dir: Path) -> dict[str, Any]:
|
|
|
70
73
|
metadata["version"] = line.split(":", 1)[1].strip().strip("'\"")
|
|
71
74
|
elif line.startswith("description:"):
|
|
72
75
|
desc = line.split(":", 1)[1].strip().strip("'\"")
|
|
76
|
+
if desc in ("|", ">", ""):
|
|
77
|
+
# YAML block scalar: collect following indented lines
|
|
78
|
+
parts = []
|
|
79
|
+
j = i + 1
|
|
80
|
+
while j < len(lines) and (
|
|
81
|
+
lines[j].startswith(" ") or lines[j].startswith("\t")
|
|
82
|
+
):
|
|
83
|
+
parts.append(lines[j].strip())
|
|
84
|
+
j += 1
|
|
85
|
+
if parts:
|
|
86
|
+
desc = " ".join(parts)
|
|
87
|
+
i = j - 1 # continue after the block scalar
|
|
73
88
|
metadata["description"] = desc
|
|
74
89
|
elif line.startswith("- ") and "allowed-tools" in frontmatter:
|
|
75
90
|
tool = line[2:].strip().strip("'\"")
|
|
76
91
|
metadata["allowed_tools"].append(tool)
|
|
92
|
+
i += 1
|
|
77
93
|
|
|
78
94
|
return metadata
|
|
79
95
|
|
|
80
96
|
|
|
97
|
+
def _is_jcli_skill(metadata: dict[str, Any]) -> bool:
|
|
98
|
+
"""Return True if the skill belongs to jcli.
|
|
99
|
+
|
|
100
|
+
A skill is considered jcli's own when:
|
|
101
|
+
- it is a symlink into the bundled skills directory
|
|
102
|
+
(``source == "bundled (symlink)"``), or
|
|
103
|
+
- its name starts with the ``jcli-`` prefix, or
|
|
104
|
+
- its path contains a ``jcli`` path segment (e.g. inside BUNDLED_SKILLS_DIR).
|
|
105
|
+
"""
|
|
106
|
+
if metadata.get("source") == "bundled (symlink)":
|
|
107
|
+
return True
|
|
108
|
+
if str(metadata.get("name", "")).startswith("jcli-"):
|
|
109
|
+
return True
|
|
110
|
+
if "jcli" in Path(str(metadata.get("path", ""))).parts:
|
|
111
|
+
return True
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
81
115
|
def get_bundled_skills() -> list[dict[str, Any]]:
|
|
82
116
|
"""Get all bundled skills from the skills directory."""
|
|
83
117
|
skills = []
|
|
@@ -149,8 +183,8 @@ def install_skill(
|
|
|
149
183
|
|
|
150
184
|
skill_dest = target_dir / name
|
|
151
185
|
|
|
152
|
-
#
|
|
153
|
-
if skill_dest.exists():
|
|
186
|
+
# Path.exists() is False for a broken symlink; is_symlink() catches it
|
|
187
|
+
if skill_dest.exists() or skill_dest.is_symlink():
|
|
154
188
|
if force:
|
|
155
189
|
# Remove existing
|
|
156
190
|
if skill_dest.is_symlink():
|
|
@@ -182,7 +216,8 @@ def uninstall_skill(name: str, install_dir: Path | None = None) -> bool:
|
|
|
182
216
|
target_dir = install_dir or DEFAULT_INSTALL_DIR
|
|
183
217
|
skill_path = target_dir / name
|
|
184
218
|
|
|
185
|
-
|
|
219
|
+
# Path.exists() is False for a broken symlink; is_symlink() catches it
|
|
220
|
+
if not skill_path.exists() and not skill_path.is_symlink():
|
|
186
221
|
raise click.ClickException(f"Skill '{name}' not found in {target_dir}.")
|
|
187
222
|
|
|
188
223
|
if skill_path.is_symlink():
|
|
@@ -223,19 +258,27 @@ def list_cmd(ctx: click.Context, installed: bool, bundled: bool, install_dir: st
|
|
|
223
258
|
|
|
224
259
|
skills_to_show = []
|
|
225
260
|
|
|
226
|
-
if
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# Avoid duplicates if both bundled and installed
|
|
235
|
-
existing_names = {s["name"] for s in skills_to_show}
|
|
236
|
-
for skill in installed_skills:
|
|
237
|
-
if skill["name"] not in existing_names:
|
|
261
|
+
if installed and bundled:
|
|
262
|
+
# Both flags explicitly given: bundled + jcli installed (dedup by name)
|
|
263
|
+
seen: set[str] = set()
|
|
264
|
+
for skill in get_bundled_skills():
|
|
265
|
+
skills_to_show.append(skill)
|
|
266
|
+
seen.add(skill["name"])
|
|
267
|
+
for skill in get_installed_skills(target_dir):
|
|
268
|
+
if _is_jcli_skill(skill) and skill["name"] not in seen:
|
|
238
269
|
skills_to_show.append(skill)
|
|
270
|
+
seen.add(skill["name"])
|
|
271
|
+
elif installed:
|
|
272
|
+
# Only jcli installed skills
|
|
273
|
+
skills_to_show.extend(
|
|
274
|
+
s for s in get_installed_skills(target_dir) if _is_jcli_skill(s)
|
|
275
|
+
)
|
|
276
|
+
elif bundled:
|
|
277
|
+
# Only bundled skills
|
|
278
|
+
skills_to_show.extend(get_bundled_skills())
|
|
279
|
+
else:
|
|
280
|
+
# Default: only bundled (jcli's own skills)
|
|
281
|
+
skills_to_show.extend(get_bundled_skills())
|
|
239
282
|
|
|
240
283
|
if not skills_to_show:
|
|
241
284
|
fmt.print_info("No skills found.")
|
jcli/skills/jcli/SKILL.md
CHANGED
|
@@ -30,7 +30,7 @@ jcli -f yaml job list # YAML 输出
|
|
|
30
30
|
|
|
31
31
|
| 技能 | 说明 |
|
|
32
32
|
|------|------|
|
|
33
|
-
| [jcli-
|
|
33
|
+
| [jcli-auth](auth/SKILL.md) | 认证配置管理 - Jenkins 连接 profile 添加、切换、查看、删除 |
|
|
34
34
|
| [jcli-job](job/SKILL.md) | Job 管理 - Job 创建、删除、复制、启用/禁用 |
|
|
35
35
|
| [jcli-build](build/SKILL.md) | 构建管理 - 构建触发、查看、停止、队列 |
|
|
36
36
|
| [jcli-node](node/SKILL.md) | 节点管理 - Jenkins Agent 节点管理 |
|
|
@@ -40,6 +40,19 @@ jcli -f yaml job list # YAML 输出
|
|
|
40
40
|
| [jcli-view](view/SKILL.md) | 视图管理 - 视图创建、删除、查看 |
|
|
41
41
|
| [jcli-system](system/SKILL.md) | 系统管理 - 系统信息、重启、安静模式 |
|
|
42
42
|
|
|
43
|
+
## 认证配置(auth)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
jcli auth add -n <name> -u <user> -p <token> -e <url> # 添加配置(token 即 Jenkins API Token)
|
|
47
|
+
jcli auth status # 查看所有配置(token 掩码)
|
|
48
|
+
jcli auth use <name> # 切换 active profile
|
|
49
|
+
jcli auth set <name> <field> <value> # 修改字段(url/username/api_token/description)
|
|
50
|
+
jcli auth show [name] # 查看单个配置详情
|
|
51
|
+
jcli auth rm <name> # 删除配置
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
详细用法见 [jcli-auth](auth/SKILL.md)。
|
|
55
|
+
|
|
43
56
|
## Skills 命令
|
|
44
57
|
|
|
45
58
|
```bash
|
|
@@ -70,4 +83,4 @@ jcli completion show fish # Fish 补全脚本
|
|
|
70
83
|
|
|
71
84
|
- 源码: `/data/git-project/jcli`
|
|
72
85
|
- 配置: `~/.jcli/config.yaml`
|
|
73
|
-
- 技能文档: `/data/git-project/jcli/skills/jcli/`
|
|
86
|
+
- 技能文档: `/data/git-project/jcli/jcli/skills/jcli/`
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jcli-auth
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: |
|
|
5
|
+
jcli 认证配置管理 - 管理 Jenkins 连接 profile 的添加、切换、查看、删除等操作。
|
|
6
|
+
allowed-tools:
|
|
7
|
+
- Bash
|
|
8
|
+
- Read
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# jcli-auth
|
|
12
|
+
|
|
13
|
+
管理 jcli 的认证配置 profile,支持多 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
|
+
`~/.jcli/config.yaml`
|
|
24
|
+
|
|
25
|
+
## 配置结构
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
active_profile: default
|
|
29
|
+
profiles:
|
|
30
|
+
default:
|
|
31
|
+
url: https://jenkins.example.com
|
|
32
|
+
username: admin
|
|
33
|
+
api_token: your-api-token-here
|
|
34
|
+
description: Default Jenkins instance
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 命令参考
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
jcli auth add -n <name> -u <user> -p <token> -e <url> # 添加(或更新)配置
|
|
41
|
+
jcli auth add -n <name> ... --default # 添加并设为 active profile
|
|
42
|
+
jcli auth status # 列出所有配置(token 掩码)
|
|
43
|
+
jcli auth use <name> # 切换 active profile
|
|
44
|
+
jcli auth set <name> <field> <value> # 修改字段(url/username/api_token/description)
|
|
45
|
+
jcli auth show [name] # 查看单个配置详情(默认 active)
|
|
46
|
+
jcli auth rm <name> # 删除配置
|
|
47
|
+
jcli auth rm --all # 删除所有配置(重置为模板)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 常见用例
|
|
51
|
+
|
|
52
|
+
### 添加新 profile
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
jcli auth add -n dev -u admin -p your-api-token -e https://jenkins-dev.example.com
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 修改配置
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
jcli auth set dev api_token your-token
|
|
62
|
+
jcli auth set dev description "Development Jenkins"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 切换 profile
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
jcli auth use dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 查看配置
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
jcli auth status
|
|
75
|
+
jcli auth show
|
|
76
|
+
jcli auth show dev
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 环境变量覆盖
|
|
80
|
+
|
|
81
|
+
| 变量 | 覆盖字段 |
|
|
82
|
+
|------|---------|
|
|
83
|
+
| `JCLI_URL` | url |
|
|
84
|
+
| `JCLI_USERNAME` | username |
|
|
85
|
+
| `JCLI_API_TOKEN` | api_token |
|
|
86
|
+
| `JCLI_PROFILE` | active_profile |
|
|
@@ -21,10 +21,12 @@ allowed-tools:
|
|
|
21
21
|
## 命令参考
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
jcli credential list
|
|
25
|
-
jcli credential
|
|
26
|
-
jcli credential
|
|
27
|
-
jcli credential
|
|
24
|
+
jcli credential list [store] [domain] # 列出凭据(默认 store=system, domain=_)
|
|
25
|
+
jcli credential list --depth 2 # 递归列出凭据
|
|
26
|
+
jcli credential get <id> [store] # 查看凭据详情
|
|
27
|
+
jcli credential create <config.xml> [store] [domain] # 从 XML 文件创建凭据
|
|
28
|
+
jcli credential update <config.xml> <id> [store] # 更新凭据
|
|
29
|
+
jcli credential delete <id> [store] [domain] # 删除凭据
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
## 常见用例
|
|
@@ -43,5 +45,12 @@ jcli credential get my-credential-id
|
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
47
|
# 从 XML 配置文件创建凭据
|
|
46
|
-
jcli credential create
|
|
48
|
+
jcli credential create credential-config.xml
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 更新凭据
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 从 XML 文件更新已有凭据
|
|
55
|
+
jcli credential update credential-config.xml my-credential-id
|
|
47
56
|
```
|
jcli/skills/jcli/job/SKILL.md
CHANGED
|
@@ -23,13 +23,16 @@ allowed-tools:
|
|
|
23
23
|
```bash
|
|
24
24
|
jcli job list # 列出所有 Job
|
|
25
25
|
jcli job get <name> # 查看 Job 详情
|
|
26
|
-
jcli job create <name> -
|
|
27
|
-
jcli job delete <name> # 删除 Job
|
|
28
|
-
jcli job delete <name> --yes # 删除 Job(跳过确认)
|
|
26
|
+
jcli job create <name> --config-file config.xml # 从 XML 创建 Job
|
|
27
|
+
jcli job delete <name> # 删除 Job
|
|
29
28
|
jcli job copy <from> <to> # 复制 Job
|
|
29
|
+
jcli job rename <old> <new> # 重命名 Job
|
|
30
30
|
jcli job enable <name> # 启用 Job
|
|
31
31
|
jcli job disable <name> # 禁用 Job
|
|
32
32
|
jcli job config <name> # 查看 Job XML 配置
|
|
33
|
+
jcli job update <config.xml> <name> # 用 XML 更新 Job 配置
|
|
34
|
+
jcli job create-folder <name> # 创建文件夹
|
|
35
|
+
jcli job delete-folder <name> # 删除文件夹(含内容)
|
|
33
36
|
```
|
|
34
37
|
|
|
35
38
|
## 常见用例
|
|
@@ -51,5 +54,18 @@ done
|
|
|
51
54
|
### 从 XML 创建 Job
|
|
52
55
|
|
|
53
56
|
```bash
|
|
54
|
-
jcli job create my-new-job -
|
|
57
|
+
jcli job create my-new-job --config-file job-config.xml
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 参数化创建 Job
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 从 git 仓库创建 freestyle 构建
|
|
64
|
+
jcli job create my-freestyle --job-type freestyle --git-url git@github.com:org/repo.git --git-branch main
|
|
65
|
+
|
|
66
|
+
# 创建 Pipeline 构建(使用仓库根目录的 Jenkinsfile)
|
|
67
|
+
jcli job create my-pipeline --job-type pipeline --git-url git@github.com:org/repo.git --script "pipeline { agent any; stages { stage('Build') { steps { echo 'hi' } } } }"
|
|
68
|
+
|
|
69
|
+
# 定时构建
|
|
70
|
+
jcli job create my-scheduled --job-type freestyle --git-url git@github.com:org/repo.git --cron "H/15 * * * *"
|
|
55
71
|
```
|
jcli/skills/jcli/node/SKILL.md
CHANGED
|
@@ -23,8 +23,10 @@ allowed-tools:
|
|
|
23
23
|
```bash
|
|
24
24
|
jcli node list # 列出所有节点
|
|
25
25
|
jcli node get <name> # 查看节点详情
|
|
26
|
+
jcli node create <name> # 创建节点(JNLP 启动器)
|
|
27
|
+
jcli node create <name> --num-executors 2 --remote-fs /data/jenkins --labels linux # 指定参数创建
|
|
26
28
|
jcli node delete <name> # 删除节点
|
|
27
|
-
jcli node toggle <name> --message "维护"
|
|
29
|
+
jcli node toggle <name> --offline-message "维护" # 节点离线(带原因)
|
|
28
30
|
jcli node toggle <name> # 节点上线
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -34,7 +36,7 @@ jcli node toggle <name> # 节点上线
|
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
38
|
# 节点离线(带维护原因)
|
|
37
|
-
jcli node toggle agent-01 --message "系统升级维护"
|
|
39
|
+
jcli node toggle agent-01 --offline-message "系统升级维护"
|
|
38
40
|
|
|
39
41
|
# 节点恢复上线
|
|
40
42
|
jcli node toggle agent-01
|
|
@@ -23,7 +23,7 @@ allowed-tools:
|
|
|
23
23
|
```bash
|
|
24
24
|
jcli pipeline stages <job> <build> # 查看阶段信息
|
|
25
25
|
jcli pipeline log <job> <build> <node-id> # 查看步骤日志
|
|
26
|
-
jcli pipeline validate
|
|
26
|
+
jcli pipeline validate <Jenkinsfile> # 验证 Jenkinsfile(位置参数,文件路径)
|
|
27
27
|
jcli pipeline pending <job> <build> # 查看待处理输入
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -46,8 +46,8 @@ jcli pipeline log my-job 42 15
|
|
|
46
46
|
### 验证 Jenkinsfile
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
# 验证 Jenkinsfile
|
|
50
|
-
jcli pipeline validate
|
|
49
|
+
# 验证 Jenkinsfile 语法(传入文件路径)
|
|
50
|
+
jcli pipeline validate Jenkinsfile
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### 查看待处理输入
|
jcli/skills/jcli/plugin/SKILL.md
CHANGED
|
@@ -25,7 +25,10 @@ jcli plugin list # 列出已安装插件
|
|
|
25
25
|
jcli plugin get <name> # 查看插件详情
|
|
26
26
|
jcli plugin install <name> # 安装插件
|
|
27
27
|
jcli plugin install git@4.15.0 # 安装指定版本
|
|
28
|
+
jcli plugin install <name1> <name2> # 一次安装多个插件
|
|
28
29
|
jcli plugin uninstall <name> # 卸载插件
|
|
30
|
+
jcli plugin restart <name> # 重启插件
|
|
31
|
+
jcli plugin check-updates # 检查插件更新
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
## 常见用例
|
jcli/skills/jcli/system/SKILL.md
CHANGED
|
@@ -27,7 +27,10 @@ jcli system restart # 安全重启 Jenkins
|
|
|
27
27
|
jcli system quiet-down # 进入安静模式
|
|
28
28
|
jcli system quiet-down --reason "维护" # 带原因的安静模式
|
|
29
29
|
jcli system cancel-quiet-down # 取消安静模式
|
|
30
|
-
jcli system script
|
|
30
|
+
jcli system script <script> # 执行 Groovy 脚本(位置参数)
|
|
31
|
+
jcli system users # 列出所有 Jenkins 用户
|
|
32
|
+
jcli system token <username> # 为用户生成 API token
|
|
33
|
+
jcli system token <username> --token-name <name> # 指定 token 名称
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
## 常见用例
|
|
@@ -62,9 +65,19 @@ jcli system restart
|
|
|
62
65
|
### 执行 Groovy 脚本
|
|
63
66
|
|
|
64
67
|
```bash
|
|
65
|
-
# 执行简单的 Groovy
|
|
68
|
+
# 执行简单的 Groovy 脚本(SCRIPT 为位置参数)
|
|
66
69
|
jcli system script "println('Hello from jcli')"
|
|
67
70
|
|
|
68
71
|
# 获取所有节点信息
|
|
69
72
|
jcli system script "Jenkins.instance.computers.each { println it.name }"
|
|
70
73
|
```
|
|
74
|
+
|
|
75
|
+
### 用户与 Token
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 列出所有用户
|
|
79
|
+
jcli system users
|
|
80
|
+
|
|
81
|
+
# 为指定用户生成 API token
|
|
82
|
+
jcli system token admin
|
|
83
|
+
```
|
jcli/skills/jcli/view/SKILL.md
CHANGED
|
@@ -23,7 +23,8 @@ allowed-tools:
|
|
|
23
23
|
```bash
|
|
24
24
|
jcli view list # 列出所有视图
|
|
25
25
|
jcli view get <name> # 查看视图详情
|
|
26
|
-
jcli view create <
|
|
26
|
+
jcli view create <config.xml> <name> # 从 XML 文件创建视图(XML 在前,名称在后)
|
|
27
|
+
jcli view update <config.xml> <name> # 用 XML 更新视图配置
|
|
27
28
|
jcli view delete <name> # 删除视图
|
|
28
29
|
```
|
|
29
30
|
|
|
@@ -42,6 +43,6 @@ jcli view get my-view
|
|
|
42
43
|
### 创建视图
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
|
-
# 从 XML
|
|
46
|
-
jcli view create
|
|
46
|
+
# 从 XML 配置文件创建视图(XML 文件路径在前,视图名在后)
|
|
47
|
+
jcli view create view-config.xml my-view
|
|
47
48
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jcli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python Jenkins CLI tool
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -9,6 +9,7 @@ Requires-Dist: click>=8.1.0
|
|
|
9
9
|
Requires-Dist: requests~=2.31.0
|
|
10
10
|
Requires-Dist: pyyaml~=6.0.1
|
|
11
11
|
Requires-Dist: rich>=13.7.1
|
|
12
|
+
Requires-Dist: cliyard>=0.6.0
|
|
12
13
|
Provides-Extra: dev
|
|
13
14
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
15
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -23,10 +24,37 @@ A command-line tool for managing Jenkins servers. jcli wraps the Jenkins REST AP
|
|
|
23
24
|
## Features
|
|
24
25
|
|
|
25
26
|
- **8 command modules** covering jobs, builds, nodes, plugins, credentials, pipelines, views, and system management
|
|
27
|
+
- **Spec-driven CLI**: commands are declared as cliyard YAML specs in `specs/` and generated at runtime, no hand-written Click groups
|
|
26
28
|
- **Multiple output formats**: human-readable tables (default, via Rich), JSON, and YAML
|
|
27
29
|
- **Multi-profile support** via `~/.jcli/config.yaml` with environment variable overrides
|
|
28
30
|
- **Automatic CSRF (Crumb) handling** for Jenkins servers with CSRF protection enabled
|
|
29
|
-
- **Built
|
|
31
|
+
- **Built on cliyard and Rich** for a polished, maintainable terminal experience
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
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.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
specs/
|
|
39
|
+
_auth.yaml Server auth chain (basic auth + crumb, as cliyard plugins)
|
|
40
|
+
resources/ One YAML spec per Jenkins domain
|
|
41
|
+
job.yaml job list/get/create/config/copy/enable/disable/delete
|
|
42
|
+
build.yaml build list/get/trigger/replay/log/stop/queue
|
|
43
|
+
node.yaml node list/get/delete/toggle
|
|
44
|
+
plugin.yaml plugin list/get/install/uninstall
|
|
45
|
+
credential.yaml credential list/get/create/delete
|
|
46
|
+
pipeline.yaml pipeline stages/log/pending/validate
|
|
47
|
+
view.yaml view list/get/create/delete
|
|
48
|
+
system.yaml system info/load/quiet-down/restart/script
|
|
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
|
|
52
|
+
jenkins_pipeline_validate.py Jenkinsfile validation
|
|
53
|
+
jenkins_plugin.py Plugin install/uninstall/update-center
|
|
54
|
+
jenkins_system.py Groovy script, quiet-down, restart
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
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.
|
|
30
58
|
|
|
31
59
|
## Requirements
|
|
32
60
|
|
|
@@ -147,6 +175,21 @@ Every command accepts these global flags before the subcommand:
|
|
|
147
175
|
-s, --server TEXT Jenkins server URL
|
|
148
176
|
```
|
|
149
177
|
|
|
178
|
+
### auth — Manage authentication profiles
|
|
179
|
+
|
|
180
|
+
Profiles are stored in `~/.jcli/config.yaml` (token shown masked).
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
jcli auth add -n NAME -u USER -p TOKEN -e URL Add (or update) a profile
|
|
184
|
+
jcli auth add -n NAME ... --default ... and make it active
|
|
185
|
+
jcli auth status List profiles (tokens masked)
|
|
186
|
+
jcli auth use NAME Switch the active profile
|
|
187
|
+
jcli auth rm NAME Remove a profile
|
|
188
|
+
jcli auth rm --all Remove all profiles (reset template)
|
|
189
|
+
jcli auth set NAME FIELD VALUE Set url/username/api_token/description
|
|
190
|
+
jcli auth show [NAME] Show a profile's details (default: active)
|
|
191
|
+
```
|
|
192
|
+
|
|
150
193
|
### job — Manage Jenkins jobs
|
|
151
194
|
|
|
152
195
|
```
|
|
@@ -166,6 +209,9 @@ jcli job delete JOB_NAME Delete a job
|
|
|
166
209
|
jcli build list JOB_NAME List recent builds for a job
|
|
167
210
|
jcli build get JOB_NAME BUILD_NUMBER Show details for a specific build
|
|
168
211
|
jcli build trigger JOB_NAME Trigger a new build
|
|
212
|
+
jcli build rebuild JOB_NAME BUILD_NUMBER Rebuild reusing previous parameters
|
|
213
|
+
jcli build rebuild JOB_NAME BUILD_NUMBER --set KEY=VAL ... and override values (repeatable)
|
|
214
|
+
jcli build replay JOB_NAME BUILD_NUMBER JENKINSFILE Replay a build with a custom Jenkinsfile
|
|
169
215
|
jcli build log JOB_NAME BUILD_NUMBER Show console log for a build
|
|
170
216
|
jcli build stop JOB_NAME BUILD_NUMBER Stop a running build
|
|
171
217
|
jcli build queue Show the current build queue
|
|
@@ -268,23 +314,20 @@ pytest --cov=jcli --cov-report=term-missing
|
|
|
268
314
|
|
|
269
315
|
```
|
|
270
316
|
jcli/
|
|
271
|
-
cli.py
|
|
317
|
+
cli.py Entry point: builds the Click CLI from cliyard YAML specs
|
|
318
|
+
cli_helpers.py Global option injection and profile/format plumbing
|
|
272
319
|
__init__.py Version
|
|
273
|
-
plugins/
|
|
274
|
-
job.py
|
|
275
|
-
build.py
|
|
276
|
-
node.py
|
|
277
|
-
plugin.py
|
|
278
|
-
credential.py
|
|
279
|
-
pipeline.py
|
|
280
|
-
view.py
|
|
281
|
-
system.py
|
|
320
|
+
plugins/ Legacy hand-written command modules (kept for SDK compatibility)
|
|
282
321
|
sdk/ Shared libraries
|
|
283
322
|
client.py Jenkins REST API client (HTTP, auth, crumb)
|
|
284
323
|
config.py Configuration management (YAML, env vars)
|
|
285
324
|
output/
|
|
286
325
|
formatter.py Table/JSON/YAML output formatting
|
|
287
326
|
exceptions.py Typed exceptions
|
|
327
|
+
specs/ cliyard YAML specs (command definitions)
|
|
328
|
+
_auth.yaml Auth chain: basic + crumb plugins
|
|
329
|
+
resources/ One YAML per Jenkins domain (job, build, node, ...)
|
|
330
|
+
plugins/ Python method plugins for non-trivial logic
|
|
288
331
|
tests/ pytest test suite
|
|
289
332
|
```
|
|
290
333
|
|
|
@@ -293,6 +336,7 @@ tests/ pytest test suite
|
|
|
293
336
|
| Package | Purpose |
|
|
294
337
|
|-----------------|-----------------------------|
|
|
295
338
|
| click >= 8.1.0 | CLI framework |
|
|
339
|
+
| cliyard >= 0.6.0 | Spec-driven CLI engine (YAML command generation) |
|
|
296
340
|
| requests ~= 2.31 | HTTP client |
|
|
297
341
|
| pyyaml ~= 6.0.1 | YAML config parsing |
|
|
298
342
|
| rich >= 13.7.1 | Terminal formatting (tables) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
jcli/__init__.py,sha256=
|
|
1
|
+
jcli/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
2
2
|
jcli/__main__.py,sha256=ARL8XYr45T4pIf0gnyCCbK8uljcn1LF7TKcFapxHt_k,34
|
|
3
|
-
jcli/cli.py,sha256=
|
|
3
|
+
jcli/cli.py,sha256=cg45uwYAW2IsdilPffCBSWK7RE2ragnKJ2xiYp9_m94,21175
|
|
4
4
|
jcli/cli_helpers.py,sha256=8GsvEwmaqS8p9Rpx7d_b3_i_Pwag8zsJNNbunxgNXS0,1831
|
|
5
5
|
jcli/plugins/__init__.py,sha256=I6vTpfYO3gIwHoNu5V-sOIdpT_g-bAy4RXR3TW3-k1U,1054
|
|
6
6
|
jcli/plugins/build.py,sha256=60F0UPMFy_eZznBeAIM0XwCcQ3pAuJJjacGaCXxOlcI,6032
|
|
@@ -10,7 +10,7 @@ jcli/plugins/job.py,sha256=dq5jbAn8ZjtYNqJlo7u-0iORubMdZMB2sj4GVK01xoU,12872
|
|
|
10
10
|
jcli/plugins/node.py,sha256=nwUDYvev9R_BOoYHVmNvNp3vhrmZAXVFN2z_-F_iO7I,4776
|
|
11
11
|
jcli/plugins/pipeline.py,sha256=vTtLk0tPiMSwy2CsCEmtQMhbt9baFoxAqGCSyHtOmOc,3049
|
|
12
12
|
jcli/plugins/plugin.py,sha256=0LMaPjr5jpZokOLENt0rDi_Klk4JYcw8uUzZUceDYvk,4713
|
|
13
|
-
jcli/plugins/skills.py,sha256=
|
|
13
|
+
jcli/plugins/skills.py,sha256=5Nw3kPLbx82-8ackdXVLnIL09jziv53jhMZ2sCqYihk,14921
|
|
14
14
|
jcli/plugins/system.py,sha256=2ojwBOsaiuNKmuiTLvU6GpLeSI3-kccy1QjpHuX_8vE,4938
|
|
15
15
|
jcli/plugins/view.py,sha256=eH7pCe7Tw1rC4kVBZDxHqVd3ZrXYLlVxNGPjWvhgw8Q,4475
|
|
16
16
|
jcli/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -28,17 +28,17 @@ jcli/sdk/system.py,sha256=L5PHlrjs-5_PFFOQ9IBA-vMwgQyqq33TfJidTcs0VSQ,6022
|
|
|
28
28
|
jcli/sdk/view.py,sha256=5Poo50l3eY9ivef3hs0CyL-K39qDk39XQuX7gEHHRlA,2300
|
|
29
29
|
jcli/sdk/output/__init__.py,sha256=Rt0F77oq24tqHvTDV4HyWKAjDtOyPOh9m7ucFy7B77s,70
|
|
30
30
|
jcli/sdk/output/formatter.py,sha256=OUjQxeD-2iSNncuuRaLa01qLi899ux3F39KNkc8ik2Q,3876
|
|
31
|
-
jcli/skills/jcli/SKILL.md,sha256=
|
|
32
|
-
jcli/skills/jcli/
|
|
33
|
-
jcli/skills/jcli/credential/SKILL.md,sha256=
|
|
34
|
-
jcli/skills/jcli/job/SKILL.md,sha256=
|
|
35
|
-
jcli/skills/jcli/node/SKILL.md,sha256=
|
|
36
|
-
jcli/skills/jcli/pipeline/SKILL.md,sha256=
|
|
37
|
-
jcli/skills/jcli/plugin/SKILL.md,sha256
|
|
38
|
-
jcli/skills/jcli/system/SKILL.md,sha256=
|
|
39
|
-
jcli/skills/jcli/view/SKILL.md,sha256=
|
|
40
|
-
python_jcli-
|
|
41
|
-
python_jcli-
|
|
42
|
-
python_jcli-
|
|
43
|
-
python_jcli-
|
|
44
|
-
python_jcli-
|
|
31
|
+
jcli/skills/jcli/SKILL.md,sha256=X9xHxRvNa4F3hUN8bhckBXAJZx2MWY58hGqqd0y158M,3040
|
|
32
|
+
jcli/skills/jcli/auth/SKILL.md,sha256=4_DLD51SwKTXh2Xi0UiQbqG4uXw7qIKv2SOOopS2Qn0,1996
|
|
33
|
+
jcli/skills/jcli/credential/SKILL.md,sha256=3VsVdGalsIySvGfFO79IdL0b5OjF7kJBS9_4P56Wt2A,1244
|
|
34
|
+
jcli/skills/jcli/job/SKILL.md,sha256=bmGyiyLXS2n395bA31gehcMRe094SUBq-FzUlUDvpQM,2066
|
|
35
|
+
jcli/skills/jcli/node/SKILL.md,sha256=k4qi6ApspdIlBcxb0v2ItPrPwT7d9AnFq4N-vGhGcqY,1247
|
|
36
|
+
jcli/skills/jcli/pipeline/SKILL.md,sha256=kUwnxBCUx53zi7efVgtJg9ainO-YuX3griaCqoUyzxU,1231
|
|
37
|
+
jcli/skills/jcli/plugin/SKILL.md,sha256=--1t5LekIHAtnCWUnsQ9BKkPIgAQ7n-p7_QGd8H2jbI,1178
|
|
38
|
+
jcli/skills/jcli/system/SKILL.md,sha256=SHdcLbaJyifjt4KJlmdcHdYLsFGwpF-6lqiWdYpw3co,1932
|
|
39
|
+
jcli/skills/jcli/view/SKILL.md,sha256=f2QZEzvFocIlR7Yh0RKnp5y4sijwUJU_1fxiW7gKJcA,1047
|
|
40
|
+
python_jcli-1.0.0.dist-info/METADATA,sha256=9A7W8m365rFW8_iTXqwPA9VFDE-YUmNvdLR-7BKH7qs,12497
|
|
41
|
+
python_jcli-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
42
|
+
python_jcli-1.0.0.dist-info/entry_points.txt,sha256=g-Gb8voGH3Ru4NEoCUMJXJiMZMapdAfYi6Xt1phyFi4,39
|
|
43
|
+
python_jcli-1.0.0.dist-info/top_level.txt,sha256=WFqrbWSEfhvQfbN2m-_Wi5KKCwv1_23LhzPUqo7AdQc,5
|
|
44
|
+
python_jcli-1.0.0.dist-info/RECORD,,
|
jcli/skills/jcli/config/SKILL.md
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: jcli-config
|
|
3
|
-
version: 1.0.0
|
|
4
|
-
description: |
|
|
5
|
-
jcli 配置管理 - 管理 Jenkins 连接配置,支持多实例管理。
|
|
6
|
-
allowed-tools:
|
|
7
|
-
- Bash
|
|
8
|
-
- Read
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# jcli-config
|
|
12
|
-
|
|
13
|
-
管理 jcli 的连接配置,支持多 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 config init
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## 配置文件位置
|
|
28
|
-
|
|
29
|
-
`~/.jcli/config.yaml`
|
|
30
|
-
|
|
31
|
-
## 配置结构
|
|
32
|
-
|
|
33
|
-
```yaml
|
|
34
|
-
active_profile: default
|
|
35
|
-
profiles:
|
|
36
|
-
default:
|
|
37
|
-
url: https://jenkins.example.com
|
|
38
|
-
username: admin
|
|
39
|
-
api_token: your-api-token-here
|
|
40
|
-
description: Default Jenkins instance
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## 多实例管理
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
# 添加新 profile
|
|
47
|
-
jcli config add dev --url https://jenkins-dev.example.com --username admin
|
|
48
|
-
|
|
49
|
-
# 修改配置
|
|
50
|
-
jcli config set dev api_token your-token
|
|
51
|
-
jcli config set dev description "Development Jenkins"
|
|
52
|
-
|
|
53
|
-
# 切换 profile
|
|
54
|
-
jcli config use dev
|
|
55
|
-
|
|
56
|
-
# 查看配置
|
|
57
|
-
jcli config show
|
|
58
|
-
jcli config list
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## 环境变量覆盖
|
|
62
|
-
|
|
63
|
-
| 变量 | 覆盖字段 |
|
|
64
|
-
|------|---------|
|
|
65
|
-
| `JCLI_URL` | url |
|
|
66
|
-
| `JCLI_USERNAME` | username |
|
|
67
|
-
| `JCLI_API_TOKEN` | api_token |
|
|
68
|
-
| `JCLI_PROFILE` | active_profile |
|
|
69
|
-
|
|
70
|
-
## 命令参考
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
jcli config init # 初始化配置文件
|
|
74
|
-
jcli config show # 查看当前配置
|
|
75
|
-
jcli config show --profile dev # 查看指定 profile
|
|
76
|
-
jcli config list # 列出所有 profiles
|
|
77
|
-
jcli config add <name> --url URL # 添加新 profile
|
|
78
|
-
jcli config set <name> <field> <value> # 修改配置
|
|
79
|
-
jcli config use <name> # 切换 active profile
|
|
80
|
-
jcli config delete <name> # 删除 profile
|
|
81
|
-
```
|
|
File without changes
|
|
File without changes
|