Use clak to build fixed-process business tools. Business code creates its own Clak instance and registers Script subclasses; the global clak command contains only built-in commands and cannot run business scripts.
Installation
Running and testing this skill's examples all require the following dependencies:
python -m pip install esclak pytest
Production projects that do not run pytest can install only esclak; however, the commands in the "Testing" and "Pre-commit Checks" sections of this document require installing pytest。
Skill Built-in Reference Implementations
This skill is self-contained with copyable reference code. Read the paths relative to this file as needed:
references/dev_tools/README.md: complete usage and run commandsreferences/dev_tools/app.py: app, status bar, and footer registrationreferences/dev_tools/__main__.py: project-level CLI entry pointreferences/dev_tools/scripts/select_res_server.py: filtering selection and shared statereferences/dev_tools/scripts/pack_upload.py: multi-step orchestration, subprocesses, conditional steps, and resource cleanupreferences/dev_tools/scripts/verify_release.py: business failures and exit codesreferences/testing.py:ScriptTesterandScriptRuntest fixtures
Do not import the reference project as a dependency. Copy its structure and patterns into the business project; the target project only needs to install the esclak package to use this skill.
Minimal Project Structure
my_tools/
├── __init__.py
├── __main__.py
├── app.py
└── scripts/
├── __init__.py
└── deploy.py
# app.py
from clak import Clak
from .scripts.deploy import DeployScript
app = Clak(status="发布工具 · staging")
app.register(DeployScript)
# __main__.py
from .app import app
app.run_cli(prog="my-tools")
The business entry point must be:
python -m my_tools
python -m my_tools exec /deploy
Do not run clak exec /deploy, as it will not load scripts registered by the business project.
Script Conventions
Each script run creates a new instance.run takes no arguments; the framework binds the session in on_enter(session) on completion, failure, or cancellation. on_exit()。
from clak import Script
class DeployScript(Script):
name = "/deploy"
description = "发布服务"
def run(self) -> None:
def deploy() -> int:
self.output.push("发布完成")
return 0
self.add_step(True, deploy, "deploy")
self.run_steps()
namemust be a slash command, such as/deployrun(self)defines and orchestrates the steps for this executionself.add_step(can_do, task, name)adds steps,can_dois a boolean or a no-argument predicate function- Steps return
0to continue; non-0means waiting for input and then resuming, not a business failure - End
self.run_steps()withrun; do not directly manipulate theSessionexecution queue self.output.push()automatically adds the[script-name]prefix- Throw
ScriptError(message, exit_code=1)for invalid argument formatsValueError
Collecting Input
Input steps return self.ask_tool(request, on_answer); the answer callback saves business state; the next step continues after the input is submitted.
from clak import AskAnswer, AskChoice, AskMode, AskRequest
def ask_environment() -> int:
def save(answer: AskAnswer) -> None:
state.environment = answer.primary
self.output.push(f"环境:{state.environment}")
return self.ask_tool(
AskRequest(
field_id="environment",
prompt="选择环境",
default="staging",
choices=(
AskChoice(label="预发", value="staging"),
AskChoice(label="生产", value="production"),
),
mode=AskMode.SINGLE,
),
save,
)
AskRequest.default is esclak a required constructor parameter ofin the current API, and must be passed explicitly even if the business does not wish to provide a usable default, for example default="". Only non-empty defaults can be automatically adopted by the headless CLI; empty defaults fail just like missing input.
The actual input rules for the headless CLI are as follows:
| Command Form | Behavior for Unprovided ask Fields |
|---|---|
Without any --set, such as python -m my_tools exec /deploy |
Automatically adopt each field's non-empty AskRequest.default. When no usable default exists, the exit code is 2。 |
With --set environment=staging |
That field uses the explicit value. If there are still unprovided fields, the default valuewill notbe automatically adopted; the exit code for missing fields is 2。 |
With --set ... --use-defaults |
Explicit --set takes precedence; the remaining fields use their respective non-empty default values. |
Therefore, when designing headless batch processing, declare defaults for safe default paths; for fields that must be confirmed by the caller, pass default="", and use --set to provide it.--use-defaults is not "skipping validation": the framework will still validate the choice, and the answer callback should still validate the business format.
field_idis the--set field_id=valuekey for headless modeAskMode.SINGLEsingle-select,MULTImulti-select,FILTERfilterable select- Only
AskChoice(value=None)allows single-select fields to accept text outside the candidates; the callback must still validate the business format - Headless run:
python -m my_tools exec /deploy --set environment=staging
Resources, state, and error boundaries
- Temporary directories, file handles, and external resources are created in
on_enterand cleaned up inon_exit; cleanup must cover completion, failure, and cancellation - In-process state across scripts goes into an explicit
context.py, and providereset()for test isolation - For invoking external commands use
subprocess.run(..., check=False, timeout=...), converting startup exceptions and non-zero exit codes intoScriptError - Validate paths, user input, and permission parameters first; do not mistakenly write a failure exit code as a step hang
Testing
Prefer using ScriptTester to verify the script lifecycle, waiting for input, and the success and failure paths.
from clak import ScriptTester
from my_tools.scripts.deploy import DeployScript
def test_deploy_asks_then_completes():
run = ScriptTester(DeployScript).run()
assert run.is_awaiting("environment")
run.provide("environment", "staging")
assert run.succeeded
assert run.output.contains("发布完成")
Each script should cover at least:
- The success path with default or complete input
- The behavior when each required input is missing
- Invalid candidates or format errors return exit code
2 ScriptErrorBusiness failures return the expected exit code- When there are resources, cleanup is performed on completion, failure, and cancellation
- Execution and skipping of conditional steps
Finally run:
python -m pytest
python -m my_tools exec /deploy --set environment=staging
Pre-commit checks
- The business entry point, not the global
clakcommand, is runnable run(self)has calledself.run_steps()- The output contains locatable business information
- The exit codes for headless success, business failure, and parameter failure are correct respectively
- TUI manual testing of search, input, cancellation, and dynamic state or footer