Adapt an existing CLI
The Config_File flow exists so that a tool with a working CLI can expose itself over WSP without writing any Python glue. This page walks through that case end to end, using Hatch as the worked example.
A note on Hatch. This page describes how Hatch could adapt itself today, with no code changes on Hatch's part — purely by shipping a config file. Hatch itself plans to integrate with wspkit directly via a
hatch serversubcommand, which is a different and tighter integration. The Config_File approach below is the path for any tool author who wants to expose their CLI via WSP without taking a runtime dependency.
Goal
Expose Hatch's environment management over WSP, so that a consuming
tool (wsp, an editor, an LSP-style client, anything) can call:
environment/list→hatch env show --jsonenvironment/create→hatch env createenvironment/delete→hatch env removeenvironment/execute→hatch run -e <env> -- <command>
…without Hatch importing wspkit or writing a single Python adapter.
The config file
# hatch-wsp.toml
[handlers."environment/list"]
command = ["hatch", "env", "show", "--json"]
result = "json"
[handlers."environment/create"]
command = ["hatch", "env", "create", "{name}"]
result = "template"
template = { id = "{name}", name = "{name}", python_version = "{python_version}", interpreter_path = "", installed_packages = [], extra = {} }
[handlers."environment/delete"]
command = ["hatch", "env", "remove", "{id}"]
result = "template"
template = { id = "{id}" }
[handlers."environment/execute"]
command = ["hatch", "run", "-e", "{id}", "--", "{argv}"]
result = "exec"
That's it. Six declarations, no Python. Run it with:
python -m wspkit --config hatch-wsp.toml
Or drive a single call through the CLI:
wsp --config hatch-wsp.toml environment/create --name scratch --python-version 3.12
Walking through each endpoint
environment/list → hatch env show --json
Hatch's env show already accepts a --json flag and emits a JSON
object describing the configured environments. The result = "json"
mode tells wspkit to parse stdout as JSON and return it verbatim.
There's a wrinkle: Hatch's JSON shape is keyed by environment name
and includes Hatch-specific fields, while WSP wants an array of
Environment objects with WSP's field names. If the shape doesn't
match, the wspkit server's response will technically be valid JSON but
won't match the WSP schema. That's the boundary to understand:
- If Hatch's JSON happens to match WSP's (it doesn't, today) → ship the config above and you're done.
- If it doesn't → fall back to an
importhandler that callshatch env show --json, transforms the shape, and returns the WSP-formatted array. See "Where the boundary breaks" below.
environment/create → hatch env create
hatch env create scratch doesn't print the created environment's
metadata to stdout. So result = "json" would fail — wspkit would try
to parse an empty stdout as JSON and raise EXECUTION_FAILED.
result = "template" is the right tool: wspkit synthesizes the WSP
result from the request params themselves.
[handlers."environment/create"]
command = ["hatch", "env", "create", "{name}"]
result = "template"
template = { id = "{name}", name = "{name}", python_version = "{python_version}", interpreter_path = "", installed_packages = [], extra = {} }
When the user calls
wsp --config hatch-wsp.toml environment/create --name scratch --python-version 3.12:
- wspkit renders the argv:
hatch env create scratch. - Hatch runs and exits 0.
- wspkit synthesizes the result from the request params and the template:
{
"id": "scratch",
"name": "scratch",
"python_version": "3.12",
"interpreter_path": "",
"installed_packages": [],
"extra": {}
}
The interpreter_path is empty because we don't know it without
running another Hatch query. WSP's contract permits that — clients
that need the interpreter call environment/get afterwards.
environment/delete → hatch env remove
Same pattern as create: Hatch's env remove doesn't print
anything useful, so we synthesize a DeleteAck from the request:
[handlers."environment/delete"]
command = ["hatch", "env", "remove", "{id}"]
result = "template"
template = { id = "{id}" }
environment/execute → hatch run -e <env> -- <argv>
Hatch can run a command inside one of its environments via
hatch run -e <env> -- <command...>. WSP's environment/execute
takes an argv array; the {argv} token in command splats the
array into multiple argv positions:
[handlers."environment/execute"]
command = ["hatch", "run", "-e", "{id}", "--", "{argv}"]
result = "exec"
A WSP request with
{"id": "scratch", "argv": ["python", "-V"]} becomes:
hatch run -e scratch -- python -V
result = "exec" tells wspkit to capture the subprocess's exit code,
stdout, and stderr and return them as the WSP ExecuteResult. The
subprocess's exit code is the value the caller cares about; wspkit
does not treat a non-zero exit as a failure here.
Where the boundary breaks
The Config_File flow handles tools whose data model is already close to WSP's. It struggles when:
Field names disagree
WSP's Environment has python_version. Hatch's JSON likely has
python (or no version field at all on env show). Wspkit's
templating doesn't have rename/transform primitives.
Fix: an import handler that calls Hatch and renames fields:
[handlers."environment/list"]
import = "hatch_wsp.handlers.list_envs"
# hatch_wsp/handlers.py
import json, subprocess
def list_envs(_params):
proc = subprocess.run(
["hatch", "env", "show", "--json"],
capture_output=True,
check=True,
)
raw = json.loads(proc.stdout)
return [
{
"id": name,
"name": name,
"python_version": cfg.get("python", "unknown"),
}
for name, cfg in raw.items()
]
The result needs data the CLI doesn't print
environment/get is supposed to return installed_packages. Hatch
doesn't emit that on its own; you'd have to call
hatch run -e <env> -- pip list --format=json and parse the result.
That's two subprocess invocations and a transformation — beyond what
the Config_File flow can express.
Fix: an import handler that orchestrates the multi-step lookup.
Conditional flags
hatch env create scratch -p 3.12 only includes -p 3.12 when the
user actually supplied a Python version. The Config_File flow has
no notion of "include this flag conditionally."
Fix: an import handler that builds argv programmatically.
Mixing the two
You can mix command and import entries in the same Config_File:
# Easy methods get the subprocess flow.
[handlers."environment/create"]
command = ["hatch", "env", "create", "{name}"]
result = "template"
template = { id = "{name}", name = "{name}", python_version = "{python_version}", interpreter_path = "", installed_packages = [], extra = {} }
[handlers."environment/delete"]
command = ["hatch", "env", "remove", "{id}"]
result = "template"
template = { id = "{id}" }
[handlers."environment/execute"]
command = ["hatch", "run", "-e", "{id}", "--", "{argv}"]
result = "exec"
# The hard ones get a small Python adapter.
[handlers."environment/list"]
import = "hatch_wsp.handlers.list_envs"
[handlers."environment/get"]
import = "hatch_wsp.handlers.get_env"
This is usually the practical sweet spot: 80% of methods are expressible declaratively, the remainder go through a small Python module the tool author owns.