1. CLI-Anything 不是又一个命令行工具,而是一套可编程的 CLI 构建范式
你有没有试过写一个 Python 脚本,跑完之后得手动复制输出、粘贴进 Excel、再改个文件名、最后发邮件给同事?或者更糟——把同样的逻辑硬编码进三个不同项目里,每次改 bug 都得同步三份?我干过。三年前在一家做数据中台的公司,团队里有七个人维护着二十多个零散的 CLI 小工具:sync-db,gen-report,clean-cache,mock-api……它们用的不是同一套参数解析器,有的用argparse,有的用fire,有的甚至直接sys.argv[1:]硬切;帮助文档格式五花八门,有的写在 README 里,有的藏在--help里,还有的靠口头传授;最要命的是——没人敢动gen-report的核心逻辑,因为没人知道它依赖哪个版本的pandas,也不知道--format=csv和--format=excel在底层到底调用了哪两个函数。
直到我把所有这些脚本全删了,用click重写了第一个统一入口:cli-anywhere。注意,不是cli-anything——那是后来开源时才定的名字。当时我就意识到,问题从来不在“要不要写 CLI”,而在于“怎么让 CLI 不再是临时胶水,而是可组合、可继承、可测试的一等公民”。CLI-Anything 正是这个认知落地后的产物:它不提供开箱即用的功能(比如“一键部署”或“自动爬虫”),它提供的是构建任意 CLI 的骨架、契约与扩展机制。它的核心关键词不是click,而是command registry、context injection、plugin manifest和help composition。它解决的不是“如何执行一个命令”,而是“如何让一百个命令共享同一套生命周期管理、配置加载、错误处理和文档生成逻辑”。
这解释了为什么你在热搜词里反复看到codex cli、claude cli、minimax code cli——它们全是具体功能型 CLI,而 CLI-Anything 是让它们能被快速、一致、可持续地构建出来的底层框架。它和click的关系,就像 React 和 DOM API 的关系:click是基础砖块,CLI-Anything 是预制好的户型模块+水电图纸+承重墙标准。你不用从@click.command()开始写,而是从@cli.command()开始,后者自动注入日志上下文、自动加载.env、自动绑定--verbose全局开关、自动生成--help的子命令树结构。这不是语法糖,是工程约束力。
提示:CLI-Anything 的设计哲学第一条就是“拒绝隐式依赖”。它不自动扫描
commands/目录,也不靠文件名约定加载插件。每个命令必须显式注册,每个插件必须声明requires = ["click>=8.0", "requests>=2.28"]。这看起来多了一步,但当你在 CI 中看到pip install -e .失败时,能立刻定位到是auth-plugin没声明对pydantic的依赖,而不是在凌晨三点翻三页日志找ImportError: cannot import name 'BaseModel'。
2. 为什么不用 argparse 或 fire?CLI-Anything 的三层抽象设计
很多人第一反应是:“argparse不够用?fire不香?”——这问题我被问过至少四十七次。答案不是“不够”,而是“不适合规模化协作”。让我用真实场景拆解这三层抽象:
2.1 第一层:命令注册与发现机制(解决“谁在提供命令”)
argparse没有注册中心。你写parser.add_argument(),它就存在;删掉那行,它就消失。没有元数据,没有依赖声明,没有启用/禁用开关。fire更激进,它直接把模块属性当命令,python mytool.py upload --file data.csv实际调用的是mytool.upload()。问题在于:如果upload()函数内部调用了另一个未声明的validate_file(),而这个函数又依赖openpyxl,那么pip install mytool后upload命令会直接报错,但--help里根本看不到这个依赖。
CLI-Anything 强制要求命令通过装饰器注册:
# commands/upload.py from cli_anything import command, option @command( name="upload", help="Upload files to remote storage", requires=["boto3>=1.26", "openpyxl>=3.1"] ) @option("--file", "-f", required=True, type=str, help="Path to file") @option("--bucket", "-b", default="default-bucket", help="Target S3 bucket") def upload(file: str, bucket: str): # 实际逻辑 pass关键点在于requires字段。CLI-Anything 在启动时会解析所有@command装饰器,收集全部requires列表,然后调用pip check验证环境完整性。如果缺失openpyxl,它不会等到upload()执行时才崩溃,而是在cli-anywhere --help阶段就提示:
⚠️ Command 'upload' requires missing packages: - openpyxl>=3.1 (not installed) Run 'pip install openpyxl>=3.1' to enable this command.这彻底改变了调试节奏——从“运行时报错→查源码→装包→重试”变成“首次查看帮助→按提示装包→立即可用”。
2.2 第二层:上下文注入与生命周期管理(解决“命令之间如何共享状态”)
fire把函数当黑盒执行,argparse的parse_args()返回一个Namespace对象,但你得自己把它传给每个函数。结果就是:日志配置重复写三次,数据库连接对象在每个命令里都sqlite3.connect()一遍,配置文件路径硬编码在五个地方。
CLI-Anything 定义了标准上下文协议:
# context.py from typing import Optional, Dict, Any import logging class CLIContext: def __init__(self, verbose: bool = False): self.verbose = verbose self.logger = logging.getLogger("cli-anywhere") self.config: Dict[str, Any] = {} self.db_conn = None def load_config(self, path: str): # 统一配置加载逻辑 pass def get_db_connection(self) -> sqlite3.Connection: if not self.db_conn: self.db_conn = sqlite3.connect(self.config.get("db_path", ":memory:")) return self.db_conn然后所有命令自动接收该上下文:
@command(name="list-users") def list_users(ctx: CLIContext): # ctx 自动注入 conn = ctx.get_db_connection() cursor = conn.execute("SELECT * FROM users") for row in cursor.fetchall(): print(row)更关键的是,CLI-Anything 支持上下文继承链。主命令cli-anywhere初始化CLIContext(verbose=True),子命令cli-anywhere db migrate会收到同一个实例,而cli-anywhere db migrate --dry-run可以在子命令中修改ctx.dry_run = True,父命令依然保持原状态。这种细粒度控制在argparse里需要手动传递args对象,在fire里根本不可控。
2.3 第三层:帮助系统与文档合成(解决“用户怎么知道命令怎么用”)
argparse的--help是静态字符串,fire的帮助是函数 docstring 的简单渲染。但真实 CLI 需要动态帮助:比如--format选项的可选值取决于当前安装的插件(json,yaml,xlsx),--target的补全列表来自远程 API。CLI-Anything 将帮助生成拆分为三阶段:
- 静态定义:装饰器中的
help=字符串; - 动态补充:命令执行前调用
get_help_extras()方法; - 合成渲染:统一模板引擎(Jinja2)生成最终文本。
例如sync-db命令:
@command(name="sync-db") def sync_db(ctx: CLIContext): pass def get_help_extras(): # 动态获取可用目标 targets = [] for plugin in ctx.plugins: if hasattr(plugin, "get_sync_targets"): targets.extend(plugin.get_sync_targets()) return { "available_targets": ", ".join(targets), "example_usage": f"cli-anywhere sync-db --target {targets[0] if targets else 'local'}" }当用户执行cli-anywhere sync-db --help,CLI-Anything 会自动调用get_help_extras(),将返回字典注入帮助模板,最终输出:
Sync database to target environment. Available targets: local, staging, production, aws-rds-us-east-1 Example usage: cli-anywhere sync-db --target staging这解决了argparse帮助文档过期、fire帮助无法反映运行时状态的根本缺陷。
3. 从零搭建你的第一个 CLI-Anything 项目:实操步骤与避坑清单
现在我们动手创建一个最小可行 CLI:weather-cli,它能查询城市天气并支持插件扩展(比如未来加一个“发送邮件提醒”插件)。整个过程严格遵循 CLI-Anything 的工程约束,我会标出每一步背后的原理和常见陷阱。
3.1 初始化项目结构与依赖声明
首先创建标准 Python 包结构:
mkdir weather-cli cd weather-cli python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install --upgrade pip关键点来了:不要直接pip install click。CLI-Anything 要求所有依赖通过pyproject.toml声明,且必须区分dependencies和optional-dependencies。创建pyproject.toml:
[build-system] requires = ["setuptools>=45", "wheel"] build-backend = "setuptools.build_meta" [project] name = "weather-cli" version = "0.1.0" description = "A CLI for weather queries" requires-python = ">=3.8" dependencies = [ "click>=8.0", "requests>=2.28", "cli-anything>=0.5.0", # 注意:这是核心框架 ] [project.optional-dependencies] email = ["smtplib", "email-validator"] # 插件依赖,不默认安装注意:
cli-anything本身不包含任何业务逻辑,它只是一个框架。你必须显式声明dependencies中的cli-anything,否则@command装饰器无法识别。很多新手在这里卡住,以为click装了就能用 CLI-Anything,结果运行cli-anywhere --help报ModuleNotFoundError: No module named 'cli_anything'。
3.2 编写主入口与基础命令
创建weather_cli/__init__.py(空文件,仅作包标识)和weather_cli/cli.py:
# weather_cli/cli.py from cli_anything import CLIApp, command, option import requests # 初始化应用实例 app = CLIApp( name="weather-cli", version="0.1.0", description="Get current weather for any city" ) @command( name="current", help="Show current weather for a city", requires=["requests>=2.28"] ) @option("--city", "-c", required=True, type=str, help="City name (e.g., Beijing)") @option("--units", "-u", default="metric", type=str, help="Temperature units: metric|imperial|kelvin") def current(city: str, units: str): """Fetch and display current weather.""" try: # 使用 CLI-Anything 提供的内置 HTTP 客户端(自动带超时和重试) response = app.http.get( "https://api.openweathermap.org/data/2.5/weather", params={"q": city, "appid": "YOUR_API_KEY", "units": units} ) data = response.json() print(f"Weather in {city}: {data['weather'][0]['description']}") print(f"Temperature: {data['main']['temp']}°C") except requests.exceptions.RequestException as e: app.error(f"Failed to fetch weather: {e}") # 注册命令到应用 app.register_command(current)重点解析:
app.http.get()不是requests.get()的简单封装。CLI-Anything 的http客户端内置了:- 默认 10 秒超时(可全局配置);
- 3 次指数退避重试(网络抖动时自动重试);
- 自动添加
User-Agent头(避免被 API 拒绝); - 错误统一转为
CLIError(便于上层捕获)。
app.error()是标准化错误输出。它会:- 以红色字体打印错误消息;
- 如果
--verbose开启,追加完整 traceback; - 退出码设为 1(符合 Unix 传统)。
3.3 创建可安装的 CLI 入口点
在pyproject.toml中添加入口点配置:
[project.entry-points."console_scripts"] weather = "weather_cli.cli:app.run"这告诉pip install -e .把weather命令映射到weather_cli.cli.app.run()。注意app.run()是 CLI-Anything 提供的主循环方法,它会:
- 解析命令行参数;
- 加载所有已注册命令;
- 验证依赖完整性;
- 注入上下文;
- 调用目标命令。
3.4 安装与验证:踩过的坑与解决方案
执行安装:
pip install -e .此时可能遇到的第一个坑:
ERROR: Could not find a version that satisfies the requirement cli-anything>=0.5.0原因:cli-anything尚未发布到 PyPI。解决方案是先安装开发版:
pip install git+https://github.com/cli-anything/cli-anything.git@main第二个坑出现在运行时:
weather current --city Beijing # Error: Missing API key. Set WEATHER_API_KEY environment variable.这是 CLI-Anything 的安全设计:敏感配置(如 API Key)必须通过环境变量或配置文件提供,绝不允许硬编码。修复方式:
export WEATHER_API_KEY="your_actual_key_here" weather current --city Beijing第三个坑是帮助文档不显示:
weather --help # Shows only basic usage, no 'current' command listed原因:app.register_command(current)必须在app.run()之前执行,且current函数必须在app实例创建之后定义。检查cli.py文件顺序,确保app = CLIApp(...)在最顶部,@command装饰器在中间,app.register_command()在底部。
3.5 添加插件支持:让 CLI 可扩展
现在我们添加一个邮件插件。创建weather_cli/plugins/email_plugin.py:
# weather_cli/plugins/email_plugin.py from cli_anything import plugin, option import smtplib from email.mime.text import MIMEText @plugin( name="email-alert", description="Send weather report via email", requires=["smtplib", "email-validator"] ) @option("--to", "-t", required=True, type=str, help="Recipient email address") @option("--subject", "-s", default="Weather Report", help="Email subject") def send_email(to: str, subject: str, ctx): """Send current weather as email.""" # 从上下文中获取最近一次查询结果(演示上下文共享) if not hasattr(ctx, "last_weather"): raise ValueError("No weather data available. Run 'weather current' first.") msg = MIMEText(f"Weather: {ctx.last_weather['description']}\nTemp: {ctx.last_weather['temp']}°C") msg["Subject"] = subject msg["From"] = "weather@cli" msg["To"] = to with smtplib.SMTP("localhost") as server: server.send_message(msg) print(f"Email sent to {to}")然后在cli.py中启用插件:
# 在 app = CLIApp(...) 之后添加 app.load_plugins_from_package("weather_cli.plugins")关键点:load_plugins_from_package()会扫描指定包下的所有模块,自动发现@plugin装饰的函数,并将其注册为子命令。用户执行weather email-alert --to user@example.com时,CLI-Anything 会:
- 验证
smtplib是否已安装(因requires声明); - 将
send_email注册为email-alert子命令; - 在
weather --help中显示该命令。
注意:插件模块必须放在
plugins/子目录下,且__init__.py文件不能为空(需包含from .email_plugin import *或类似导入)。否则load_plugins_from_package()扫描不到模块。
4. CLI-Anything 的真实生产约束:为什么它能在企业级项目中存活三年
我在上一家公司推动 CLI-Anything 落地时,CTO 提出三个尖锐问题:“它比click多出的 200 行代码,能带来什么不可替代的价值?”、“如果团队新人不理解上下文注入,会不会写出更难维护的代码?”、“当我们要对接内部认证系统时,它能否无缝集成?”——这些问题的答案,构成了 CLI-Anything 在生产环境存活三年的核心约束。以下是我用血泪经验总结的四条铁律:
4.1 铁律一:所有命令必须可独立测试,且测试覆盖率强制 ≥85%
CLI-Anything 不提供测试工具,但它定义了测试契约。每个@command函数必须能脱离 CLI 环境单独调用。看current命令的测试用例:
# tests/test_current.py import pytest from weather_cli.cli import current from unittest.mock import patch, MagicMock def test_current_success(): # 模拟 HTTP 响应 mock_response = MagicMock() mock_response.json.return_value = { "weather": [{"description": "clear sky"}], "main": {"temp": 25.5} } with patch("weather_cli.cli.app.http.get", return_value=mock_response): # 直接调用函数,不经过 CLI 解析 result = current(city="Beijing", units="metric") assert result is None # 命令无返回值,只打印输出 def test_current_failure(): with patch("weather_cli.cli.app.http.get") as mock_get: mock_get.side_effect = Exception("Network error") with pytest.raises(Exception, match="Network error"): current(city="Beijing", units="metric")关键点:测试不依赖click.testing.CliRunner,因为 CLI-Anything 的命令本质是普通函数。current(city="Beijing", units="metric")和cli-anywhere current --city Beijing --units metric应该产生相同副作用(打印、HTTP 请求)。这使得单元测试速度极快(毫秒级),且能精准定位问题在业务逻辑还是 CLI 层。
实战教训:曾有个团队把数据库连接逻辑写在
@command函数内部,导致测试时每次都要启动 SQLite 内存库。后来我们强制要求:所有外部依赖(DB、HTTP、FS)必须通过ctx参数注入,测试时传入 Mock 对象即可。这条规则让平均测试执行时间从 12 秒降到 0.3 秒。
4.2 铁律二:配置必须分层,且禁止跨层覆盖
CLI-Anything 定义三级配置优先级:
- 环境变量(最高优先级):
WEATHER_API_KEY; - 用户配置文件(中优先级):
~/.weather-cli/config.toml; - 默认值(最低优先级):
@option(default="metric")。
但严禁环境变量覆盖用户配置文件的结构。例如,用户配置文件定义:
# ~/.weather-cli/config.toml [api] timeout = 30 retry = 5 [output] format = "json"环境变量只能覆盖叶子节点:WEATHER_API_TIMEOUT=60有效,但WEATHER_API='{"timeout":60}'无效——CLI-Anything 会忽略整个字符串,因为它无法解析为 TOML 结构。这防止了配置混乱:运维人员设置环境变量时,不会意外破坏开发者的本地配置结构。
4.3 铁律三:错误必须分类,且每类错误对应唯一退出码
CLI-Anything 内置错误类型体系:
CLIError(退出码 1):用户输入错误(如--city缺失);ConfigError(退出码 2):配置文件损坏或缺失;NetworkError(退出码 3):HTTP 请求失败;PluginError(退出码 4):插件加载失败。
每个错误类型都有标准消息格式:
❌ ConfigError: Failed to load ~/.weather-cli/config.toml Reason: Invalid TOML syntax at line 5, column 3 Hint: Run 'weather config init' to generate a valid template这种结构化错误让自动化脚本能精准判断失败原因。例如 CI 流程可以这样处理:
if ! weather current --city Tokyo; then case $? in 1) echo "User error - fix command args"; exit 1;; 2) echo "Config issue - regenerate config"; weather config init;; 3) echo "Network outage - retry later"; exit 0;; *) echo "Unknown error"; exit 1;; esac fi4.4 铁律四:插件必须声明能力契约,而非仅依赖声明
requires=["smtplib"]只保证包存在,但不保证功能可用。CLI-Anything 要求插件实现get_capabilities()方法:
# weather_cli/plugins/email_plugin.py def get_capabilities(): return { "email_provider": "smtp", "max_recipients": 100, "supports_attachments": False }主应用在加载插件后,会调用此方法并缓存结果。当用户执行weather email-alert --to group@company.com(150 个收件人)时,CLI-Anything 会在调用send_email()前检查:
if len(recipients) > plugin.capabilities["max_recipients"]: app.error(f"Too many recipients. Plugin supports max {plugin.capabilities['max_recipients']}")这比单纯检查smtplib是否安装更进一步——它验证了插件的实际服务能力。我们在对接内部邮件网关时,正是靠这个机制避免了因收件人数量超限导致的批量邮件失败。
5. CLI-Anything 与生态工具的协同:如何不重复造轮子
CLI-Anything 不是一个封闭王国,它刻意设计为与现有生态工具共生。以下是它与四个关键工具的真实协同模式,附带配置片段和效果对比。
5.1 与 Click 的关系:CLI-Anything 是 Click 的“企业级封装”
CLI-Anything 底层完全基于click,但它隐藏了click的复杂性。看一个典型对比:
纯 Click 写法(易出错):
import click @click.group() @click.option('--verbose', is_flag=True, help='Enable verbose output') @click.pass_context def cli(ctx, verbose): ctx.ensure_object(dict) ctx.obj['verbose'] = verbose @cli.command() @click.option('--city', required=True) @click.pass_context def current(ctx, city): if ctx.obj['verbose']: print("Debug: fetching weather...") # ...业务逻辑问题:@click.pass_context必须在每层装饰器中显式传递;ctx.obj是弱类型字典,IDE 无法提示;错误处理需手动raise click.UsageError()。
CLI-Anything 写法(类型安全):
from cli_anything import command, option @command(name="current", help="Show current weather") @option("--city", "-c", required=True, type=str) def current(city: str, ctx): # ctx 是 CLIContext 类型,IDE 可提示 ctx.logger, ctx.config if ctx.verbose: ctx.logger.debug("Fetching weather...") # 自动日志 # ...业务逻辑CLI-Anything 的ctx是强类型对象,ctx.verbose是布尔值,ctx.logger是logging.Logger实例。这带来的收益是:VS Code 中按Ctrl+Space能看到所有可用属性,类型检查器(如 mypy)能捕获ctx.nonexistent_attr错误。
5.2 与 Poetry 的协同:依赖管理的黄金搭档
Poetry 是现代 Python 项目的事实标准。CLI-Anything 与 Poetry 的集成体现在pyproject.toml的tool.poetry部分:
[tool.poetry] name = "weather-cli" version = "0.1.0" description = "Weather CLI built with CLI-Anything" authors = ["Your Name <you@example.com>"] [tool.poetry.dependencies] python = "^3.8" click = "^8.1" cli-anything = "^0.5.0" requests = "^2.28" [tool.poetry.group.dev.dependencies] pytest = "^7.0" pytest-cov = "^4.0" [tool.poetry.group.plugin.dependencies] smtplib = "^1.0" # 插件依赖单独分组 [[tool.poetry.source]] name = "internal-pypi" url = "https://pypi.internal.company.com/simple/" priority = "explicit"关键优势:Poetry 的poetry install会自动处理optional-dependencies,而 CLI-Anything 的app.load_plugins_from_package()会根据当前安装的依赖动态启用/禁用插件。例如:
poetry install # 只安装 core 依赖 poetry install --with plugin # 安装 email 插件依赖运行weather --help时,CLI-Anything 会检测smtplib是否可用,决定是否显示email-alert命令。这实现了真正的“按需加载”,避免了传统 CLI 中“插件存在但无法用”的尴尬。
5.3 与 VS Code 的深度整合:开发体验优化
CLI-Anything 提供 VS Code 调试配置模板。在项目根目录创建.vscode/launch.json:
{ "version": "0.2.0", "configurations": [ { "name": "Debug current weather", "type": "python", "request": "launch", "module": "weather_cli.cli", "args": ["current", "--city", "Beijing", "--verbose"], "console": "integratedTerminal", "justMyCode": true, "env": { "WEATHER_API_KEY": "test_key_123" } } ] }配合 CLI-Anything 的--debug标志(自动启用pdb),开发者能:
- 在
current()函数内设断点; - 查看
ctx对象的所有属性; - 实时修改
ctx.config并观察后续命令行为。
这比click.testing.CliRunner.invoke()的调试体验好得多——后者需要在测试代码中模拟参数,而 CLI-Anything 允许直接调试真实命令流。
5.4 与 GitHub Actions 的 CI/CD 流水线:保障 CLI 质量
我们为 CLI-Anything 项目定制了 GitHub Actions 工作流,核心检查项:
# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.8, 3.9, 3.10] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install poetry poetry install - name: Run unit tests run: poetry run pytest tests/ --cov=weather_cli --cov-report=term-missing - name: Validate CLI help output run: | # 检查 help 文本是否包含所有注册命令 weather --help | grep -q "current" weather --help | grep -q "email-alert" || echo "email-alert command missing" - name: Check for uncommitted changes run: | # 确保 pyproject.toml 中的 version 与 __version__ 一致 python -c "import weather_cli; assert weather_cli.__version__ == '0.1.0'"其中Validate CLI help output步骤至关重要。它确保每次 PR 合并前,--help输出与实际命令集一致。我们曾因忘记app.register_command()导致新命令上线后用户查不到帮助,这条检查现在成了质量红线。
6. CLI-Anything 的边界与演进:它不适合做什么,以及未来方向
坦白说,CLI-Anything 不是万能钥匙。它在某些场景下会成为累赘,而它的演进也始终围绕“让 CLI 成为可靠基础设施”这一核心,而非追逐热点。以下是明确的边界声明和路线图。
6.1 明确的不适用场景:什么时候该放弃 CLI-Anything
场景一:单次脚本,生命周期 < 1 周
如果你写一个脚本只为临时处理某次数据迁移,且确定未来不会再用,那么#!/usr/bin/env python3+argparse是最优解。CLI-Anything 的项目结构(pyproject.toml、包目录、插件机制)会增加 5 分钟 setup 时间,而收益为零。我的经验法则:脚本预期使用次数 < 3 次,或维护者 < 1 人,就别用 CLI-Anything。
场景二:需要 GUI 交互的 CLI
CLI-Anything 严格遵循 Unix 哲学:输入 → 处理 → 输出。它不提供dialog弹窗、rich进度条或inquirer交互式提问。如果你的需求是“让用户选择菜单项”,应该用rich或questionary单独实现,然后作为 CLI-Anything 命令的内部逻辑调用。CLI-Anything 的立场是:“CLI 是管道,不是界面”。
场景三:实时流式输出(如 tail -f)
CLI-Anything 的命令执行模型是“同步完成”。它不支持async def命令(尽管底层click支持),因为异步会破坏上下文注入的确定性。如果你需要weather stream --city Tokyo持续推送更新,正确做法是:
- 用
asyncio写一个独立的stream.py模块; - 在 CLI-Anything 命令中调用
subprocess.run(["python", "stream.py", "--city", "Tokyo"]); - 让 CLI-Anything 负责参数解析和错误包装,让
stream.py负责异步逻辑。
6.2 当前核心演进方向:稳定性 > 新功能
CLI-Anything 的 GitHub Issues 中,92% 是 bug 报告和文档请求,仅 8% 是新功能建议。团队明确聚焦三个方向:
方向一:Windows 兼容性加固node_modules\@opencode\cli\bin\opencode.exe 与你运行的 windows 版本不兼容这类错误在 Windows 用户中高频出现。CLI-Anything 正在重构二进制打包流程,采用pyinstaller生成真正跨平台的weather.exe,并内置 Windows SDK 版本检测。目标:让pip install weather-cli后,weather.exe在 Win7+ 上 100% 可运行。
方向二:配置文件 Schema 验证
用户常因 TOML 语法错误导致 CLI 启动失败。CLI-Anything 将集成pydantic,为~/.weather-cli/config.toml定义严格 Schema:
from pydantic import BaseModel, HttpUrl class Config(BaseModel): api: dict output: dict cache: dict # 自动生成验证错误提示 # "Error: config.toml invalid. Field 'api.timeout' must be integer, got '30s'"方向三:插件市场协议标准化
目前插件发现依赖包内路径。CLI-Anything 正在设计cli-plugin.json协议,允许插件作者发布独立包:
{ "name": "weather-email-plugin", "version": "0.1.0", "cli-anything-version": ">=0.5.0", "commands": ["email-alert"], "requires": ["smtplib"] }用户执行weather plugin install weather-email-plugin时,CLI-Anything 会:
- 从 PyPI 下载包;
- 验证
cli-plugin.json兼容性; - 自动安装依赖;
- 注册命令。
这将终结“插件安装后不显示”的历史难题。
6.3 我的个人体会:CLI-Anything 是写给未来自己的情书
最后分享一个真实故事。去年我离职前,把weather-cli交接给新人。他第一天就问我:“为什么current命令里要调用app.http.get()而不是requests.get()?” 我没直接回答,而是让他看tests/test_current.py里的patch("weather_cli.cli.app.http.get")。他愣了几秒,然后笑了:“哦,这样测试就不用 mock requests 了。”
那一刻我意识到,CLI-Anything 最大的价值不是它省了多少行代码,而是它把工程决策固化为代码契约。app.http.get()不是技术选择,是“所有 HTTP 请求必须可 mock”的承诺;ctx.logger不是便利,是“所有日志必须可配置级别”的约束;@plugin装饰器不是语法糖,是“扩展必须声明能力”的宣言。
它不讨好当下,它服务未来。当你在深夜修复一个三年前写的 CLI 命令时,看到def current(city: str, ctx: CLIContext)这行签名,你就知道——那个过去的自己,