@sakanano-moe/agenthelm 6.8.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 +228 -147
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +64 -59
- 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 +4 -4
- 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 +26 -1
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +399 -80
- 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 +210 -76
- 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 +210 -62
- 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 +4 -0
- package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +2 -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 +222 -189
- package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +241 -204
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +145 -89
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +184 -105
- 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:
|
|
@@ -165,14 +170,14 @@ def command_status() -> dict:
|
|
|
165
170
|
def missing_text(data: dict, commands: dict) -> dict:
|
|
166
171
|
"""Wording for “no user-level runtime installed”: says which commands work, instead of leaving “wf: not found” unexplained."""
|
|
167
172
|
def state(name: str) -> str:
|
|
168
|
-
return
|
|
173
|
+
return _("{0} 在 PATH 上({1})").format(name, commands[name]) if commands.get(name) else _("终端里的 {0} 命令目前不可用").format(name)
|
|
169
174
|
return {
|
|
170
|
-
"reason":
|
|
171
|
-
"impact": "项目根目录的 ./menu 会通过客户端插件兜底,所以现在能用;"
|
|
175
|
+
"reason": _("本次由 {0} 提供;{1},{2}。").format(data['running_from'], state('agenthelm'), state('wf')),
|
|
176
|
+
"impact": _("项目根目录的 ./menu 会通过客户端插件兜底,所以现在能用;"
|
|
172
177
|
"没有 AI 客户端或客户端插件被移除时打不开。wf 是单独的查找命令,运行时不会提供它:"
|
|
173
|
-
"用 ./menu find 词 或 agenthelm project find 词 代替",
|
|
174
|
-
"label": "安装运行时(agenthelm runtime install --apply,只写用户数据目录);"
|
|
175
|
-
"要在终端直接输入 agenthelm 时加 --link-bin ~/.local/bin(只链接 agenthelm,不含 wf)",
|
|
178
|
+
"用 ./menu find 词 或 agenthelm project find 词 代替"),
|
|
179
|
+
"label": _("安装运行时(agenthelm runtime install --apply,只写用户数据目录);"
|
|
180
|
+
"要在终端直接输入 agenthelm 时加 --link-bin ~/.local/bin(只链接 agenthelm,不含 wf)"),
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
|
|
@@ -199,15 +204,15 @@ def plan(env: Any, root: Path, origin: str, *, link_bin: Path | None = None) ->
|
|
|
199
204
|
existing = _read_manifest(target)
|
|
200
205
|
active = current(env)
|
|
201
206
|
if existing and existing.get("digest") != info["digest"]:
|
|
202
|
-
raise RuntimeInstallError(
|
|
203
|
-
[
|
|
204
|
-
"确认要换掉时先手动移走该目录(例如移到 ~/.agent-pack/recovery/),再重新安装"])
|
|
207
|
+
raise RuntimeInstallError(_("运行时 {0} 已安装,但内容与来源不同(来源:{1})").format(info['version'], origin),
|
|
208
|
+
[_("已装:{0};同一版本不会被改写").format(target),
|
|
209
|
+
_("确认要换掉时先手动移走该目录(例如移到 ~/.agent-pack/recovery/),再重新安装")])
|
|
205
210
|
writes = []
|
|
206
211
|
if not existing:
|
|
207
|
-
writes.append({"path": str(target), "scope": "用户数据目录(新版本,旧版本保留)"})
|
|
212
|
+
writes.append({"path": str(target), "scope": _("用户数据目录(新版本,旧版本保留)")})
|
|
208
213
|
switch = not active or active.get("version") != info["version"]
|
|
209
214
|
if switch:
|
|
210
|
-
writes.append({"path": str(current_link(env)), "scope":
|
|
215
|
+
writes.append({"path": str(current_link(env)), "scope": _("切换当前版本 → {0}").format(info['version'])})
|
|
211
216
|
link = None
|
|
212
217
|
if link_bin is not None:
|
|
213
218
|
link = link_bin.expanduser() / "agenthelm"
|
|
@@ -215,9 +220,9 @@ def plan(env: Any, root: Path, origin: str, *, link_bin: Path | None = None) ->
|
|
|
215
220
|
if link.is_symlink() and Path(os.readlink(link)) == expected:
|
|
216
221
|
link = None
|
|
217
222
|
elif link.exists() or link.is_symlink():
|
|
218
|
-
raise RuntimeInstallError(
|
|
223
|
+
raise RuntimeInstallError(_("{0} 已存在且不是本运行时的链接,不会覆盖").format(link), [_("换一个 --link-bin 目录,或手动处理该文件")])
|
|
219
224
|
else:
|
|
220
|
-
writes.append({"path": str(link), "scope": "命令链接(指向当前运行时)"})
|
|
225
|
+
writes.append({"path": str(link), "scope": _("命令链接(指向当前运行时)")})
|
|
221
226
|
return {"version": info["version"], "plugins": info["plugins"], "digest": info["digest"], "source": origin,
|
|
222
227
|
"target": str(target), "install": not existing, "switch": switch,
|
|
223
228
|
"previous": active.get("version") if active else None, "link": str(link) if link else None,
|
|
@@ -237,7 +242,7 @@ def apply(env: Any, planned: dict, root: Path) -> dict:
|
|
|
237
242
|
shutil.copy2(root / relative, destination)
|
|
238
243
|
shutil.copy2(root / "index.json", staging / "index.json")
|
|
239
244
|
if describe(staging)["digest"] != planned["digest"]:
|
|
240
|
-
raise RuntimeInstallError("复制后的内容与来源摘要不一致,已放弃安装")
|
|
245
|
+
raise RuntimeInstallError(_("复制后的内容与来源摘要不一致,已放弃安装"))
|
|
241
246
|
shim = staging / "bin" / "agenthelm"
|
|
242
247
|
shim.parent.mkdir()
|
|
243
248
|
shim.write_text(SHIM.format(version=planned["version"]), encoding="utf-8")
|
|
@@ -260,8 +265,8 @@ def apply(env: Any, planned: dict, root: Path) -> dict:
|
|
|
260
265
|
shim = current_link(env) / "bin" / "agenthelm"
|
|
261
266
|
verified = bool(active and active["version"] == planned["version"] and os.access(shim, os.X_OK))
|
|
262
267
|
return {"ok": verified, "version": planned["version"], "current": active,
|
|
263
|
-
"verify": (
|
|
264
|
-
else "回读不一致:请运行 agenthelm runtime status 查看"),
|
|
268
|
+
"verify": (_("重新读取:当前运行时 {0},命令 {1}").format(planned['version'], shim) if verified
|
|
269
|
+
else _("回读不一致:请运行 agenthelm runtime status 查看")),
|
|
265
270
|
"rollback": f"agenthelm runtime use {planned['previous']} --apply" if planned["previous"]
|
|
266
271
|
and planned["previous"] != planned["version"] else None}
|
|
267
272
|
|
|
@@ -278,8 +283,8 @@ def _switch(env: Any, target: Path) -> None:
|
|
|
278
283
|
def use(env: Any, version: str, *, apply_change: bool) -> dict:
|
|
279
284
|
target = runtimes_dir(env) / version
|
|
280
285
|
if not _read_manifest(target):
|
|
281
|
-
versions = "、".join(item["version"] for item in installed(env)) or "(无)"
|
|
282
|
-
raise RuntimeInstallError(
|
|
286
|
+
versions = _("、").join(item["version"] for item in installed(env)) or _("(无)")
|
|
287
|
+
raise RuntimeInstallError(_("没有安装运行时 {0}").format(version), [_("已安装:{0}").format(versions)])
|
|
283
288
|
active = current(env)
|
|
284
289
|
result = {"version": version, "previous": active.get("version") if active else None, "applied": False}
|
|
285
290
|
if apply_change:
|
|
@@ -300,45 +305,45 @@ def resolve_source(host: Any, env: Any, args: argparse.Namespace, workdir: Path,
|
|
|
300
305
|
host._safe_extract(source, extracted)
|
|
301
306
|
roots = [path for path in extracted.iterdir() if path.is_dir()]
|
|
302
307
|
if len(roots) != 1:
|
|
303
|
-
raise RuntimeInstallError("发布包结构不符合预期")
|
|
304
|
-
return roots[0],
|
|
305
|
-
return source,
|
|
308
|
+
raise RuntimeInstallError(_("发布包结构不符合预期"))
|
|
309
|
+
return roots[0], _("本地发布包 {0}").format(source.name)
|
|
310
|
+
return source, _("本地目录 {0}").format(source)
|
|
306
311
|
bundled = bundled_root()
|
|
307
312
|
if bundled is not None and not prefer_release and not args.from_release:
|
|
308
|
-
return bundled,
|
|
313
|
+
return bundled, _("随附内容({0})").format(running_from(env))
|
|
309
314
|
try:
|
|
310
|
-
latest,
|
|
311
|
-
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(已核对摘要)")
|
|
312
317
|
except (host.CheckError, OSError, ValueError) as exc:
|
|
313
|
-
raise RuntimeInstallError(
|
|
314
|
-
["离线时用 --source 指向发布包(.tar.gz)或仓库检出",
|
|
315
|
-
"下载地址见 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
|
|
316
321
|
|
|
317
322
|
|
|
318
323
|
# ---------------------------------------------------------------- command line
|
|
319
324
|
|
|
320
325
|
def parser() -> argparse.ArgumentParser:
|
|
321
|
-
result = argparse.ArgumentParser(prog="agenthelm runtime", description=
|
|
326
|
+
result = argparse.ArgumentParser(prog="agenthelm runtime", description=_("agenthelm 用户级运行时:把四个插件的确定性工具装进用户数据目录,日常只需要 Python 3.11+。"))
|
|
322
327
|
sub = result.add_subparsers(dest="action", required=True)
|
|
323
|
-
for name, text in (("status", "当前运行时、已装版本与可用更新"),
|
|
324
|
-
("install", "安装运行时(默认用本命令随附的内容;默认预览)"),
|
|
325
|
-
("update", "联网安装最新发布版本并切换(默认预览)"),
|
|
326
|
-
("use", "切换到已安装的某个版本(默认预览)")):
|
|
328
|
+
for name, text in (("status", _("当前运行时、已装版本与可用更新")),
|
|
329
|
+
("install", _("安装运行时(默认用本命令随附的内容;默认预览)")),
|
|
330
|
+
("update", _("联网安装最新发布版本并切换(默认预览)")),
|
|
331
|
+
("use", _("切换到已安装的某个版本(默认预览)"))):
|
|
327
332
|
command = sub.add_parser(name, help=text)
|
|
328
333
|
command.add_argument("--output", choices=("text", "json"), default="text")
|
|
329
334
|
command.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
330
|
-
command.add_argument("--latest", help="latest.json 的 URL 或本地路径")
|
|
335
|
+
command.add_argument("--latest", help=_("latest.json 的 URL 或本地路径"))
|
|
331
336
|
if name == "use":
|
|
332
337
|
command.add_argument("version")
|
|
333
338
|
if name in ("install", "update"):
|
|
334
|
-
command.add_argument("--source", type=Path, help="本地目录或 .tar.gz 发布包(离线安装)")
|
|
335
|
-
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)"))
|
|
336
341
|
if name == "install":
|
|
337
|
-
command.add_argument("--from-release", action="store_true", help="联网下载发布包,而不用随附内容")
|
|
342
|
+
command.add_argument("--from-release", action="store_true", help=_("联网下载发布包,而不用随附内容"))
|
|
338
343
|
if name != "status":
|
|
339
344
|
command.add_argument("--apply", action="store_true")
|
|
340
345
|
if name == "status":
|
|
341
|
-
command.add_argument("--offline", action="store_true", help="只用缓存的版本信息")
|
|
346
|
+
command.add_argument("--offline", action="store_true", help=_("只用缓存的版本信息"))
|
|
342
347
|
return result
|
|
343
348
|
|
|
344
349
|
|
|
@@ -353,55 +358,55 @@ def main(argv: list[str], host: Any) -> int:
|
|
|
353
358
|
args = parser().parse_args(argv)
|
|
354
359
|
env = host.current_env(args.home)
|
|
355
360
|
if sys.platform == "win32":
|
|
356
|
-
print("agenthelm runtime 目前只支持 macOS 与 Linux。", file=sys.stderr)
|
|
361
|
+
print(_("agenthelm runtime 目前只支持 macOS 与 Linux。"), file=sys.stderr)
|
|
357
362
|
return 4
|
|
358
363
|
try:
|
|
359
364
|
if args.action == "status":
|
|
360
365
|
try:
|
|
361
|
-
latest,
|
|
366
|
+
latest, _origin = host.load_latest(env, args.latest, offline=args.offline)
|
|
362
367
|
except (host.CheckError, OSError, ValueError):
|
|
363
368
|
latest = None
|
|
364
369
|
data = report(env, latest)
|
|
365
370
|
active = data["current"]
|
|
366
|
-
lines = [
|
|
367
|
-
|
|
368
|
-
|
|
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'])]
|
|
369
374
|
if data["update_available"]:
|
|
370
|
-
lines.append(
|
|
375
|
+
lines.append(_("有新版本 {0}:agenthelm runtime update 预览").format(data['latest_release']))
|
|
371
376
|
if not active:
|
|
372
|
-
lines.append("安装:agenthelm runtime install(预览)→ 加 --apply 执行")
|
|
377
|
+
lines.append(_("安装:agenthelm runtime install(预览)→ 加 --apply 执行"))
|
|
373
378
|
_print({"ok": True, **data}, args.output, lines)
|
|
374
379
|
return 0
|
|
375
380
|
if args.action == "use":
|
|
376
381
|
result = use(env, args.version, apply_change=args.apply)
|
|
377
|
-
lines = [
|
|
382
|
+
lines = [_("{0}:{1} → {2}").format(_('已切换') if result['applied'] else _('将切换'), result['previous'], result['version'])]
|
|
378
383
|
if not args.apply:
|
|
379
|
-
lines.append("以上为预览;加 --apply 执行。")
|
|
384
|
+
lines.append(_("以上为预览;加 --apply 执行。"))
|
|
380
385
|
_print({"ok": True, **result}, args.output, lines)
|
|
381
386
|
return 0
|
|
382
387
|
with tempfile.TemporaryDirectory(prefix="agenthelm-runtime-") as temporary:
|
|
383
388
|
root, origin = resolve_source(host, env, args, Path(temporary), prefer_release=args.action == "update")
|
|
384
389
|
planned = plan(env, root, origin, link_bin=args.link_bin)
|
|
385
|
-
lines = [
|
|
386
|
-
lines += [
|
|
390
|
+
lines = [_("运行时 {0}(来源:{1})").format(planned['version'], origin)]
|
|
391
|
+
lines += [_(" 将写入:{0}({1})").format(item['path'], item['scope']) for item in planned["writes"]] or [_(" 已是当前版本,无需改动。")]
|
|
387
392
|
if not args.apply or planned["action"] == "none" and not planned["link"]:
|
|
388
393
|
if args.apply is False and planned["writes"]:
|
|
389
|
-
lines.append("以上为预览;加 --apply 执行。")
|
|
394
|
+
lines.append(_("以上为预览;加 --apply 执行。"))
|
|
390
395
|
_print({"ok": True, "applied": False, "plan": planned}, args.output, lines)
|
|
391
396
|
return 0
|
|
392
397
|
result = apply(env, planned, root)
|
|
393
398
|
lines.append((" " + result["verify"]))
|
|
394
399
|
if result["rollback"]:
|
|
395
|
-
lines.append(
|
|
400
|
+
lines.append(_(" 回到旧版本:{0}").format(result['rollback']))
|
|
396
401
|
if not os.environ.get("PATH", "").count(str(current_link(env) / "bin")) and not planned["link"]:
|
|
397
|
-
lines.append(
|
|
402
|
+
lines.append(_(" 直接用命令:把 {0} 加入 PATH,或重新运行并加 --link-bin ~/.local/bin").format(current_link(env) / 'bin'))
|
|
398
403
|
_print({"ok": result["ok"], "applied": True, "plan": planned, **result}, args.output, lines)
|
|
399
404
|
return 0 if result["ok"] else 6
|
|
400
405
|
except RuntimeInstallError as exc:
|
|
401
406
|
if args.output == "json":
|
|
402
407
|
print(json.dumps({"ok": False, "error": str(exc), "next": exc.next_steps}, ensure_ascii=False, indent=2))
|
|
403
408
|
else:
|
|
404
|
-
print(
|
|
409
|
+
print(_("出错:{0}").format(exc), file=sys.stderr)
|
|
405
410
|
for step in exc.next_steps:
|
|
406
411
|
print(f" · {step}", file=sys.stderr)
|
|
407
|
-
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
|