@sakanano-moe/agenthelm 6.7.0 → 6.11.0-rc.1
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.
- package/package.json +1 -1
- package/runtime/index.json +6 -6
- package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
- package/runtime/plugins/agent-pack/plugin.json +1 -1
- package/runtime/plugins/agent-pack/references/local-only.md +126 -0
- package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
- package/runtime/plugins/agent-pack/references/nudge.md +58 -0
- package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
- package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
- package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
- package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
- package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
- package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
- package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
- package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
- package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agents-init/content.json +17 -2
- package/runtime/plugins/agents-init/plugin.json +1 -1
- package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
- package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
- package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
- package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
- package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
- package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
- package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
- package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
- package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
- package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
- package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
- package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
- package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
- package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
- package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
- package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
- package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
- package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/content.json +30 -3
- package/runtime/plugins/project-orchestrator/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
- package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
- package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
- package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
- package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
- package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
- package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
- package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
- package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
- package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
- package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
- package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
- package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
- package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
- package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
- package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
- package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
- package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
- package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
- package/runtime/plugins/project-orchestrator/templates/CASE.en.md +39 -0
|
@@ -23,12 +23,13 @@ import tempfile
|
|
|
23
23
|
from datetime import UTC, datetime
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Any
|
|
26
|
+
from i18n_agent_pack import _ # noqa: E402
|
|
26
27
|
|
|
27
28
|
PLUGINS = ("agent-pack", "agents-init", "opinion-manager", "project-orchestrator")
|
|
28
29
|
SKIP_PARTS = {"tests", "__pycache__", ".DS_Store", "node_modules"}
|
|
29
30
|
MANIFEST = "runtime.json"
|
|
30
31
|
SHIM = """#!/bin/sh
|
|
31
|
-
# agenthelm
|
|
32
|
+
# agenthelm runtime {version} (generated by agenthelm runtime install). Needs only Python 3.11+, no AI client.
|
|
32
33
|
self=$0
|
|
33
34
|
while [ -L "$self" ]; do # a command link (--link-bin) points here; follow it to the runtime
|
|
34
35
|
link=$(readlink "$self")
|
|
@@ -42,15 +43,19 @@ for candidate in "${{AGENTHELM_PYTHON:-}}" python3 python; do
|
|
|
42
43
|
exec "$candidate" "$here/plugins/agent-pack/runtime/agenthelm.py" "$@"
|
|
43
44
|
fi
|
|
44
45
|
done
|
|
45
|
-
|
|
46
|
+
case "${{AGENTHELM_LANG:-${{LC_ALL:-${{LANG:-}}}}}}" in
|
|
47
|
+
zh*) echo "agenthelm 需要 Python 3.11 或更高版本:安装后重试,或设置 AGENTHELM_PYTHON。" >&2 ;;
|
|
48
|
+
*) echo "agenthelm needs Python 3.11 or newer: install it and retry, or set AGENTHELM_PYTHON." >&2 ;;
|
|
49
|
+
esac
|
|
46
50
|
exit 3
|
|
47
51
|
"""
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class RuntimeInstallError(Exception):
|
|
51
|
-
def __init__(self, message: str, next_steps: list[str] | None = None) -> None:
|
|
55
|
+
def __init__(self, message: str, next_steps: list[str] | None = None, *, download: bool = False) -> None:
|
|
52
56
|
super().__init__(message)
|
|
53
57
|
self.next_steps = next_steps or []
|
|
58
|
+
self.download = download
|
|
54
59
|
|
|
55
60
|
|
|
56
61
|
# ---------------------------------------------------------------- locations
|
|
@@ -110,12 +115,12 @@ def _shipped_files(root: Path) -> list[Path]:
|
|
|
110
115
|
|
|
111
116
|
def describe(root: Path) -> dict:
|
|
112
117
|
if not _is_release_root(root):
|
|
113
|
-
raise RuntimeInstallError(
|
|
114
|
-
["--source 指向仓库检出、解压后的发布包目录或 .tar.gz 发布包"])
|
|
118
|
+
raise RuntimeInstallError(_("{0} 不是 agenthelm 发布内容(缺少 index.json 或 plugins/<插件>)").format(root),
|
|
119
|
+
[_("--source 指向仓库检出、解压后的发布包目录或 .tar.gz 发布包")])
|
|
115
120
|
index = json.loads((root / "index.json").read_text(encoding="utf-8"))
|
|
116
121
|
version = str(index.get("plugin", {}).get("version") or "")
|
|
117
122
|
if not version:
|
|
118
|
-
raise RuntimeInstallError(
|
|
123
|
+
raise RuntimeInstallError(_("{0}/index.json 没有发布版本号").format(root))
|
|
119
124
|
digest = hashlib.sha256()
|
|
120
125
|
files = _shipped_files(root)
|
|
121
126
|
for relative in files:
|
|
@@ -157,6 +162,25 @@ def release_version(latest: dict | None) -> str | None:
|
|
|
157
162
|
return tag[1:] if isinstance(tag, str) and tag.startswith("v") else tag
|
|
158
163
|
|
|
159
164
|
|
|
165
|
+
def command_status() -> dict:
|
|
166
|
+
"""Which of the commands people type are on PATH right now (``wf`` is never installed by the runtime)."""
|
|
167
|
+
return {"agenthelm": shutil.which("agenthelm"), "wf": shutil.which("wf")}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def missing_text(data: dict, commands: dict) -> dict:
|
|
171
|
+
"""Wording for “no user-level runtime installed”: says which commands work, instead of leaving “wf: not found” unexplained."""
|
|
172
|
+
def state(name: str) -> str:
|
|
173
|
+
return _("{0} 在 PATH 上({1})").format(name, commands[name]) if commands.get(name) else _("终端里的 {0} 命令目前不可用").format(name)
|
|
174
|
+
return {
|
|
175
|
+
"reason": _("本次由 {0} 提供;{1},{2}。").format(data['running_from'], state('agenthelm'), state('wf')),
|
|
176
|
+
"impact": _("项目根目录的 ./menu 会通过客户端插件兜底,所以现在能用;"
|
|
177
|
+
"没有 AI 客户端或客户端插件被移除时打不开。wf 是单独的查找命令,运行时不会提供它:"
|
|
178
|
+
"用 ./menu find 词 或 agenthelm project find 词 代替"),
|
|
179
|
+
"label": _("安装运行时(agenthelm runtime install --apply,只写用户数据目录);"
|
|
180
|
+
"要在终端直接输入 agenthelm 时加 --link-bin ~/.local/bin(只链接 agenthelm,不含 wf)"),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
160
184
|
def report(env: Any, latest: dict | None = None) -> dict:
|
|
161
185
|
active = current(env)
|
|
162
186
|
newest = release_version(latest)
|
|
@@ -180,15 +204,15 @@ def plan(env: Any, root: Path, origin: str, *, link_bin: Path | None = None) ->
|
|
|
180
204
|
existing = _read_manifest(target)
|
|
181
205
|
active = current(env)
|
|
182
206
|
if existing and existing.get("digest") != info["digest"]:
|
|
183
|
-
raise RuntimeInstallError(
|
|
184
|
-
[
|
|
185
|
-
"确认要换掉时先手动移走该目录(例如移到 ~/.agent-pack/recovery/),再重新安装"])
|
|
207
|
+
raise RuntimeInstallError(_("运行时 {0} 已安装,但内容与来源不同(来源:{1})").format(info['version'], origin),
|
|
208
|
+
[_("已装:{0};同一版本不会被改写").format(target),
|
|
209
|
+
_("确认要换掉时先手动移走该目录(例如移到 ~/.agent-pack/recovery/),再重新安装")])
|
|
186
210
|
writes = []
|
|
187
211
|
if not existing:
|
|
188
|
-
writes.append({"path": str(target), "scope": "用户数据目录(新版本,旧版本保留)"})
|
|
212
|
+
writes.append({"path": str(target), "scope": _("用户数据目录(新版本,旧版本保留)")})
|
|
189
213
|
switch = not active or active.get("version") != info["version"]
|
|
190
214
|
if switch:
|
|
191
|
-
writes.append({"path": str(current_link(env)), "scope":
|
|
215
|
+
writes.append({"path": str(current_link(env)), "scope": _("切换当前版本 → {0}").format(info['version'])})
|
|
192
216
|
link = None
|
|
193
217
|
if link_bin is not None:
|
|
194
218
|
link = link_bin.expanduser() / "agenthelm"
|
|
@@ -196,9 +220,9 @@ def plan(env: Any, root: Path, origin: str, *, link_bin: Path | None = None) ->
|
|
|
196
220
|
if link.is_symlink() and Path(os.readlink(link)) == expected:
|
|
197
221
|
link = None
|
|
198
222
|
elif link.exists() or link.is_symlink():
|
|
199
|
-
raise RuntimeInstallError(
|
|
223
|
+
raise RuntimeInstallError(_("{0} 已存在且不是本运行时的链接,不会覆盖").format(link), [_("换一个 --link-bin 目录,或手动处理该文件")])
|
|
200
224
|
else:
|
|
201
|
-
writes.append({"path": str(link), "scope": "命令链接(指向当前运行时)"})
|
|
225
|
+
writes.append({"path": str(link), "scope": _("命令链接(指向当前运行时)")})
|
|
202
226
|
return {"version": info["version"], "plugins": info["plugins"], "digest": info["digest"], "source": origin,
|
|
203
227
|
"target": str(target), "install": not existing, "switch": switch,
|
|
204
228
|
"previous": active.get("version") if active else None, "link": str(link) if link else None,
|
|
@@ -218,7 +242,7 @@ def apply(env: Any, planned: dict, root: Path) -> dict:
|
|
|
218
242
|
shutil.copy2(root / relative, destination)
|
|
219
243
|
shutil.copy2(root / "index.json", staging / "index.json")
|
|
220
244
|
if describe(staging)["digest"] != planned["digest"]:
|
|
221
|
-
raise RuntimeInstallError("复制后的内容与来源摘要不一致,已放弃安装")
|
|
245
|
+
raise RuntimeInstallError(_("复制后的内容与来源摘要不一致,已放弃安装"))
|
|
222
246
|
shim = staging / "bin" / "agenthelm"
|
|
223
247
|
shim.parent.mkdir()
|
|
224
248
|
shim.write_text(SHIM.format(version=planned["version"]), encoding="utf-8")
|
|
@@ -241,8 +265,8 @@ def apply(env: Any, planned: dict, root: Path) -> dict:
|
|
|
241
265
|
shim = current_link(env) / "bin" / "agenthelm"
|
|
242
266
|
verified = bool(active and active["version"] == planned["version"] and os.access(shim, os.X_OK))
|
|
243
267
|
return {"ok": verified, "version": planned["version"], "current": active,
|
|
244
|
-
"verify": (
|
|
245
|
-
else "回读不一致:请运行 agenthelm runtime status 查看"),
|
|
268
|
+
"verify": (_("重新读取:当前运行时 {0},命令 {1}").format(planned['version'], shim) if verified
|
|
269
|
+
else _("回读不一致:请运行 agenthelm runtime status 查看")),
|
|
246
270
|
"rollback": f"agenthelm runtime use {planned['previous']} --apply" if planned["previous"]
|
|
247
271
|
and planned["previous"] != planned["version"] else None}
|
|
248
272
|
|
|
@@ -259,8 +283,8 @@ def _switch(env: Any, target: Path) -> None:
|
|
|
259
283
|
def use(env: Any, version: str, *, apply_change: bool) -> dict:
|
|
260
284
|
target = runtimes_dir(env) / version
|
|
261
285
|
if not _read_manifest(target):
|
|
262
|
-
versions = "、".join(item["version"] for item in installed(env)) or "(无)"
|
|
263
|
-
raise RuntimeInstallError(
|
|
286
|
+
versions = _("、").join(item["version"] for item in installed(env)) or _("(无)")
|
|
287
|
+
raise RuntimeInstallError(_("没有安装运行时 {0}").format(version), [_("已安装:{0}").format(versions)])
|
|
264
288
|
active = current(env)
|
|
265
289
|
result = {"version": version, "previous": active.get("version") if active else None, "applied": False}
|
|
266
290
|
if apply_change:
|
|
@@ -281,45 +305,45 @@ def resolve_source(host: Any, env: Any, args: argparse.Namespace, workdir: Path,
|
|
|
281
305
|
host._safe_extract(source, extracted)
|
|
282
306
|
roots = [path for path in extracted.iterdir() if path.is_dir()]
|
|
283
307
|
if len(roots) != 1:
|
|
284
|
-
raise RuntimeInstallError("发布包结构不符合预期")
|
|
285
|
-
return roots[0],
|
|
286
|
-
return source,
|
|
308
|
+
raise RuntimeInstallError(_("发布包结构不符合预期"))
|
|
309
|
+
return roots[0], _("本地发布包 {0}").format(source.name)
|
|
310
|
+
return source, _("本地目录 {0}").format(source)
|
|
287
311
|
bundled = bundled_root()
|
|
288
312
|
if bundled is not None and not prefer_release and not args.from_release:
|
|
289
|
-
return bundled,
|
|
313
|
+
return bundled, _("随附内容({0})").format(running_from(env))
|
|
290
314
|
try:
|
|
291
|
-
latest,
|
|
292
|
-
return host.release_source(latest, workdir), "GitHub Release(已核对摘要)"
|
|
315
|
+
latest, _origin = host.load_latest(env, args.latest, max_age=0)
|
|
316
|
+
return host.release_source(latest, workdir), _("GitHub Release(已核对摘要)")
|
|
293
317
|
except (host.CheckError, OSError, ValueError) as exc:
|
|
294
|
-
raise RuntimeInstallError(
|
|
295
|
-
["离线时用 --source 指向发布包(.tar.gz)或仓库检出",
|
|
296
|
-
"下载地址见 https://github.com/lc4t/agenthelm/releases"]) from exc
|
|
318
|
+
raise RuntimeInstallError(_("无法下载发布包:{0}").format(exc),
|
|
319
|
+
[_("离线时用 --source 指向发布包(.tar.gz)或仓库检出"),
|
|
320
|
+
_("下载地址见 https://github.com/lc4t/agenthelm/releases")], download=True) from exc
|
|
297
321
|
|
|
298
322
|
|
|
299
323
|
# ---------------------------------------------------------------- command line
|
|
300
324
|
|
|
301
325
|
def parser() -> argparse.ArgumentParser:
|
|
302
|
-
result = argparse.ArgumentParser(prog="agenthelm runtime", description=
|
|
326
|
+
result = argparse.ArgumentParser(prog="agenthelm runtime", description=_("agenthelm 用户级运行时:把四个插件的确定性工具装进用户数据目录,日常只需要 Python 3.11+。"))
|
|
303
327
|
sub = result.add_subparsers(dest="action", required=True)
|
|
304
|
-
for name, text in (("status", "当前运行时、已装版本与可用更新"),
|
|
305
|
-
("install", "安装运行时(默认用本命令随附的内容;默认预览)"),
|
|
306
|
-
("update", "联网安装最新发布版本并切换(默认预览)"),
|
|
307
|
-
("use", "切换到已安装的某个版本(默认预览)")):
|
|
328
|
+
for name, text in (("status", _("当前运行时、已装版本与可用更新")),
|
|
329
|
+
("install", _("安装运行时(默认用本命令随附的内容;默认预览)")),
|
|
330
|
+
("update", _("联网安装最新发布版本并切换(默认预览)")),
|
|
331
|
+
("use", _("切换到已安装的某个版本(默认预览)"))):
|
|
308
332
|
command = sub.add_parser(name, help=text)
|
|
309
333
|
command.add_argument("--output", choices=("text", "json"), default="text")
|
|
310
334
|
command.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
311
|
-
command.add_argument("--latest", help="latest.json 的 URL 或本地路径")
|
|
335
|
+
command.add_argument("--latest", help=_("latest.json 的 URL 或本地路径"))
|
|
312
336
|
if name == "use":
|
|
313
337
|
command.add_argument("version")
|
|
314
338
|
if name in ("install", "update"):
|
|
315
|
-
command.add_argument("--source", type=Path, help="本地目录或 .tar.gz 发布包(离线安装)")
|
|
316
|
-
command.add_argument("--link-bin", type=Path, help="在这个目录建 agenthelm 命令链接(例如 ~/.local/bin)")
|
|
339
|
+
command.add_argument("--source", type=Path, help=_("本地目录或 .tar.gz 发布包(离线安装)"))
|
|
340
|
+
command.add_argument("--link-bin", type=Path, help=_("在这个目录建 agenthelm 命令链接(例如 ~/.local/bin)"))
|
|
317
341
|
if name == "install":
|
|
318
|
-
command.add_argument("--from-release", action="store_true", help="联网下载发布包,而不用随附内容")
|
|
342
|
+
command.add_argument("--from-release", action="store_true", help=_("联网下载发布包,而不用随附内容"))
|
|
319
343
|
if name != "status":
|
|
320
344
|
command.add_argument("--apply", action="store_true")
|
|
321
345
|
if name == "status":
|
|
322
|
-
command.add_argument("--offline", action="store_true", help="只用缓存的版本信息")
|
|
346
|
+
command.add_argument("--offline", action="store_true", help=_("只用缓存的版本信息"))
|
|
323
347
|
return result
|
|
324
348
|
|
|
325
349
|
|
|
@@ -334,55 +358,55 @@ def main(argv: list[str], host: Any) -> int:
|
|
|
334
358
|
args = parser().parse_args(argv)
|
|
335
359
|
env = host.current_env(args.home)
|
|
336
360
|
if sys.platform == "win32":
|
|
337
|
-
print("agenthelm runtime 目前只支持 macOS 与 Linux。", file=sys.stderr)
|
|
361
|
+
print(_("agenthelm runtime 目前只支持 macOS 与 Linux。"), file=sys.stderr)
|
|
338
362
|
return 4
|
|
339
363
|
try:
|
|
340
364
|
if args.action == "status":
|
|
341
365
|
try:
|
|
342
|
-
latest,
|
|
366
|
+
latest, _origin = host.load_latest(env, args.latest, offline=args.offline)
|
|
343
367
|
except (host.CheckError, OSError, ValueError):
|
|
344
368
|
latest = None
|
|
345
369
|
data = report(env, latest)
|
|
346
370
|
active = data["current"]
|
|
347
|
-
lines = [
|
|
348
|
-
|
|
349
|
-
|
|
371
|
+
lines = [_("当前运行时:{0}({1})").format(active['version'], active['path']) if active else _("当前运行时:未安装"),
|
|
372
|
+
_("已安装:{0}").format(_('、').join(item['version'] for item in data['installed']) or _('(无)')),
|
|
373
|
+
_("本次命令来自:{0}").format(data['running_from'])]
|
|
350
374
|
if data["update_available"]:
|
|
351
|
-
lines.append(
|
|
375
|
+
lines.append(_("有新版本 {0}:agenthelm runtime update 预览").format(data['latest_release']))
|
|
352
376
|
if not active:
|
|
353
|
-
lines.append("安装:agenthelm runtime install(预览)→ 加 --apply 执行")
|
|
377
|
+
lines.append(_("安装:agenthelm runtime install(预览)→ 加 --apply 执行"))
|
|
354
378
|
_print({"ok": True, **data}, args.output, lines)
|
|
355
379
|
return 0
|
|
356
380
|
if args.action == "use":
|
|
357
381
|
result = use(env, args.version, apply_change=args.apply)
|
|
358
|
-
lines = [
|
|
382
|
+
lines = [_("{0}:{1} → {2}").format(_('已切换') if result['applied'] else _('将切换'), result['previous'], result['version'])]
|
|
359
383
|
if not args.apply:
|
|
360
|
-
lines.append("以上为预览;加 --apply 执行。")
|
|
384
|
+
lines.append(_("以上为预览;加 --apply 执行。"))
|
|
361
385
|
_print({"ok": True, **result}, args.output, lines)
|
|
362
386
|
return 0
|
|
363
387
|
with tempfile.TemporaryDirectory(prefix="agenthelm-runtime-") as temporary:
|
|
364
388
|
root, origin = resolve_source(host, env, args, Path(temporary), prefer_release=args.action == "update")
|
|
365
389
|
planned = plan(env, root, origin, link_bin=args.link_bin)
|
|
366
|
-
lines = [
|
|
367
|
-
lines += [
|
|
390
|
+
lines = [_("运行时 {0}(来源:{1})").format(planned['version'], origin)]
|
|
391
|
+
lines += [_(" 将写入:{0}({1})").format(item['path'], item['scope']) for item in planned["writes"]] or [_(" 已是当前版本,无需改动。")]
|
|
368
392
|
if not args.apply or planned["action"] == "none" and not planned["link"]:
|
|
369
393
|
if args.apply is False and planned["writes"]:
|
|
370
|
-
lines.append("以上为预览;加 --apply 执行。")
|
|
394
|
+
lines.append(_("以上为预览;加 --apply 执行。"))
|
|
371
395
|
_print({"ok": True, "applied": False, "plan": planned}, args.output, lines)
|
|
372
396
|
return 0
|
|
373
397
|
result = apply(env, planned, root)
|
|
374
398
|
lines.append((" " + result["verify"]))
|
|
375
399
|
if result["rollback"]:
|
|
376
|
-
lines.append(
|
|
400
|
+
lines.append(_(" 回到旧版本:{0}").format(result['rollback']))
|
|
377
401
|
if not os.environ.get("PATH", "").count(str(current_link(env) / "bin")) and not planned["link"]:
|
|
378
|
-
lines.append(
|
|
402
|
+
lines.append(_(" 直接用命令:把 {0} 加入 PATH,或重新运行并加 --link-bin ~/.local/bin").format(current_link(env) / 'bin'))
|
|
379
403
|
_print({"ok": result["ok"], "applied": True, "plan": planned, **result}, args.output, lines)
|
|
380
404
|
return 0 if result["ok"] else 6
|
|
381
405
|
except RuntimeInstallError as exc:
|
|
382
406
|
if args.output == "json":
|
|
383
407
|
print(json.dumps({"ok": False, "error": str(exc), "next": exc.next_steps}, ensure_ascii=False, indent=2))
|
|
384
408
|
else:
|
|
385
|
-
print(
|
|
409
|
+
print(_("出错:{0}").format(exc), file=sys.stderr)
|
|
386
410
|
for step in exc.next_steps:
|
|
387
411
|
print(f" · {step}", file=sys.stderr)
|
|
388
|
-
return 4 if
|
|
412
|
+
return 4 if exc.download else 2
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""运行时文案的多语言:源语言是中文,``_("中文原文")`` 在中文下原样返回,在英文下查 ``locale/en.json``。
|
|
2
|
+
|
|
3
|
+
语言按这个顺序决定:调用方指定(如 ``nudge.language``)→ 环境变量 ``AGENTHELM_LANG`` → 用户配置
|
|
4
|
+
``~/.config/agenthelm/config.json`` 的 ``language`` → 系统 locale(``LC_ALL``、``LC_MESSAGES``、``LANG``)→ 英文。
|
|
5
|
+
``auto``、空值和 C/POSIX 视为未指定;中文 locale 用中文,其他语言的 locale 暂用英文。
|
|
6
|
+
没有英文译文的文案原样输出(中文),测试守住译文完整。四个插件各带一份相同的本模块(模块名不同,以免
|
|
7
|
+
同一进程里互相覆盖),``tests/test_i18n_parity.py`` 守住它们行为一致。模块级常量里的文案用 ``N_`` 标记
|
|
8
|
+
(原样返回,仅供提取),使用处再调用 ``_``,这样语言在运行时而不是导入时决定。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Mapping
|
|
17
|
+
|
|
18
|
+
LANGUAGES = ("zh", "en")
|
|
19
|
+
DEFAULT_LANGUAGE = "en"
|
|
20
|
+
LOCALE_DIR = Path(__file__).resolve().parent / "locale"
|
|
21
|
+
_catalogs: dict[str, dict[str, str]] = {}
|
|
22
|
+
_configured: dict[str, tuple[tuple, str | None]] = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize(value: object) -> str | None:
|
|
26
|
+
text = str(value or "").strip().lower().replace("_", "-")
|
|
27
|
+
if not text or text in ("auto", "c", "posix") or text.startswith(("c.", "posix.")):
|
|
28
|
+
return None
|
|
29
|
+
return "zh" if text.startswith("zh") else "en"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def configured_language(environ: Mapping[str, str]) -> str | None:
|
|
33
|
+
"""用户配置里的 language;按文件修改时间缓存,读不了就当没有。"""
|
|
34
|
+
base = environ.get("XDG_CONFIG_HOME") or (str(Path(environ["HOME"]) / ".config") if environ.get("HOME") else "")
|
|
35
|
+
if not base:
|
|
36
|
+
return None
|
|
37
|
+
path = Path(base) / "agenthelm" / "config.json"
|
|
38
|
+
try:
|
|
39
|
+
info = path.stat()
|
|
40
|
+
signature = (info.st_mtime_ns, info.st_size)
|
|
41
|
+
cached = _configured.get(str(path))
|
|
42
|
+
if cached and cached[0] == signature:
|
|
43
|
+
return cached[1]
|
|
44
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
45
|
+
value = normalize(data.get("language")) if isinstance(data, dict) else None
|
|
46
|
+
except (OSError, ValueError):
|
|
47
|
+
return None
|
|
48
|
+
_configured[str(path)] = (signature, value)
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_language(requested: str | None = None, environ: Mapping[str, str] | None = None) -> str:
|
|
53
|
+
environ = os.environ if environ is None else environ
|
|
54
|
+
for candidate in (requested, environ.get("AGENTHELM_LANG"), configured_language(environ),
|
|
55
|
+
environ.get("LC_ALL"), environ.get("LC_MESSAGES"), environ.get("LANG")):
|
|
56
|
+
language = normalize(candidate)
|
|
57
|
+
if language:
|
|
58
|
+
return language
|
|
59
|
+
return DEFAULT_LANGUAGE
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def catalog(language: str) -> dict[str, str]:
|
|
63
|
+
if language not in _catalogs:
|
|
64
|
+
try:
|
|
65
|
+
data = json.loads((LOCALE_DIR / f"{language}.json").read_text(encoding="utf-8"))
|
|
66
|
+
except (OSError, ValueError):
|
|
67
|
+
data = {}
|
|
68
|
+
_catalogs[language] = data if isinstance(data, dict) else {}
|
|
69
|
+
return _catalogs[language]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _(message: str) -> str:
|
|
73
|
+
language = resolve_language()
|
|
74
|
+
return message if language == "zh" else catalog(language).get(message, message)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def N_(message: str) -> str:
|
|
78
|
+
return message
|