vcf-super-cli 0.2.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.
- vcf_super_cli-0.2.0.dist-info/METADATA +101 -0
- vcf_super_cli-0.2.0.dist-info/RECORD +31 -0
- vcf_super_cli-0.2.0.dist-info/WHEEL +4 -0
- vcf_super_cli-0.2.0.dist-info/entry_points.txt +3 -0
- vcf_super_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
- vsc/__init__.py +11 -0
- vsc/_version.py +7 -0
- vsc/cli/__init__.py +0 -0
- vsc/cli/app.py +98 -0
- vsc/cli/profiles.py +208 -0
- vsc/cli/skill.py +28 -0
- vsc/config/__init__.py +7 -0
- vsc/config/schema.py +39 -0
- vsc/config/store.py +105 -0
- vsc/connect/__init__.py +6 -0
- vsc/connect/session.py +73 -0
- vsc/connect/targets.py +121 -0
- vsc/gen/__init__.py +7 -0
- vsc/gen/builder.py +243 -0
- vsc/gen/discover.py +221 -0
- vsc/gen/model.py +104 -0
- vsc/gen/params.py +232 -0
- vsc/gen/preview.py +82 -0
- vsc/logging_config.py +30 -0
- vsc/output/__init__.py +0 -0
- vsc/output/errors.py +106 -0
- vsc/output/exit_codes.py +40 -0
- vsc/output/render.py +134 -0
- vsc/skill/__init__.py +1 -0
- vsc/skill/assets/SKILL.md +96 -0
- vsc/skill/export.py +26 -0
vsc/output/errors.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Map SDK/transport exceptions to the stable error envelope + exit codes.
|
|
2
|
+
|
|
3
|
+
vAPI errors are both ``Exception`` and ``VapiStruct``; dispatch on the stable
|
|
4
|
+
``error_type`` string rather than ``isinstance`` chains. Transport-layer failures
|
|
5
|
+
(``requests`` connection/TLS errors) happen before the vAPI layer and are mapped
|
|
6
|
+
separately.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import com.vmware.vapi.std.errors_client as verr
|
|
16
|
+
|
|
17
|
+
from vsc.output.exit_codes import ExitCode
|
|
18
|
+
|
|
19
|
+
# vAPI ``error_type`` -> exit code. Unlisted/None -> ExitCode.ERROR.
|
|
20
|
+
ERROR_TYPE_TO_EXIT: dict[str, ExitCode] = {
|
|
21
|
+
"UNAUTHENTICATED": ExitCode.AUTH,
|
|
22
|
+
"UNAUTHORIZED": ExitCode.AUTH,
|
|
23
|
+
"INVALID_ARGUMENT": ExitCode.USAGE,
|
|
24
|
+
"UNSUPPORTED": ExitCode.USAGE,
|
|
25
|
+
"UNEXPECTED_INPUT": ExitCode.USAGE,
|
|
26
|
+
"OPERATION_NOT_FOUND": ExitCode.USAGE,
|
|
27
|
+
"NOT_FOUND": ExitCode.NOT_FOUND,
|
|
28
|
+
"RESOURCE_INACCESSIBLE": ExitCode.NOT_FOUND,
|
|
29
|
+
"ALREADY_EXISTS": ExitCode.CONFLICT,
|
|
30
|
+
"ALREADY_IN_DESIRED_STATE": ExitCode.CONFLICT,
|
|
31
|
+
"RESOURCE_IN_USE": ExitCode.CONFLICT,
|
|
32
|
+
"FEATURE_IN_USE": ExitCode.CONFLICT,
|
|
33
|
+
"NOT_ALLOWED_IN_CURRENT_STATE": ExitCode.CONFLICT,
|
|
34
|
+
"CONCURRENT_CHANGE": ExitCode.CONFLICT,
|
|
35
|
+
"SERVICE_UNAVAILABLE": ExitCode.UNAVAILABLE,
|
|
36
|
+
"RESOURCE_BUSY": ExitCode.UNAVAILABLE,
|
|
37
|
+
"TIMED_OUT": ExitCode.UNAVAILABLE,
|
|
38
|
+
"UNABLE_TO_ALLOCATE_RESOURCE": ExitCode.UNAVAILABLE,
|
|
39
|
+
"INTERNAL_SERVER_ERROR": ExitCode.ERROR,
|
|
40
|
+
"CANCELED": ExitCode.ERROR,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _messages(err: Any) -> str:
|
|
45
|
+
msgs = getattr(err, "messages", None) or []
|
|
46
|
+
texts = [getattr(m, "default_message", None) for m in msgs]
|
|
47
|
+
joined = "; ".join(t for t in texts if t)
|
|
48
|
+
return joined or str(err) or err.__class__.__name__
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def envelope_for_vapi(err: Any) -> tuple[dict[str, Any], ExitCode]:
|
|
52
|
+
"""Build the error envelope + exit code for a vAPI error."""
|
|
53
|
+
error_type = getattr(err, "error_type", None)
|
|
54
|
+
code = ERROR_TYPE_TO_EXIT.get(error_type or "", ExitCode.ERROR)
|
|
55
|
+
detail: Any
|
|
56
|
+
try:
|
|
57
|
+
detail = err.to_dict()
|
|
58
|
+
except Exception:
|
|
59
|
+
detail = None
|
|
60
|
+
env = {
|
|
61
|
+
"error": {
|
|
62
|
+
"code": int(code),
|
|
63
|
+
"kind": error_type or err.__class__.__name__,
|
|
64
|
+
"message": _messages(err),
|
|
65
|
+
"details": detail,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return env, code
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def envelope_for_transport(err: Exception) -> tuple[dict[str, Any], ExitCode]:
|
|
72
|
+
"""Build the error envelope + exit code for a transport-layer error.
|
|
73
|
+
|
|
74
|
+
TLS trust failures are a *connection* problem (failure to negotiate), not an
|
|
75
|
+
authentication failure — they get :class:`ExitCode.CONNECTION` plus a hint
|
|
76
|
+
pointing at the ``VSC_<BACKEND>_INSECURE`` escape hatch.
|
|
77
|
+
"""
|
|
78
|
+
name = err.__class__.__name__
|
|
79
|
+
message = str(err) or name
|
|
80
|
+
if "SSL" in name:
|
|
81
|
+
message = (
|
|
82
|
+
f"TLS verification failed: {message} "
|
|
83
|
+
"(for a self-signed/lab cert, set VSC_<BACKEND>_INSECURE=1)"
|
|
84
|
+
)
|
|
85
|
+
env = {
|
|
86
|
+
"error": {
|
|
87
|
+
"code": int(ExitCode.CONNECTION),
|
|
88
|
+
"kind": name,
|
|
89
|
+
"message": message,
|
|
90
|
+
"details": None,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return env, ExitCode.CONNECTION
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def render_error(env: dict[str, Any], fmt: str) -> None:
|
|
97
|
+
"""Print the error envelope to stderr (JSON always; ``message`` for tables)."""
|
|
98
|
+
if fmt == "table":
|
|
99
|
+
print(env["error"]["message"], file=sys.stderr)
|
|
100
|
+
else:
|
|
101
|
+
print(json.dumps(env, default=str), file=sys.stderr)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def is_vapi_error(err: BaseException) -> bool:
|
|
105
|
+
"""True if ``err`` is a vAPI ``Error`` (has the stable ``error_type``)."""
|
|
106
|
+
return isinstance(err, verr.Error)
|
vsc/output/exit_codes.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Documented, stable process exit codes.
|
|
2
|
+
|
|
3
|
+
These are part of the CLI's public contract for scripts and agents. Values are
|
|
4
|
+
frozen; add new codes rather than renumbering existing ones.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import IntEnum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExitCode(IntEnum):
|
|
13
|
+
"""Stable exit codes returned by ``vsc``."""
|
|
14
|
+
|
|
15
|
+
OK = 0
|
|
16
|
+
"""Success."""
|
|
17
|
+
|
|
18
|
+
ERROR = 1
|
|
19
|
+
"""Generic/unexpected failure."""
|
|
20
|
+
|
|
21
|
+
USAGE = 2
|
|
22
|
+
"""Invalid arguments or usage (Typer/Click default)."""
|
|
23
|
+
|
|
24
|
+
AUTH = 3
|
|
25
|
+
"""Authentication or authorization failure."""
|
|
26
|
+
|
|
27
|
+
NOT_FOUND = 4
|
|
28
|
+
"""A requested resource does not exist."""
|
|
29
|
+
|
|
30
|
+
CONNECTION = 5
|
|
31
|
+
"""Could not reach or negotiate with the target (vCenter/NSX)."""
|
|
32
|
+
|
|
33
|
+
CONFIG = 6
|
|
34
|
+
"""Missing or invalid configuration/profile."""
|
|
35
|
+
|
|
36
|
+
CONFLICT = 7
|
|
37
|
+
"""Resource conflict (already exists, in use, wrong state, concurrent change)."""
|
|
38
|
+
|
|
39
|
+
UNAVAILABLE = 8
|
|
40
|
+
"""Target temporarily unavailable, busy, or the request timed out."""
|
vsc/output/render.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Render SDK results to JSON (default) or a Rich table.
|
|
2
|
+
|
|
3
|
+
vAPI results are ``VapiStruct`` instances (or lists/maps of them). ``to_dict()``
|
|
4
|
+
parses floats as ``Decimal``, so JSON serialization uses ``default=str``. Dict
|
|
5
|
+
keys are wire/canonical names (e.g. ``memory_size_MiB``), not the snake_case
|
|
6
|
+
constructor kwargs — we surface them as-is.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import enum
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from vmware.vapi.bindings.struct import VapiStruct
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OutputFormat(str, enum.Enum):
|
|
21
|
+
"""Supported ``--output`` formats."""
|
|
22
|
+
|
|
23
|
+
json = "json"
|
|
24
|
+
table = "table"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def jsonable(value: Any) -> Any:
|
|
28
|
+
"""Convert a vAPI result into a JSON-serializable structure."""
|
|
29
|
+
if isinstance(value, VapiStruct):
|
|
30
|
+
return {k: jsonable(v) for k, v in value.to_dict().items()}
|
|
31
|
+
if isinstance(value, enum.Enum):
|
|
32
|
+
return value.value
|
|
33
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
34
|
+
return [jsonable(v) for v in value]
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
return {str(k): jsonable(v) for k, v in value.items()}
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_json(value: Any) -> str:
|
|
41
|
+
"""Serialize a result to indented JSON text."""
|
|
42
|
+
return json.dumps(jsonable(value), indent=2, default=str, sort_keys=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _rows(data: Any) -> list[dict[str, Any]]:
|
|
46
|
+
if isinstance(data, dict):
|
|
47
|
+
return [data]
|
|
48
|
+
if isinstance(data, list):
|
|
49
|
+
return [r for r in data if isinstance(r, dict)]
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def to_table(value: Any, console: Console) -> bool:
|
|
54
|
+
"""Render ``value`` as a Rich table. Returns ``False`` if it isn't tabular."""
|
|
55
|
+
data = jsonable(value)
|
|
56
|
+
rows = _rows(data)
|
|
57
|
+
if not rows:
|
|
58
|
+
return False
|
|
59
|
+
columns: list[str] = []
|
|
60
|
+
for row in rows:
|
|
61
|
+
for key in row:
|
|
62
|
+
if key not in columns:
|
|
63
|
+
columns.append(key)
|
|
64
|
+
table = Table(show_header=True, header_style="bold")
|
|
65
|
+
for col in columns:
|
|
66
|
+
table.add_column(col)
|
|
67
|
+
for row in rows:
|
|
68
|
+
table.add_row(*[_cell(row.get(col)) for col in columns])
|
|
69
|
+
console.print(table)
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cell(value: Any) -> str:
|
|
74
|
+
if value is None:
|
|
75
|
+
return ""
|
|
76
|
+
if isinstance(value, (dict, list)):
|
|
77
|
+
return json.dumps(value, default=str)
|
|
78
|
+
return str(value)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def emit(value: Any, fmt: str | OutputFormat = "json", *, console: Console | None = None) -> None:
|
|
82
|
+
"""Print a result in the requested format (``json`` or ``table``)."""
|
|
83
|
+
fmt = fmt.value if isinstance(fmt, OutputFormat) else fmt
|
|
84
|
+
if fmt == "table":
|
|
85
|
+
console = console or Console()
|
|
86
|
+
if to_table(value, console):
|
|
87
|
+
return
|
|
88
|
+
# Fall back to JSON for non-tabular payloads (e.g. a single scalar).
|
|
89
|
+
print(to_json(value))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_APPLY_HINT = "re-run with --apply to execute"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_envelope(plan: dict[str, Any], *, applied: bool, result: Any = None) -> dict[str, Any]:
|
|
96
|
+
"""Build the stable write envelope shared by dry-run and apply.
|
|
97
|
+
|
|
98
|
+
Dry-run (``applied=False``) carries the plan and an apply hint and never a
|
|
99
|
+
result; apply (``applied=True``) carries the plan and the SDK response.
|
|
100
|
+
"""
|
|
101
|
+
env: dict[str, Any] = {"applied": applied, "request": plan}
|
|
102
|
+
if applied:
|
|
103
|
+
env["result"] = jsonable(result)
|
|
104
|
+
else:
|
|
105
|
+
env["apply_hint"] = _APPLY_HINT
|
|
106
|
+
return env
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def emit_request(
|
|
110
|
+
plan: dict[str, Any],
|
|
111
|
+
*,
|
|
112
|
+
applied: bool,
|
|
113
|
+
result: Any = None,
|
|
114
|
+
fmt: str | OutputFormat = "json",
|
|
115
|
+
console: Console | None = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Print the write envelope (dry-run preview or applied result)."""
|
|
118
|
+
fmt = fmt.value if isinstance(fmt, OutputFormat) else fmt
|
|
119
|
+
env = write_envelope(plan, applied=applied, result=result)
|
|
120
|
+
if fmt == "table":
|
|
121
|
+
console = console or Console()
|
|
122
|
+
status = "APPLIED" if applied else "DRY-RUN"
|
|
123
|
+
console.print(f"[bold]{status}[/bold] {plan['method']} {plan['url']}")
|
|
124
|
+
if not applied:
|
|
125
|
+
console.print(f"({_APPLY_HINT})")
|
|
126
|
+
return
|
|
127
|
+
# Applied: show the result as a table, or a clean scalar/empty line — never
|
|
128
|
+
# fall through to dumping the whole JSON envelope in table mode.
|
|
129
|
+
if result is None:
|
|
130
|
+
console.print("(no body)")
|
|
131
|
+
elif not to_table(result, console):
|
|
132
|
+
console.print(to_json(result))
|
|
133
|
+
return
|
|
134
|
+
print(to_json(env))
|
vsc/skill/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The bundled agent Skill and its export command."""
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vcf-super-cli
|
|
3
|
+
description: Use when querying or changing VMware Cloud Foundation 9 (vSphere/vCenter and NSX Policy) from the command line via `vsc`. The command tree is generated from the vcf-sdk. Reads return JSON; writes are dry-run by default and require `--apply`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# vcf-super-cli (`vsc`)
|
|
7
|
+
|
|
8
|
+
`vsc` is a CLI for VMware Cloud Foundation 9. Its command tree is generated from
|
|
9
|
+
the `vcf-sdk` vAPI bindings and split into two product groups:
|
|
10
|
+
|
|
11
|
+
- `vsc vsphere …` — vCenter (vm, host, cluster, datacenter, datastore, folder, network,
|
|
12
|
+
resource-pool, and the VM power/hardware leaves: power, cpu, memory, disk, ethernet —
|
|
13
|
+
each takes the VM id as an argument, e.g. `vsc vsphere power stop <vm>`)
|
|
14
|
+
- `vsc nsx …` — NSX Policy (segments, tier0s, tier1s, services, groups, security-policies,
|
|
15
|
+
gateway-policies, ip-pools, dhcp-server-configs, dhcp-relay-configs, locale-services)
|
|
16
|
+
|
|
17
|
+
Discover the live surface with `vsc --help`, `vsc vsphere --help`, `vsc nsx --help`,
|
|
18
|
+
and `vsc vsphere vm --help`. Leaves expose `list`/`get <id>` reads and — where the SDK
|
|
19
|
+
provides them — write verbs (`create`, `delete`, `set`, `patch`, power actions, …).
|
|
20
|
+
|
|
21
|
+
## Writes are dry-run by default
|
|
22
|
+
|
|
23
|
+
**Every write command previews and changes nothing unless you pass `--apply`.** This is
|
|
24
|
+
the core safety contract — a write without `--apply` never opens a connection.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
vsc --profile prod vsphere vm delete vm-42 # DRY-RUN: prints the plan, changes nothing
|
|
28
|
+
vsc --profile prod vsphere vm delete vm-42 --apply # executes the delete
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Dry-run emits the resolved request so you can see exactly what `--apply` would send:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "applied": false,
|
|
35
|
+
"request": { "method": "DELETE", "url": "/vcenter/vm/vm-42",
|
|
36
|
+
"path_params": {"vm": "vm-42"}, "query": {}, "body": null,
|
|
37
|
+
"backend": "vsphere", "service": "vm", "operation": "delete" },
|
|
38
|
+
"apply_hint": "re-run with --apply to execute" }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With `--apply`, the same envelope carries `"applied": true` and a `"result"` field
|
|
42
|
+
(the SDK response) instead of `apply_hint`. Branch on `applied`.
|
|
43
|
+
|
|
44
|
+
Bodies/specs are passed as JSON to the relevant option and built into the SDK struct:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
vsc --profile prod nsx segments set web --segment '{"display_name":"web"}' # dry-run
|
|
48
|
+
vsc --profile prod nsx segments set web --segment '{"display_name":"web"}' --apply # executes
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Write failures use the same error envelope + exit codes — notably `7` CONFLICT
|
|
52
|
+
(already-exists / in-use / wrong-state / concurrent-change) and `8` UNAVAILABLE
|
|
53
|
+
(busy / timed-out / temporarily unavailable).
|
|
54
|
+
|
|
55
|
+
## Conventions for agents
|
|
56
|
+
|
|
57
|
+
- **Output is JSON by default** — parse `stdout`. Use `--output table` / `-o table`
|
|
58
|
+
only for human display.
|
|
59
|
+
- **Errors** go to `stderr` as `{ "error": { "code", "kind", "message", "details" } }`.
|
|
60
|
+
`stdout` is data only; logs/diagnostics go to `stderr`.
|
|
61
|
+
- **Exit codes** are stable — branch on these, not on message text:
|
|
62
|
+
`0` ok · `1` generic · `2` usage · `3` auth · `4` not-found · `5` connection ·
|
|
63
|
+
`6` config · `7` conflict · `8` unavailable.
|
|
64
|
+
|
|
65
|
+
## Targeting an environment
|
|
66
|
+
|
|
67
|
+
Configure once, then select per command with `--profile/-p`:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
vsc profiles add prod --vsphere-server vc.example --vsphere-username administrator@vsphere.local
|
|
71
|
+
vsc profiles set-password prod vsphere # prompts; stored in the OS keyring
|
|
72
|
+
vsc --profile prod vsphere vm list
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Environment variables override the profile (useful in CI):
|
|
76
|
+
`VSC_VSPHERE_SERVER`, `VSC_VSPHERE_USERNAME`, `VSC_VSPHERE_PASSWORD`,
|
|
77
|
+
`VSC_VSPHERE_INSECURE`, and the `VSC_NSX_*` equivalents; `VSC_PROFILE` selects a profile.
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
# Reads
|
|
83
|
+
vsc --profile prod vsphere vm list
|
|
84
|
+
vsc --profile prod vsphere vm get vm-42
|
|
85
|
+
vsc --profile prod vsphere host list -o table
|
|
86
|
+
vsc --profile prod nsx segments list
|
|
87
|
+
vsc --profile prod nsx tier1s get <tier1-id>
|
|
88
|
+
|
|
89
|
+
# Writes — preview first (dry-run), then --apply
|
|
90
|
+
vsc --profile prod vsphere power stop vm-42 # preview
|
|
91
|
+
vsc --profile prod vsphere power stop vm-42 --apply # execute
|
|
92
|
+
vsc --profile prod nsx tier1s set t1-web --tier1 '{"display_name":"web"}' --apply
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> This Skill ships with the package. Refresh an exported copy with
|
|
96
|
+
> `vsc skill export <dir> --apply`.
|
vsc/skill/export.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Read the bundled agent Skill and export it to a directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_SKILL_NAME = "vcf-super-cli"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def skill_text() -> str:
|
|
12
|
+
"""Return the bundled SKILL.md content."""
|
|
13
|
+
return (files("vsc.skill") / "assets" / "SKILL.md").read_text(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def export_path(dest_dir: Path) -> Path:
|
|
17
|
+
"""The file that :func:`export_skill` would write under ``dest_dir``."""
|
|
18
|
+
return dest_dir / _SKILL_NAME / "SKILL.md"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def export_skill(dest_dir: Path) -> Path:
|
|
22
|
+
"""Write the bundled SKILL.md to ``dest_dir/vcf-super-cli/SKILL.md``."""
|
|
23
|
+
target = export_path(dest_dir)
|
|
24
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
target.write_text(skill_text(), encoding="utf-8")
|
|
26
|
+
return target
|