@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""agenthelm:在 Claude Code、Codex、Cursor 间安装、检查与更新 agenthelm 插件。
|
|
3
3
|
|
|
4
4
|
``project`` 打开不需要 AI Agent 的项目自助入口(由 project-orchestrator 实现);``runtime``
|
|
5
|
-
|
|
5
|
+
管理用户级运行时;``nudge`` 是各客户端 hook 调用的观点候选采集与未决事项提示(见 references/nudge.md)。``check`` 只读:比较各客户端已安装的插件版本、项目里由模板渲染的文件,
|
|
6
6
|
以及 Opinion 来源与云端 ``latest.json``。``install``、``update``、``migrate``
|
|
7
7
|
默认只预览,``--apply`` 才执行。通过 ``npx @sakanano-moe/agenthelm <命令>`` 或
|
|
8
8
|
``python3 <agent-pack>/runtime/agenthelm.py <命令>`` 运行。``--hook`` 供会话开始时静默调用:
|
|
@@ -28,6 +28,8 @@ from dataclasses import dataclass, field
|
|
|
28
28
|
from datetime import UTC, datetime
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any
|
|
31
|
+
from i18n_agent_pack import _ # noqa: E402
|
|
32
|
+
from i18n_agent_pack import _ # noqa: E402
|
|
31
33
|
|
|
32
34
|
MARKETPLACE = "agenthelm"
|
|
33
35
|
CLI = "npx @sakanano-moe/agenthelm"
|
|
@@ -174,14 +176,14 @@ def load_latest(env: Env, source: str | None = None, *, offline: bool = False,
|
|
|
174
176
|
if cached and (offline or time.time() - cached.get("fetched_at", 0) < max_age):
|
|
175
177
|
return cached["latest"], {"source": cached.get("url"), "cached": True, "fetched_at": cached.get("fetched_at")}
|
|
176
178
|
if offline:
|
|
177
|
-
raise CheckError("离线且没有缓存的 latest.json")
|
|
179
|
+
raise CheckError(_("离线且没有缓存的 latest.json"))
|
|
178
180
|
failure_file = env.cache_dir / "last_failure.json"
|
|
179
181
|
if backoff:
|
|
180
182
|
try:
|
|
181
183
|
if time.time() - read_json(failure_file).get("at", 0) < FAILURE_BACKOFF_SECONDS:
|
|
182
184
|
if cached:
|
|
183
185
|
return cached["latest"], {"source": cached.get("url"), "cached": True, "stale": True}
|
|
184
|
-
raise CheckError("最近联网失败,稍后再试")
|
|
186
|
+
raise CheckError(_("最近联网失败,稍后再试"))
|
|
185
187
|
except (OSError, ValueError, AttributeError):
|
|
186
188
|
pass
|
|
187
189
|
errors = []
|
|
@@ -203,7 +205,7 @@ def load_latest(env: Env, source: str | None = None, *, offline: bool = False,
|
|
|
203
205
|
if cached:
|
|
204
206
|
return cached["latest"], {"source": cached.get("url"), "cached": True, "stale": True,
|
|
205
207
|
"fetched_at": cached.get("fetched_at")}
|
|
206
|
-
raise CheckError("无法获取 latest.json:" + "; ".join(errors))
|
|
208
|
+
raise CheckError(_("无法获取 latest.json:") + "; ".join(errors))
|
|
207
209
|
|
|
208
210
|
|
|
209
211
|
# ---------------------------------------------------------------- installed plugins
|
|
@@ -222,7 +224,7 @@ def installed_plugins(env: Env, names: set[str]) -> list[dict]:
|
|
|
222
224
|
except (OSError, ValueError, AttributeError):
|
|
223
225
|
record = {}
|
|
224
226
|
for key, entries in sorted(record.items()):
|
|
225
|
-
plugin,
|
|
227
|
+
plugin, _sep, market = key.partition("@")
|
|
226
228
|
if market != MARKETPLACE or plugin not in names:
|
|
227
229
|
continue
|
|
228
230
|
for entry in entries:
|
|
@@ -250,14 +252,18 @@ def installed_plugins(env: Env, names: set[str]) -> list[dict]:
|
|
|
250
252
|
|
|
251
253
|
|
|
252
254
|
def migrations_between(latest: dict, plugin: str, current: str, target: str) -> list[dict]:
|
|
253
|
-
steps
|
|
255
|
+
"""The steps from ``current`` to ``target``; when two entries end at the same version (a span kept for the
|
|
256
|
+
release gate and the single steps inside it) the narrower one, with the newer ``from``, is shown."""
|
|
257
|
+
steps: dict[str, dict] = {}
|
|
254
258
|
for entry in latest.get("migrations", []):
|
|
255
259
|
if entry.get("plugin") != plugin:
|
|
256
260
|
continue
|
|
257
261
|
to = str(entry.get("to"))
|
|
258
262
|
if is_newer(to, current) and not is_newer(to, target):
|
|
259
|
-
steps.
|
|
260
|
-
|
|
263
|
+
kept = steps.get(to)
|
|
264
|
+
if kept is None or is_newer(str(entry.get("from")), str(kept.get("from"))):
|
|
265
|
+
steps[to] = entry
|
|
266
|
+
return sorted(steps.values(), key=lambda entry: version_key(str(entry.get("to"))) or (0,))
|
|
261
267
|
|
|
262
268
|
|
|
263
269
|
def plugin_findings(latest: dict, installed: list[dict]) -> list[dict]:
|
|
@@ -301,7 +307,7 @@ def content_findings(project: Path, latest: dict) -> list[dict]:
|
|
|
301
307
|
try:
|
|
302
308
|
lock = read_json(lock_path)
|
|
303
309
|
except (OSError, ValueError) as exc:
|
|
304
|
-
return [{"pack": None, "status": "error", "error":
|
|
310
|
+
return [{"pack": None, "status": "error", "error": _("{0} 无法解析:{1}").format(CONTENT_LOCK, exc)}]
|
|
305
311
|
findings = []
|
|
306
312
|
for pack_id, entry in sorted(lock.get("packs", {}).items()):
|
|
307
313
|
plugin = entry.get("plugin") or pack_id.split("/", 1)[0]
|
|
@@ -342,11 +348,11 @@ def opinion_findings(project: Path, installed: list[dict]) -> list[dict]:
|
|
|
342
348
|
return []
|
|
343
349
|
manager = locate_companion("opinion-manager", installed)
|
|
344
350
|
if manager is None:
|
|
345
|
-
return [{"status": "unknown", "error": "未找到已安装的 opinion-manager"}]
|
|
351
|
+
return [{"status": "unknown", "error": _("未找到已安装的 opinion-manager")}]
|
|
346
352
|
try:
|
|
347
353
|
profile = read_json(lock_path)["profile"]
|
|
348
354
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
349
|
-
return [{"status": "error", "error":
|
|
355
|
+
return [{"status": "error", "error": _("{0} 无法解析:{1}").format(OPINION_LOCK, exc)}]
|
|
350
356
|
with tempfile.TemporaryDirectory() as temporary:
|
|
351
357
|
# macOS temporary paths live under /var, a symlink opinion-manager refuses.
|
|
352
358
|
profile_file = Path(temporary).resolve() / "profile.json"
|
|
@@ -361,7 +367,7 @@ def opinion_findings(project: Path, installed: list[dict]) -> list[dict]:
|
|
|
361
367
|
except ValueError:
|
|
362
368
|
return [{"status": "error", "error": (completed.stderr or completed.stdout).strip()[:300]}]
|
|
363
369
|
if not result.get("ok", False):
|
|
364
|
-
return [{"status": "error", "error": result.get("error", "opinion-manager updates 失败")}]
|
|
370
|
+
return [{"status": "error", "error": result.get("error", _("opinion-manager updates 失败"))}]
|
|
365
371
|
return [{"status": "update-available" if result["updates"] else "current", "updates": result["updates"]}]
|
|
366
372
|
|
|
367
373
|
|
|
@@ -408,7 +414,7 @@ def scaffold_report(env: Env, project: Path | None, *, reprompt: bool = False) -
|
|
|
408
414
|
base = {"available": False, "root": str(root), "findings": [], "skipped": [], "error": ""}
|
|
409
415
|
module = scaffold_module(env)
|
|
410
416
|
if module is None:
|
|
411
|
-
return {**base, "error": "已安装的 agents-init 没有脚手架检查(更新 agents-init 后再检查)"}
|
|
417
|
+
return {**base, "error": _("已安装的 agents-init 没有脚手架检查(更新 agents-init 后再检查)")}
|
|
412
418
|
try:
|
|
413
419
|
return {**module.check(root, state_dir=env.state_dir, reprompt=reprompt), "available": True,
|
|
414
420
|
"root": str(root), "error": ""}
|
|
@@ -416,12 +422,16 @@ def scaffold_report(env: Env, project: Path | None, *, reprompt: bool = False) -
|
|
|
416
422
|
return {**base, "error": str(exc)}
|
|
417
423
|
|
|
418
424
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
+
def _local_only() -> Any:
|
|
426
|
+
"""local_only 模块(与本文件同目录,只用标准库)。"""
|
|
427
|
+
if str(RUNTIME_DIR) not in sys.path:
|
|
428
|
+
sys.path.insert(0, str(RUNTIME_DIR))
|
|
429
|
+
import local_only # noqa: PLC0415 - shipped next to this file
|
|
430
|
+
|
|
431
|
+
return local_only
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
SYNC_SERVICES = tuple((name, pattern) for _identifier, name, pattern in _local_only().SYNC_SERVICES)
|
|
425
435
|
REBUILDABLE_DIRS = {"node_modules", ".venv", "venv", "__pycache__", ".pytest_cache", ".cache", "dist", "build", "target",
|
|
426
436
|
".tox", ".next", ".gradle"}
|
|
427
437
|
|
|
@@ -445,7 +455,7 @@ def sync_folder(root: Path) -> dict | None:
|
|
|
445
455
|
return {"service": service, "path": str(resolved), "rebuildable": sorted(found)}
|
|
446
456
|
|
|
447
457
|
|
|
448
|
-
def machine_findings(env: Env, project: Path | None, *, reprompt: bool = False) -> dict:
|
|
458
|
+
def machine_findings(env: Env, project: Path | None, *, reprompt: bool = False, quick: bool = False) -> dict:
|
|
449
459
|
"""Findings about this machine only (no user-level runtime ...); their decisions live in the user state directory."""
|
|
450
460
|
root = profile_root(project)
|
|
451
461
|
result: dict = {"findings": [], "skipped": []}
|
|
@@ -458,19 +468,29 @@ def machine_findings(env: Env, project: Path | None, *, reprompt: bool = False)
|
|
|
458
468
|
if data["current"] is None:
|
|
459
469
|
text = agenthelm_runtime.missing_text(data, agenthelm_runtime.command_status())
|
|
460
470
|
findings.append({"id": "local:runtime-missing", "kind": "runtime-missing", "level": "suggest", "scope": "machine",
|
|
461
|
-
"title": "这台机器还没有安装用户级运行时", "reason": text["reason"], "impact": text["impact"],
|
|
471
|
+
"title": _("这台机器还没有安装用户级运行时"), "reason": text["reason"], "impact": text["impact"],
|
|
462
472
|
"fingerprint": "runtime-missing", "fix": "install-runtime", "next": text["label"]})
|
|
463
473
|
where = sync_folder(root)
|
|
464
474
|
if where is not None:
|
|
465
|
-
extra =
|
|
475
|
+
extra = _(";其中有可重建目录:{0}").format(_('、').join(where['rebuildable'][:6])) if where["rebuildable"] else ""
|
|
466
476
|
findings.append({"id": "local:sync-folder", "kind": "sync-folder", "level": "suggest", "scope": "machine",
|
|
467
|
-
"title":
|
|
477
|
+
"title": _("项目位于同步盘({0})").format(where['service']),
|
|
468
478
|
"reason": f"{where['path']}{extra}",
|
|
469
|
-
"impact": "多台机器共用同一份项目时:项目文件随同步盘走,但运行时、缓存与 wf 的台账是每台机器各一份;"
|
|
470
|
-
"可重建目录(依赖、构建产物)同步很慢,也容易产生冲突副本",
|
|
479
|
+
"impact": _("多台机器共用同一份项目时:项目文件随同步盘走,但运行时、缓存与 wf 的台账是每台机器各一份;"
|
|
480
|
+
"可重建目录(依赖、构建产物)同步很慢,也容易产生冲突副本"),
|
|
471
481
|
"fingerprint": sha256_text(where["service"]), "fix": "sync-folder",
|
|
472
|
-
"next": "运行 agenthelm fix sync-folder 查看指引(只给方案,不改文件、不删内容);不需要就 fix --skip local:sync-folder",
|
|
482
|
+
"next": _("运行 agenthelm fix sync-folder 查看指引(只给方案,不改文件、不删内容);不需要就 fix --skip local:sync-folder"),
|
|
473
483
|
"sync": where})
|
|
484
|
+
# local-only:同步盘里的本机专属目录、外置链接、HOME、体积……没有同步服务也没有配置时不扫描。
|
|
485
|
+
try:
|
|
486
|
+
engine = _local_only().check(root, home=env.home, environ=env.variables, mode="quick" if quick else "standard",
|
|
487
|
+
user_config=env.config_file)
|
|
488
|
+
findings.extend(engine["findings"])
|
|
489
|
+
except Exception as exc: # noqa: BLE001 - 本机目录检查出错不能拖垮插件与脚手架检查
|
|
490
|
+
findings.append({"id": "local:local-only-error", "kind": "error", "level": "suggest", "scope": "machine",
|
|
491
|
+
"title": _("无法检查本机专属目录"), "reason": str(exc), "impact": _("只影响同步盘相关提示"),
|
|
492
|
+
"fingerprint": sha256_text(str(exc)), "fix": None, "attention": False, "options": [],
|
|
493
|
+
"next": _("运行 agenthelm local-only check 查看详情")})
|
|
474
494
|
module = scaffold_module(env)
|
|
475
495
|
if module is None or not findings:
|
|
476
496
|
result["findings"] = findings
|
|
@@ -480,6 +500,30 @@ def machine_findings(env: Env, project: Path | None, *, reprompt: bool = False)
|
|
|
480
500
|
return result
|
|
481
501
|
|
|
482
502
|
|
|
503
|
+
def local_only_report(env: Env, project: Path | None, *, mode: str = "full", reprompt: bool = False) -> dict:
|
|
504
|
+
"""local-only 的完整检查结果(供 ``agenthelm local-only check`` 与 MCP 工具),已套用这台机器上记录的决定。"""
|
|
505
|
+
module = _local_only()
|
|
506
|
+
base = (project or Path.cwd()).resolve()
|
|
507
|
+
root = profile_root(base) or base
|
|
508
|
+
report = module.check(root, home=env.home, environ=env.variables, mode=mode, user_config=env.config_file)
|
|
509
|
+
report["skipped"] = []
|
|
510
|
+
scaffold = scaffold_module(env)
|
|
511
|
+
if scaffold is not None and report["findings"]:
|
|
512
|
+
decisions = scaffold.load_decisions(root, env.state_dir)
|
|
513
|
+
report["findings"], skipped = scaffold.partition(report["findings"], decisions, reprompt=reprompt)
|
|
514
|
+
report["skipped"] = [{"id": item["id"], "decision": item["decision"]} for item in skipped]
|
|
515
|
+
report["summary"] = {"findings": len(report["findings"]),
|
|
516
|
+
"attention": sum(1 for item in report["findings"] if item.get("attention"))}
|
|
517
|
+
return report
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def local_only_plan(env: Env, project: Path | None, **options: Any) -> dict:
|
|
521
|
+
"""``agenthelm local-only fix`` 的预览(不写任何东西);原因不能处理时抛出 LocalOnlyError。"""
|
|
522
|
+
base = (project or Path.cwd()).resolve()
|
|
523
|
+
return _local_only().plan_fix(profile_root(base) or base, home=env.home, environ=env.variables,
|
|
524
|
+
user_config=env.config_file, **options)
|
|
525
|
+
|
|
526
|
+
|
|
483
527
|
# ---------------------------------------------------------------- report
|
|
484
528
|
|
|
485
529
|
def scaffold_fingerprint(env: Env, scaffold: dict | None) -> dict | None:
|
|
@@ -501,7 +545,8 @@ def run_check(env: Env, project: Path | None, *, latest_source: str | None = Non
|
|
|
501
545
|
content = content_findings(project, latest) if project else []
|
|
502
546
|
opinion = opinion_findings(project, installed) if project else []
|
|
503
547
|
scaffold = scaffold_report(env, project, reprompt=reprompt)
|
|
504
|
-
machine = machine_findings(env, project, reprompt=reprompt)
|
|
548
|
+
machine = machine_findings(env, project, reprompt=reprompt, quick=hook_mode)
|
|
549
|
+
local_attention = sum(1 for item in machine["findings"] if item.get("attention"))
|
|
505
550
|
scaffold_active = len(scaffold["findings"]) if scaffold else 0
|
|
506
551
|
behind = [item for item in plugins if item["status"] == "behind"]
|
|
507
552
|
legacy = [item for item in plugins if item["client"] == "legacy"]
|
|
@@ -519,13 +564,15 @@ def run_check(env: Env, project: Path | None, *, latest_source: str | None = Non
|
|
|
519
564
|
"summary": {
|
|
520
565
|
"scaffold_findings": scaffold_active,
|
|
521
566
|
"scaffold_skipped": len(scaffold["skipped"]) if scaffold else 0,
|
|
567
|
+
"local_only_attention": local_attention,
|
|
522
568
|
"plugins_behind": len(behind),
|
|
523
569
|
"breaking": any(item["breaking"] for item in behind),
|
|
524
570
|
"content_updates": len(content_updates),
|
|
525
571
|
"content_conflicts": sum(1 for item in content if item["status"] == "conflict"),
|
|
526
572
|
"opinion_updates": len(opinion_updates),
|
|
527
573
|
"legacy_installs": len(legacy),
|
|
528
|
-
"up_to_date": not (behind or content_updates or opinion_updates or legacy or scaffold_active
|
|
574
|
+
"up_to_date": not (behind or content_updates or opinion_updates or legacy or scaffold_active
|
|
575
|
+
or local_attention),
|
|
529
576
|
},
|
|
530
577
|
}
|
|
531
578
|
|
|
@@ -544,17 +591,19 @@ def one_line(report: dict) -> str | None:
|
|
|
544
591
|
return None
|
|
545
592
|
parts = []
|
|
546
593
|
if summary["plugins_behind"]:
|
|
547
|
-
parts.append(
|
|
594
|
+
parts.append(_("{0} 个插件可更新").format(summary['plugins_behind']) + (_("(含不兼容变更)") if summary["breaking"] else ""))
|
|
548
595
|
if summary["content_updates"]:
|
|
549
|
-
parts.append(
|
|
550
|
-
+ (
|
|
596
|
+
parts.append(_("{0} 个模板/样例包可更新").format(summary['content_updates'])
|
|
597
|
+
+ (_("({0} 个本地有改动,更新时逐文件确认)").format(summary['content_conflicts']) if summary["content_conflicts"] else ""))
|
|
551
598
|
if summary["opinion_updates"]:
|
|
552
|
-
parts.append("Opinion 有新版本模板或规则")
|
|
599
|
+
parts.append(_("Opinion 有新版本模板或规则"))
|
|
553
600
|
if summary["legacy_installs"]:
|
|
554
|
-
parts.append(
|
|
601
|
+
parts.append(_("{0} 个旧方式安装待迁移").format(summary['legacy_installs']))
|
|
555
602
|
if summary["scaffold_findings"]:
|
|
556
|
-
parts.append(
|
|
557
|
-
|
|
603
|
+
parts.append(_("项目脚手架有 {0} 项差异").format(summary['scaffold_findings']))
|
|
604
|
+
if summary.get("local_only_attention"):
|
|
605
|
+
parts.append(_("同步目录里有 {0} 项本机专属目录需要处理").format(summary['local_only_attention']))
|
|
606
|
+
return _("agenthelm:") + _(",").join(parts) + _("。运行 `{0} check` 查看详情;更新前会先预览,由你决定。").format(CLI)
|
|
558
607
|
|
|
559
608
|
|
|
560
609
|
def render_scaffold(report: dict, show_skipped: bool) -> list[str]:
|
|
@@ -563,91 +612,89 @@ def render_scaffold(report: dict, show_skipped: bool) -> list[str]:
|
|
|
563
612
|
if scaffold is None:
|
|
564
613
|
return lines
|
|
565
614
|
if scaffold.get("error"):
|
|
566
|
-
lines.append(
|
|
615
|
+
lines.append(_(" [脚手架] 无法检查:{0}").format(scaffold['error']))
|
|
567
616
|
return lines
|
|
568
617
|
for item in scaffold["findings"]:
|
|
569
|
-
mark = "(稍后)" if item.get("deferred") else "(期望内容已变化,重新提示)" if item.get("changed_since_decision") else ""
|
|
570
|
-
fix =
|
|
571
|
-
lines.append(
|
|
618
|
+
mark = _("(稍后)") if item.get("deferred") else _("(期望内容已变化,重新提示)") if item.get("changed_since_decision") else ""
|
|
619
|
+
fix = _(" 处理:fix {0}").format(item['fix']) if item.get("fix") else _(" 需要你自己处理")
|
|
620
|
+
lines.append(_(" [脚手架] {0}{1}:{2}\n ID {3}{4}").format(item['title'], mark, item['reason'], item['id'], fix))
|
|
572
621
|
for message in scaffold["decisions"]["errors"]:
|
|
573
|
-
lines.append(
|
|
622
|
+
lines.append(_(" [脚手架] 决定文件有问题:{0}").format(message))
|
|
574
623
|
if scaffold["decisions"]["conflict_copies"]:
|
|
575
|
-
lines.append(" [脚手架] 同步服务留下了决定文件的冲突副本:"
|
|
576
|
-
+ "、".join(scaffold["decisions"]["conflict_copies"]) + "(合并到 check-decisions.json 后删除)")
|
|
624
|
+
lines.append(_(" [脚手架] 同步服务留下了决定文件的冲突副本:")
|
|
625
|
+
+ _("、").join(scaffold["decisions"]["conflict_copies"]) + _("(合并到 check-decisions.json 后删除)"))
|
|
577
626
|
if not scaffold["findings"] and not scaffold["decisions"]["errors"]:
|
|
578
|
-
lines.append(" [脚手架] 与期望一致" + (
|
|
627
|
+
lines.append(_(" [脚手架] 与期望一致") + (_("({0} 项已跳过)").format(len(scaffold['skipped'])) if scaffold["skipped"] else ""))
|
|
579
628
|
skipped = [*scaffold["skipped"], *report.get("machine", {}).get("skipped", [])]
|
|
580
629
|
if show_skipped:
|
|
581
630
|
for item in skipped:
|
|
582
631
|
decision = item["decision"]
|
|
583
|
-
lines.append(
|
|
584
|
-
f" {decision.get('decided_at', '')}")
|
|
632
|
+
lines.append(_(" [已跳过·{0}] {1}:{2} {3}").format(decision['decision'], item['id'], decision.get('reason') or _('(未写理由)'), decision.get('decided_at', '')))
|
|
585
633
|
if not skipped:
|
|
586
|
-
lines.append(" 没有被跳过的项。")
|
|
634
|
+
lines.append(_(" 没有被跳过的项。"))
|
|
587
635
|
elif skipped:
|
|
588
|
-
lines.append(
|
|
636
|
+
lines.append(_(" (另有 {0} 项已被你跳过:check --show-skipped 查看)").format(len(skipped)))
|
|
589
637
|
return lines
|
|
590
638
|
|
|
591
639
|
|
|
592
640
|
def render_machine(report: dict) -> list[str]:
|
|
593
641
|
lines = []
|
|
594
642
|
for item in report.get("machine", {}).get("findings", []):
|
|
595
|
-
lines.append(
|
|
596
|
-
f"\n ID {item['id']}(只记在这台机器上;fix --skip {item['id']} --reason …)")
|
|
643
|
+
lines.append(_(" [本机] {0}:{1}\n 影响:{2}\n 处理:{3}\n ID {4}(只记在这台机器上;fix --skip {5} --reason …)").format(item['title'], item['reason'], item['impact'], item['next'], item['id'], item['id']))
|
|
597
644
|
return lines
|
|
598
645
|
|
|
599
646
|
|
|
600
647
|
def render_text(report: dict, *, show_skipped: bool = False) -> str:
|
|
601
648
|
lines = []
|
|
602
649
|
origin = report["latest"]["source"]
|
|
603
|
-
lines.append(
|
|
650
|
+
lines.append(_("云端版本源:{0}").format(origin.get('source')) + (_("(缓存)") if origin.get("cached") else ""))
|
|
604
651
|
if not report["plugins"]:
|
|
605
|
-
lines.append("未发现已安装的 agenthelm 插件。")
|
|
652
|
+
lines.append(_("未发现已安装的 agenthelm 插件。"))
|
|
606
653
|
for item in report["plugins"]:
|
|
607
|
-
label = {"behind": "可更新", "current": "最新", "ahead": "高于云端", "unknown": "版本未知",
|
|
608
|
-
"legacy": "旧方式安装,待迁移"}[item["status"]]
|
|
654
|
+
label = {"behind": _("可更新"), "current": _("最新"), "ahead": _("高于云端"), "unknown": _("版本未知"),
|
|
655
|
+
"legacy": _("旧方式安装,待迁移")}[item["status"]]
|
|
609
656
|
if item["status"] == "legacy":
|
|
610
|
-
lines.append(
|
|
657
|
+
lines.append(_(" [旧方式] {0} {1} {2}").format(item['plugin'], item.get('version'), label))
|
|
611
658
|
continue
|
|
612
659
|
line = f" [{item['client']}] {item['plugin']} {item.get('version')} → {item.get('latest')} {label}"
|
|
613
660
|
if item["status"] == "behind":
|
|
614
|
-
kinds = {"code": "代码", "content": "仅模板/样例", "both": "代码与模板"}
|
|
661
|
+
kinds = {"code": _("代码"), "content": _("仅模板/样例"), "both": _("代码与模板")}
|
|
615
662
|
if item["change_kinds"]:
|
|
616
|
-
line += "(" + "、".join(kinds[kind] for kind in item["change_kinds"]) + ")"
|
|
663
|
+
line += _("(") + _("、").join(kinds[kind] for kind in item["change_kinds"]) + _(")")
|
|
617
664
|
if item["breaking"]:
|
|
618
|
-
line += "(含不兼容变更)"
|
|
665
|
+
line += _("(含不兼容变更)")
|
|
619
666
|
for step in item["steps"]:
|
|
620
667
|
if step.get("breaking"):
|
|
621
|
-
line +=
|
|
668
|
+
line += _("\n {0} 不兼容:{1}").format(step['to'], step.get('summary'))
|
|
622
669
|
line += "".join(f"\n - {action}" for action in step.get("actions") or [])
|
|
623
670
|
hint = UPDATE_HINTS.get(item["client"])
|
|
624
671
|
if hint:
|
|
625
|
-
line +=
|
|
672
|
+
line += _("\n 更新:{0}").format(hint.format(plugin=item['plugin']))
|
|
626
673
|
lines.append(line)
|
|
627
674
|
for item in report["content"]:
|
|
628
675
|
if item.get("pack") is None:
|
|
629
|
-
lines.append(
|
|
676
|
+
lines.append(_(" 内容锁:{0}").format(item.get('error')))
|
|
630
677
|
continue
|
|
631
|
-
label = {"current": "最新", "update-available": "可更新(本地未改动)",
|
|
632
|
-
"conflict": "可更新(本地有改动,更新时逐文件确认)", "unknown": "云端未列出"}[item["status"]]
|
|
633
|
-
line =
|
|
678
|
+
label = {"current": _("最新"), "update-available": _("可更新(本地未改动)"),
|
|
679
|
+
"conflict": _("可更新(本地有改动,更新时逐文件确认)"), "unknown": _("云端未列出")}[item["status"]]
|
|
680
|
+
line = _(" [内容] {0} {1} → {2} {3}").format(item['pack'], item['version'], item.get('latest'), label)
|
|
634
681
|
if item["locally_changed"]:
|
|
635
|
-
line += "\n 本地已改动:" + "、".join(item["locally_changed"])
|
|
682
|
+
line += _("\n 本地已改动:") + _("、").join(item["locally_changed"])
|
|
636
683
|
lines.append(line)
|
|
637
684
|
for item in report["opinion"]:
|
|
638
685
|
if item["status"] == "update-available":
|
|
639
|
-
refs = ";".join(f"{update['current']} → {update['available'][-1]}" for update in item["updates"])
|
|
640
|
-
lines.append(
|
|
686
|
+
refs = _(";").join(f"{update['current']} → {update['available'][-1]}" for update in item["updates"])
|
|
687
|
+
lines.append(_(" [Opinion] 有新版本:{0}(用 opinion-manager 预览后确认)").format(refs))
|
|
641
688
|
elif item["status"] in {"error", "unknown"}:
|
|
642
689
|
lines.append(f" [Opinion] {item.get('error')}")
|
|
643
690
|
lines += render_scaffold(report, show_skipped)
|
|
644
691
|
lines += render_machine(report)
|
|
645
692
|
if not report["plugins"]:
|
|
646
|
-
lines.append(
|
|
693
|
+
lines.append(_("运行 `{0} install` 预览安装。").format(CLI))
|
|
647
694
|
else:
|
|
648
695
|
if any(item["status"] == "legacy" for item in report["plugins"]):
|
|
649
|
-
lines.append(
|
|
650
|
-
lines.append(one_line(report) or "全部为最新。")
|
|
696
|
+
lines.append(_("旧方式安装可用 `{0}` 预览迁移。").format(UPDATE_HINTS['legacy']))
|
|
697
|
+
lines.append(one_line(report) or _("全部为最新。"))
|
|
651
698
|
return "\n".join(lines) + "\n"
|
|
652
699
|
|
|
653
700
|
|
|
@@ -695,7 +742,7 @@ CLIENTS = ("claude", "codex", "cursor")
|
|
|
695
742
|
def github_slug(latest: dict) -> str:
|
|
696
743
|
match = re.match(r"https://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$", str(latest.get("repository", "")))
|
|
697
744
|
if not match:
|
|
698
|
-
raise CheckError(
|
|
745
|
+
raise CheckError(_("latest.json 的 repository 不是 GitHub 仓库:{0}").format(latest.get('repository')))
|
|
699
746
|
return match.group(1)
|
|
700
747
|
|
|
701
748
|
|
|
@@ -715,9 +762,9 @@ def _safe_extract(archive: Path, destination: Path) -> None:
|
|
|
715
762
|
for member in bundle.getmembers():
|
|
716
763
|
target = (destination / member.name).resolve()
|
|
717
764
|
if destination.resolve() not in target.parents and target != destination.resolve():
|
|
718
|
-
raise CheckError(
|
|
765
|
+
raise CheckError(_("发布包含越界路径:{0}").format(member.name))
|
|
719
766
|
if not (member.isfile() or member.isdir()):
|
|
720
|
-
raise CheckError(
|
|
767
|
+
raise CheckError(_("发布包含不支持的条目:{0}").format(member.name))
|
|
721
768
|
try:
|
|
722
769
|
bundle.extractall(destination, filter="data") # noqa: S202 - members validated above
|
|
723
770
|
except TypeError: # Python without extraction filters
|
|
@@ -748,25 +795,25 @@ def release_source(latest: dict, workdir: Path, source: Path | None = None) -> P
|
|
|
748
795
|
if source is not None:
|
|
749
796
|
root = source.expanduser().resolve()
|
|
750
797
|
if not (root / "plugins").is_dir():
|
|
751
|
-
raise CheckError(
|
|
798
|
+
raise CheckError(_("{0} 不含 plugins/ 目录").format(root))
|
|
752
799
|
return root
|
|
753
800
|
release = latest.get("release") or github_release(latest)
|
|
754
801
|
tag, digest = release.get("tag"), release.get("tarball_sha256")
|
|
755
802
|
if not tag or not digest:
|
|
756
|
-
raise CheckError("云端 latest.json 没有发布 tag 与摘要;请用 --source 指定本地仓库")
|
|
803
|
+
raise CheckError(_("云端 latest.json 没有发布 tag 与摘要;请用 --source 指定本地仓库"))
|
|
757
804
|
url = release.get("tarball_url") or f"https://github.com/{github_slug(latest)}/releases/download/{tag}/agenthelm-{tag}.tar.gz"
|
|
758
805
|
archive = workdir / "release.tar.gz"
|
|
759
806
|
request = urllib.request.Request(url, headers={"User-Agent": "agenthelm"})
|
|
760
807
|
with urllib.request.urlopen(request, timeout=60) as response, archive.open("wb") as handle: # noqa: S310
|
|
761
808
|
shutil.copyfileobj(response, handle)
|
|
762
809
|
if sha256_file(archive) != digest:
|
|
763
|
-
raise CheckError(
|
|
810
|
+
raise CheckError(_("发布包摘要不符:{0}").format(url))
|
|
764
811
|
extracted = workdir / "release"
|
|
765
812
|
extracted.mkdir()
|
|
766
813
|
_safe_extract(archive, extracted)
|
|
767
814
|
roots = [path for path in extracted.iterdir() if path.is_dir()]
|
|
768
815
|
if len(roots) != 1 or not (roots[0] / "plugins").is_dir():
|
|
769
|
-
raise CheckError("发布包结构不符合预期")
|
|
816
|
+
raise CheckError(_("发布包结构不符合预期"))
|
|
770
817
|
return roots[0]
|
|
771
818
|
|
|
772
819
|
|
|
@@ -824,7 +871,7 @@ def find_cli(env: Env, name: str) -> str | None:
|
|
|
824
871
|
def _cli(env: Env, name: str, *args: str) -> tuple[list[str], str | None]:
|
|
825
872
|
"""The command (using the resolved executable) and why it cannot run, if it cannot."""
|
|
826
873
|
found = find_cli(env, name)
|
|
827
|
-
return [found or name, *args], (None if found else
|
|
874
|
+
return [found or name, *args], (None if found else _("未找到 {0} 命令(已查 PATH 与常见安装位置;可用 AGENTHELM_{1} 指定)").format(name, name.upper()))
|
|
828
875
|
|
|
829
876
|
|
|
830
877
|
def install_steps(env: Env, latest: dict, clients: list[str], plugins: list[str], source: Path | None,
|
|
@@ -834,18 +881,18 @@ def install_steps(env: Env, latest: dict, clients: list[str], plugins: list[str]
|
|
|
834
881
|
for client in clients:
|
|
835
882
|
if client == "claude":
|
|
836
883
|
command, blocked = _cli(env, "claude", "plugin", "marketplace", "add", slug)
|
|
837
|
-
steps.append(Step("install", "claude",
|
|
884
|
+
steps.append(Step("install", "claude", _("Claude Code 添加 agenthelm 市场({0})").format(slug), command, blocked=blocked))
|
|
838
885
|
for plugin in plugins:
|
|
839
886
|
command, blocked = _cli(env, "claude", "plugin", "install", f"{plugin}@{MARKETPLACE}")
|
|
840
|
-
steps.append(Step("install", "claude",
|
|
887
|
+
steps.append(Step("install", "claude", _("Claude Code 安装 {0}").format(plugin), command, blocked=blocked))
|
|
841
888
|
elif client == "codex":
|
|
842
889
|
command, blocked = _cli(env, "codex", "plugin", "marketplace", "add", slug)
|
|
843
|
-
steps.append(Step("install", "codex",
|
|
890
|
+
steps.append(Step("install", "codex", _("Codex 添加 agenthelm 市场({0})").format(slug), command, blocked=blocked))
|
|
844
891
|
for plugin in plugins:
|
|
845
892
|
command, blocked = _cli(env, "codex", "plugin", "add", f"{plugin}@{MARKETPLACE}")
|
|
846
|
-
steps.append(Step("install", "codex",
|
|
893
|
+
steps.append(Step("install", "codex", _("Codex 安装 {0}").format(plugin), command, blocked=blocked))
|
|
847
894
|
elif client == "cursor":
|
|
848
|
-
steps.append(cursor_step(latest, plugins, source, home, "安装"))
|
|
895
|
+
steps.append(cursor_step(latest, plugins, source, home, _("安装")))
|
|
849
896
|
return steps
|
|
850
897
|
|
|
851
898
|
|
|
@@ -857,7 +904,7 @@ def cursor_step(latest: dict, plugins: list[str], source: Path | None, home: Pat
|
|
|
857
904
|
return [config.install_plugin(root / "plugins" / plugin, "cursor", home=home, apply=apply,
|
|
858
905
|
replace=True, recovery_root=recovery_root)
|
|
859
906
|
for plugin in plugins]
|
|
860
|
-
return Step("install", "cursor",
|
|
907
|
+
return Step("install", "cursor", _("Cursor {0} {1} 到 ~/.cursor/plugins/local(重启 Cursor 生效)").format(verb, _('、').join(plugins)),
|
|
861
908
|
action=run)
|
|
862
909
|
|
|
863
910
|
|
|
@@ -871,18 +918,18 @@ def update_steps(env: Env, report: dict, project: Path | None, source: Path | No
|
|
|
871
918
|
claude = sorted({item["plugin"] for item in behind if item["client"] == "claude"})
|
|
872
919
|
if claude:
|
|
873
920
|
command, blocked = _cli(env, "claude", "plugin", "marketplace", "update", MARKETPLACE)
|
|
874
|
-
steps.append(Step("update", "claude", "刷新 Claude Code 的 agenthelm 市场", command, blocked=blocked))
|
|
921
|
+
steps.append(Step("update", "claude", _("刷新 Claude Code 的 agenthelm 市场"), command, blocked=blocked))
|
|
875
922
|
for item in [entry for entry in behind if entry["client"] == "claude"]:
|
|
876
923
|
extra = ["--scope", item["scope"]] if item.get("scope") and item["scope"] != "user" else []
|
|
877
924
|
command, blocked = _cli(env, "claude", "plugin", "update", f"{item['plugin']}@{MARKETPLACE}", *extra)
|
|
878
|
-
steps.append(Step("update", "claude",
|
|
925
|
+
steps.append(Step("update", "claude", _("Claude Code 更新 {0} → {1}").format(item['plugin'], item['latest']),
|
|
879
926
|
command, blocked=blocked))
|
|
880
927
|
if any(item["client"] == "codex" for item in behind):
|
|
881
928
|
command, blocked = _cli(env, "codex", "plugin", "marketplace", "upgrade", MARKETPLACE)
|
|
882
|
-
steps.append(Step("update", "codex", "Codex 升级 agenthelm 市场与已装插件", command, blocked=blocked))
|
|
929
|
+
steps.append(Step("update", "codex", _("Codex 升级 agenthelm 市场与已装插件"), command, blocked=blocked))
|
|
883
930
|
cursor = sorted({item["plugin"] for item in behind if item["client"] == "cursor"})
|
|
884
931
|
if cursor:
|
|
885
|
-
steps.append(cursor_step(latest, cursor, source, home, "更新"))
|
|
932
|
+
steps.append(cursor_step(latest, cursor, source, home, _("更新")))
|
|
886
933
|
if only in (None, "content") and project is not None:
|
|
887
934
|
for item in report["content"]:
|
|
888
935
|
if item.get("status") not in {"update-available", "conflict"}:
|
|
@@ -894,15 +941,15 @@ def update_steps(env: Env, report: dict, project: Path | None, source: Path | No
|
|
|
894
941
|
def content_step(env: Env, item: dict, report: dict, project: Path, accept: list[str], keep: list[str],
|
|
895
942
|
recovery_dir: Path | None) -> Step:
|
|
896
943
|
if item["plugin"] != "agents-init":
|
|
897
|
-
return Step("content", None,
|
|
944
|
+
return Step("content", None, _("{0}:没有可用的刷新工具").format(item['pack']), blocked="unsupported")
|
|
898
945
|
stale = any(entry["plugin"] == "agents-init" and entry["status"] == "behind" for entry in report["plugins"])
|
|
899
|
-
description =
|
|
946
|
+
description = _("重新渲染 {0} {1} → {2}").format(item['pack'], item['version'], item['latest'])
|
|
900
947
|
|
|
901
948
|
def run(apply: bool, _recovery_root: Path | None) -> dict:
|
|
902
949
|
installed = installed_plugins(env, {"agents-init"})
|
|
903
950
|
manager = locate_companion("agents-init", installed)
|
|
904
951
|
if manager is None:
|
|
905
|
-
raise CheckError("未找到已安装的 agents-init")
|
|
952
|
+
raise CheckError(_("未找到已安装的 agents-init"))
|
|
906
953
|
command = [sys.executable, str(manager / "skills/agents-init/scripts/init_project.py"),
|
|
907
954
|
"--project", str(project), "--mode", "refresh", "--output", "json"]
|
|
908
955
|
for path in accept:
|
|
@@ -920,7 +967,7 @@ def content_step(env: Env, item: dict, report: dict, project: Path, accept: list
|
|
|
920
967
|
except ValueError as exc:
|
|
921
968
|
raise CheckError((completed.stderr or completed.stdout).strip()[:300]) from exc
|
|
922
969
|
|
|
923
|
-
blocked = "先更新 agents-init 插件,再用新模板预览" if stale else None
|
|
970
|
+
blocked = _("先更新 agents-init 插件,再用新模板预览") if stale else None
|
|
924
971
|
return Step("content", None, description, action=run, blocked=blocked)
|
|
925
972
|
|
|
926
973
|
|
|
@@ -1122,95 +1169,97 @@ def doctor(env: Env, report: dict) -> dict:
|
|
|
1122
1169
|
clients = [item["client"] for item in items]
|
|
1123
1170
|
if "legacy" in clients and any(client in {"claude", "codex"} for client in clients):
|
|
1124
1171
|
issues.append({"plugin": plugin, "issue": "duplicate",
|
|
1125
|
-
"detail":
|
|
1172
|
+
"detail": _("同时存在市场安装与旧方式安装,运行 {0} migrate").format(CLI)})
|
|
1126
1173
|
versions = {item.get("version") for item in items if item["client"] != "legacy"}
|
|
1127
1174
|
if len(versions) > 1:
|
|
1128
1175
|
issues.append({"plugin": plugin, "issue": "version-skew",
|
|
1129
|
-
"detail": "各客户端版本不一致:" + "、".join(f"{i['client']}={i.get('version')}" for i in items)})
|
|
1176
|
+
"detail": _("各客户端版本不一致:") + _("、").join(f"{i['client']}={i.get('version')}" for i in items)})
|
|
1130
1177
|
names = set(report["_latest"].get("plugins", {}))
|
|
1131
1178
|
installed_names = {item["plugin"] for item in report["plugins"] if item["client"] != "legacy"}
|
|
1132
1179
|
if installed_names and installed_names != names:
|
|
1133
1180
|
issues.append({"plugin": None, "issue": "incomplete",
|
|
1134
|
-
"detail": "缺少:" + "、".join(sorted(names - installed_names))})
|
|
1181
|
+
"detail": _("缺少:") + _("、").join(sorted(names - installed_names))})
|
|
1135
1182
|
if not update_check_enabled(env):
|
|
1136
|
-
issues.append({"plugin": None, "issue": "update-check-disabled", "detail": "会话内更新检查已关闭"})
|
|
1183
|
+
issues.append({"plugin": None, "issue": "update-check-disabled", "detail": _("会话内更新检查已关闭")})
|
|
1137
1184
|
return {"ok": not [issue for issue in issues if issue["issue"] != "update-check-disabled"],
|
|
1138
1185
|
"issues": issues, "legacy": legacy_artifacts(env, names), "summary": report["summary"]}
|
|
1139
1186
|
|
|
1140
1187
|
|
|
1141
1188
|
def parser() -> argparse.ArgumentParser:
|
|
1142
|
-
result = argparse.ArgumentParser(prog="agenthelm", description=
|
|
1189
|
+
result = argparse.ArgumentParser(prog="agenthelm", description=_("agenthelm:在 Claude Code、Codex、Cursor 间安装、检查与更新 agenthelm 插件。"))
|
|
1143
1190
|
sub = result.add_subparsers(dest="command", required=True)
|
|
1144
1191
|
|
|
1145
1192
|
def common(command: argparse.ArgumentParser, *, project: bool = True) -> None:
|
|
1146
|
-
command.add_argument("--latest", help="latest.json 的 URL 或本地路径(测试、离线镜像)")
|
|
1193
|
+
command.add_argument("--latest", help=_("latest.json 的 URL 或本地路径(测试、离线镜像)"))
|
|
1147
1194
|
command.add_argument("--output", choices=("text", "json"), default="text")
|
|
1148
1195
|
command.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
1149
1196
|
if project:
|
|
1150
|
-
command.add_argument("--project", type=Path, default=None, help="项目目录;默认当前目录")
|
|
1151
|
-
command.add_argument("--no-project", action="store_true", help="不检查项目内容")
|
|
1197
|
+
command.add_argument("--project", type=Path, default=None, help=_("项目目录;默认当前目录"))
|
|
1198
|
+
command.add_argument("--no-project", action="store_true", help=_("不检查项目内容"))
|
|
1152
1199
|
|
|
1153
|
-
check = sub.add_parser("check", help="只读检查插件、模板/样例与 Opinion 是否落后于云端")
|
|
1200
|
+
check = sub.add_parser("check", help=_("只读检查插件、模板/样例与 Opinion 是否落后于云端"))
|
|
1154
1201
|
common(check)
|
|
1155
|
-
check.add_argument("--offline", action="store_true", help="只用缓存,不联网")
|
|
1156
|
-
check.add_argument("--refresh", action="store_true", help="忽略一天的缓存,立即联网")
|
|
1157
|
-
check.add_argument("--hook", action="store_true", help="会话开始时静默调用:有更新才输出一行")
|
|
1202
|
+
check.add_argument("--offline", action="store_true", help=_("只用缓存,不联网"))
|
|
1203
|
+
check.add_argument("--refresh", action="store_true", help=_("忽略一天的缓存,立即联网"))
|
|
1204
|
+
check.add_argument("--hook", action="store_true", help=_("会话开始时静默调用:有更新才输出一行"))
|
|
1158
1205
|
check.add_argument("--hook-format", choices=("text", "claude-hook"), default="text")
|
|
1159
|
-
check.add_argument("--strict", action="store_true", help="有可更新项或脚手架差异时退出码为 1(CI 用)")
|
|
1160
|
-
check.add_argument("--show-skipped", action="store_true", help="列出被你跳过的项及理由")
|
|
1161
|
-
check.add_argument("--reprompt", action="store_true", help="忽略已记录的决定,重新评估全部脚手架差异")
|
|
1206
|
+
check.add_argument("--strict", action="store_true", help=_("有可更新项或脚手架差异时退出码为 1(CI 用)"))
|
|
1207
|
+
check.add_argument("--show-skipped", action="store_true", help=_("列出被你跳过的项及理由"))
|
|
1208
|
+
check.add_argument("--reprompt", action="store_true", help=_("忽略已记录的决定,重新评估全部脚手架差异"))
|
|
1162
1209
|
check.add_argument("--fingerprint", action="store_true",
|
|
1163
|
-
help="输出脚手架指纹(骨架状态 + 受检 Profile 字段 + 内容锁 + 命名规则;不含 Opinion、自有字段、已跳过项)")
|
|
1164
|
-
check.add_argument("--explain", nargs="?", const="-", metavar="对方的 JSON",
|
|
1165
|
-
help="与 --fingerprint 合用:不带参数时逐项列出这台的明细;带另一台机器 check --fingerprint "
|
|
1166
|
-
"--output json 的输出文件时,指出两台差在哪里(有差异退出码 1)")
|
|
1210
|
+
help=_("输出脚手架指纹(骨架状态 + 受检 Profile 字段 + 内容锁 + 命名规则;不含 Opinion、自有字段、已跳过项)"))
|
|
1211
|
+
check.add_argument("--explain", nargs="?", const="-", metavar=_("对方的 JSON"),
|
|
1212
|
+
help=_("与 --fingerprint 合用:不带参数时逐项列出这台的明细;带另一台机器 check --fingerprint "
|
|
1213
|
+
"--output json 的输出文件时,指出两台差在哪里(有差异退出码 1)"))
|
|
1167
1214
|
|
|
1168
|
-
install = sub.add_parser("install", help="经各客户端的原生市场安装 agenthelm 插件(默认预览)")
|
|
1215
|
+
install = sub.add_parser("install", help=_("经各客户端的原生市场安装 agenthelm 插件(默认预览)"))
|
|
1169
1216
|
common(install, project=False)
|
|
1170
|
-
install.add_argument("--client", action="append", choices=CLIENTS, help="默认安装到检测到的全部客户端")
|
|
1171
|
-
install.add_argument("--plugin", action="append", help="默认安装全部插件")
|
|
1172
|
-
install.add_argument("--source", type=Path, help="Cursor 安装用的本地仓库(默认下载发布包)")
|
|
1173
|
-
install.add_argument("--marketplace-source", help="市场来源(默认 latest.json 的 GitHub 仓库;可用镜像或本地路径)")
|
|
1217
|
+
install.add_argument("--client", action="append", choices=CLIENTS, help=_("默认安装到检测到的全部客户端"))
|
|
1218
|
+
install.add_argument("--plugin", action="append", help=_("默认安装全部插件"))
|
|
1219
|
+
install.add_argument("--source", type=Path, help=_("Cursor 安装用的本地仓库(默认下载发布包)"))
|
|
1220
|
+
install.add_argument("--marketplace-source", help=_("市场来源(默认 latest.json 的 GitHub 仓库;可用镜像或本地路径)"))
|
|
1174
1221
|
install.add_argument("--apply", action="store_true")
|
|
1175
1222
|
|
|
1176
|
-
update = sub.add_parser("update", help="更新落后的插件与模板/样例(默认预览,确认后 --apply)")
|
|
1223
|
+
update = sub.add_parser("update", help=_("更新落后的插件与模板/样例(默认预览,确认后 --apply)"))
|
|
1177
1224
|
common(update)
|
|
1178
1225
|
update.add_argument("--only", choices=("plugins", "content"))
|
|
1179
|
-
update.add_argument("--accept", action="append", default=[], help="内容更新:用新模板替换这个本地已改动的文件")
|
|
1180
|
-
update.add_argument("--keep", action="append", default=[], help="内容更新:保留这个文件的本地内容")
|
|
1181
|
-
update.add_argument("--recovery-dir", type=Path, help="被替换文件的备份目录(项目外)")
|
|
1182
|
-
update.add_argument("--source", type=Path, help="Cursor 更新用的本地仓库(默认下载发布包)")
|
|
1226
|
+
update.add_argument("--accept", action="append", default=[], help=_("内容更新:用新模板替换这个本地已改动的文件"))
|
|
1227
|
+
update.add_argument("--keep", action="append", default=[], help=_("内容更新:保留这个文件的本地内容"))
|
|
1228
|
+
update.add_argument("--recovery-dir", type=Path, help=_("被替换文件的备份目录(项目外)"))
|
|
1229
|
+
update.add_argument("--source", type=Path, help=_("Cursor 更新用的本地仓库(默认下载发布包)"))
|
|
1183
1230
|
update.add_argument("--apply", action="store_true")
|
|
1184
1231
|
|
|
1185
|
-
migrate = sub.add_parser("migrate", help="把旧方式安装(agent-pack 复制 + 软链)迁到市场安装(默认预览)")
|
|
1232
|
+
migrate = sub.add_parser("migrate", help=_("把旧方式安装(agent-pack 复制 + 软链)迁到市场安装(默认预览)"))
|
|
1186
1233
|
common(migrate, project=False)
|
|
1187
|
-
migrate.add_argument("--install", action="store_true", help="先经市场安装到检测到的客户端")
|
|
1234
|
+
migrate.add_argument("--install", action="store_true", help=_("先经市场安装到检测到的客户端"))
|
|
1188
1235
|
migrate.add_argument("--client", action="append", choices=CLIENTS)
|
|
1189
1236
|
migrate.add_argument("--source", type=Path)
|
|
1190
1237
|
migrate.add_argument("--marketplace-source")
|
|
1191
1238
|
migrate.add_argument("--apply", action="store_true")
|
|
1192
1239
|
|
|
1193
|
-
doctor_parser = sub.add_parser("doctor", help="报告重复安装、版本不一致、旧方式残留与检查开关")
|
|
1240
|
+
doctor_parser = sub.add_parser("doctor", help=_("报告重复安装、版本不一致、旧方式残留与检查开关"))
|
|
1194
1241
|
common(doctor_parser)
|
|
1195
1242
|
|
|
1196
1243
|
sub.add_parser("fix", add_help=False,
|
|
1197
|
-
help="修复项目脚手架差异与更新(等同 project fix;默认预览,--apply 写入;--skip/--unskip 记录决定)")
|
|
1244
|
+
help=_("修复项目脚手架差异与更新(等同 project fix;默认预览,--apply 写入;--skip/--unskip 记录决定)"))
|
|
1245
|
+
sub.add_parser("local-only", add_help=False,
|
|
1246
|
+
help=_("只属于这台机器的目录(.venv、node_modules、缓存)不进云同步盘:check / fix / config(详见 local-only --help)"))
|
|
1198
1247
|
sub.add_parser("runtime", add_help=False,
|
|
1199
|
-
help="用户级运行时:install / update / use / status(日常只需要 Python,不经过 npx)")
|
|
1248
|
+
help=_("用户级运行时:install / update / use / status(日常只需要 Python,不经过 npx)"))
|
|
1200
1249
|
sub.add_parser("project", add_help=False,
|
|
1201
|
-
help="项目自助入口:找资料、看进展、项目指南、检查(不需要 AI Agent;详见 project --help)")
|
|
1202
|
-
locate_parser = sub.add_parser("locate", help="输出插件的已安装位置(最新优先),供下游脚本查找")
|
|
1250
|
+
help=_("项目自助入口:找资料、看进展、项目指南、检查(不需要 AI Agent;详见 project --help)"))
|
|
1251
|
+
locate_parser = sub.add_parser("locate", help=_("输出插件的已安装位置(最新优先),供下游脚本查找"))
|
|
1203
1252
|
locate_parser.add_argument("plugin")
|
|
1204
|
-
locate_parser.add_argument("--version", help="只要这个版本")
|
|
1253
|
+
locate_parser.add_argument("--version", help=_("只要这个版本"))
|
|
1205
1254
|
locate_parser.add_argument("--include-legacy", action="store_true",
|
|
1206
|
-
help="也列出旧方式安装(默认不列,避免清理后才失效)")
|
|
1255
|
+
help=_("也列出旧方式安装(默认不列,避免清理后才失效)"))
|
|
1207
1256
|
locate_parser.add_argument("--output", choices=("text", "json"), default="json")
|
|
1208
1257
|
locate_parser.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
1209
1258
|
return result
|
|
1210
1259
|
|
|
1211
1260
|
|
|
1212
1261
|
def _print_steps(results: list[dict], apply: bool) -> None:
|
|
1213
|
-
labels = {"preview": "预览", "applied": "完成", "blocked": "阻塞", "failed": "失败"}
|
|
1262
|
+
labels = {"preview": _("预览"), "applied": _("完成"), "blocked": _("阻塞"), "failed": _("失败")}
|
|
1214
1263
|
for record in results:
|
|
1215
1264
|
line = f" [{labels.get(record['status'], record['status'])}] {record['description']}"
|
|
1216
1265
|
if record.get("command"):
|
|
@@ -1227,7 +1276,7 @@ def _print_steps(results: list[dict], apply: bool) -> None:
|
|
|
1227
1276
|
line += f"\n {result['hint']}"
|
|
1228
1277
|
print(line)
|
|
1229
1278
|
if not apply and results:
|
|
1230
|
-
print("以上为预览;确认后追加 --apply 执行。")
|
|
1279
|
+
print(_("以上为预览;确认后追加 --apply 执行。"))
|
|
1231
1280
|
|
|
1232
1281
|
|
|
1233
1282
|
def run_project(argv: list[str]) -> int:
|
|
@@ -1238,9 +1287,8 @@ def run_project(argv: list[str]) -> int:
|
|
|
1238
1287
|
usable = [path for path in copies if (path / "runtime" / "project_cli.py").is_file()]
|
|
1239
1288
|
if not usable:
|
|
1240
1289
|
stale = any((path / "plugin.json").is_file() for path in copies)
|
|
1241
|
-
found = "(已安装的 project-orchestrator 版本过旧)" if stale else ""
|
|
1242
|
-
print(
|
|
1243
|
-
f" · 更新插件:{CLI} update,或从发布包安装运行时(见 INSTALL.md)", file=sys.stderr)
|
|
1290
|
+
found = _("(已安装的 project-orchestrator 版本过旧)") if stale else ""
|
|
1291
|
+
print(_("agenthelm project 需要 project-orchestrator 的项目入口,但没有找到{0}。\n · 更新插件:{1} update,或从发布包安装运行时(见 INSTALL.md)").format(found, CLI), file=sys.stderr)
|
|
1244
1292
|
return 3
|
|
1245
1293
|
# Prefer the copy shipped next to this agent-pack (same release), then the newest installed one.
|
|
1246
1294
|
sibling = PLUGIN_ROOT.parent / "project-orchestrator"
|
|
@@ -1253,6 +1301,19 @@ def run_project(argv: list[str]) -> int:
|
|
|
1253
1301
|
return project_cli.main(argv)
|
|
1254
1302
|
|
|
1255
1303
|
|
|
1304
|
+
def run_nudge(argv: list[str]) -> int:
|
|
1305
|
+
"""``agenthelm nudge``: hooks that collect opinion candidates and surface unconfirmed items."""
|
|
1306
|
+
sys.path.insert(0, str(RUNTIME_DIR))
|
|
1307
|
+
import agent_pack_nudge # noqa: PLC0415 - shipped next to this file
|
|
1308
|
+
|
|
1309
|
+
return agent_pack_nudge.main(argv, sys.modules[__name__])
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def run_local_only(argv: list[str]) -> int:
|
|
1313
|
+
"""``agenthelm local-only``: keep rebuildable directories out of cloud-synced folders."""
|
|
1314
|
+
return _local_only().main(argv, sys.modules[__name__])
|
|
1315
|
+
|
|
1316
|
+
|
|
1256
1317
|
def run_runtime(argv: list[str]) -> int:
|
|
1257
1318
|
"""``agenthelm runtime``: the user-level runtime that project launchers use without any AI client."""
|
|
1258
1319
|
sys.path.insert(0, str(RUNTIME_DIR))
|
|
@@ -1261,35 +1322,51 @@ def run_runtime(argv: list[str]) -> int:
|
|
|
1261
1322
|
return agenthelm_runtime.main(argv, sys.modules[__name__])
|
|
1262
1323
|
|
|
1263
1324
|
|
|
1264
|
-
def print_fingerprint(env: Env, report: dict, args: argparse.Namespace) -> int:
|
|
1325
|
+
def print_fingerprint(env: Env, report: dict, args: argparse.Namespace, project: Path | None = None) -> int:
|
|
1265
1326
|
"""``check --fingerprint [--explain [file]]``: the scaffold fingerprint, and where two machines differ."""
|
|
1266
1327
|
result = scaffold_fingerprint(env, report.get("scaffold"))
|
|
1267
1328
|
if result is None:
|
|
1268
|
-
reason = (report.get("scaffold") or {}).get("error") or "当前目录不在 agenthelm 项目里"
|
|
1329
|
+
reason = (report.get("scaffold") or {}).get("error") or _("当前目录不在 agenthelm 项目里")
|
|
1269
1330
|
print(json.dumps({"ok": False, "error": reason}, ensure_ascii=False) if args.output == "json"
|
|
1270
|
-
else
|
|
1331
|
+
else _("无法计算脚手架指纹:{0}").format(reason), file=sys.stderr if args.output != "json" else sys.stdout)
|
|
1271
1332
|
return 4
|
|
1272
1333
|
payload: dict = {"ok": True, "fingerprint": result["fingerprint"], "detail": result["detail"]}
|
|
1273
|
-
lines = [
|
|
1334
|
+
lines = [_("脚手架指纹 {0}").format(result['fingerprint'])]
|
|
1335
|
+
mine_local: dict = {}
|
|
1336
|
+
if project is not None:
|
|
1337
|
+
# 本机环境(外置链接、HOME)单独成段:没有 local-only 配置的项目,输出与以前完全一样。
|
|
1338
|
+
mine_local = local_only_report(env, project, mode="quick")["fingerprint"]
|
|
1339
|
+
if mine_local["detail"]:
|
|
1340
|
+
payload["local_only"] = mine_local
|
|
1341
|
+
lines.append(_("本机环境指纹 {0}").format(mine_local['fingerprint']))
|
|
1274
1342
|
differences: list[str] = []
|
|
1275
1343
|
if args.explain and args.explain != "-":
|
|
1276
1344
|
try:
|
|
1277
1345
|
other = read_json(Path(args.explain).expanduser())
|
|
1278
1346
|
except (OSError, ValueError) as exc:
|
|
1279
|
-
print(
|
|
1347
|
+
print(_("读不了对方的输出:{0}").format(exc), file=sys.stderr)
|
|
1280
1348
|
return 2
|
|
1281
1349
|
theirs = other.get("detail") or (other.get("scaffold_fingerprint") or {}).get("detail") or other.get("fingerprint", {}).get("detail")
|
|
1282
1350
|
if not isinstance(theirs, dict):
|
|
1283
|
-
print("对方的文件里没有指纹明细:请在对方机器运行 agenthelm check --fingerprint --output json 并保存输出", file=sys.stderr)
|
|
1351
|
+
print(_("对方的文件里没有指纹明细:请在对方机器运行 agenthelm check --fingerprint --output json 并保存输出"), file=sys.stderr)
|
|
1284
1352
|
return 2
|
|
1285
1353
|
module = scaffold_module(env)
|
|
1286
1354
|
differences = module.explain(result["detail"], theirs)
|
|
1355
|
+
their_local = (other.get("local_only") or {}).get("detail") or {}
|
|
1356
|
+
if mine_local.get("detail") or their_local:
|
|
1357
|
+
if not their_local:
|
|
1358
|
+
differences.append(_("本机环境:对方没有 local-only 明细(对方的 agent-pack 较旧或项目还没配置)"))
|
|
1359
|
+
elif not mine_local.get("detail"):
|
|
1360
|
+
differences.append(_("本机环境:这台没有 local-only 配置,对方有"))
|
|
1361
|
+
else:
|
|
1362
|
+
differences += _local_only().explain(mine_local["detail"], their_local)
|
|
1287
1363
|
payload["same"] = not differences
|
|
1288
1364
|
payload["differences"] = differences
|
|
1289
|
-
lines.append("两台机器的脚手架一致。" if not differences else
|
|
1365
|
+
lines.append(_("两台机器的脚手架一致。") if not differences else _("有 {0} 处不同:").format(len(differences)))
|
|
1290
1366
|
lines += [f" {item}" for item in differences]
|
|
1291
1367
|
elif args.explain:
|
|
1292
1368
|
lines += [f" {key} = {value}" for key, value in result["detail"].items()]
|
|
1369
|
+
lines += [f" local-only {key} = {value}" for key, value in mine_local.get("detail", {}).items()]
|
|
1293
1370
|
if args.output == "json":
|
|
1294
1371
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
1295
1372
|
else:
|
|
@@ -1305,6 +1382,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1305
1382
|
return run_project(["fix", *argv[1:]])
|
|
1306
1383
|
if argv[:1] == ["runtime"]:
|
|
1307
1384
|
return run_runtime(argv[1:])
|
|
1385
|
+
if argv[:1] == ["nudge"]:
|
|
1386
|
+
return run_nudge(argv[1:])
|
|
1387
|
+
if argv[:1] == ["local-only"]:
|
|
1388
|
+
return run_local_only(argv[1:])
|
|
1308
1389
|
args = parser().parse_args(argv)
|
|
1309
1390
|
env = current_env(args.home)
|
|
1310
1391
|
home = env.home
|
|
@@ -1326,7 +1407,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1326
1407
|
max_age=0 if args.refresh else CACHE_TTL_SECONDS, reprompt=args.reprompt)
|
|
1327
1408
|
report.pop("_latest")
|
|
1328
1409
|
if args.fingerprint or args.explain:
|
|
1329
|
-
return print_fingerprint(env, report, args)
|
|
1410
|
+
return print_fingerprint(env, report, args, project)
|
|
1330
1411
|
if args.output == "json":
|
|
1331
1412
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
1332
1413
|
else:
|
|
@@ -1341,9 +1422,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1341
1422
|
print(f" [{issue['issue']}] {issue.get('plugin') or ''} {issue['detail']}".rstrip())
|
|
1342
1423
|
for item in result["legacy"]:
|
|
1343
1424
|
print(f" [legacy] {item['kind']} {item['name']}: {item['path']}")
|
|
1344
|
-
print("没有发现问题。" if result["ok"] and not result["legacy"] else "")
|
|
1425
|
+
print(_("没有发现问题。") if result["ok"] and not result["legacy"] else "")
|
|
1345
1426
|
return 0 if result["ok"] else 1
|
|
1346
|
-
latest,
|
|
1427
|
+
latest, _origin = load_latest(env, args.latest, max_age=0)
|
|
1347
1428
|
if args.command == "install":
|
|
1348
1429
|
clients = args.client or detect_clients(env)
|
|
1349
1430
|
plugins = args.plugin or sorted(latest.get("plugins", {}))
|
|
@@ -1364,7 +1445,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1364
1445
|
payload = {"ok": all(r["status"] != "failed" for r in results), "applied": args.apply,
|
|
1365
1446
|
"steps": results, "summary": report["summary"]}
|
|
1366
1447
|
if not steps:
|
|
1367
|
-
payload["message"] = "没有需要更新的内容。"
|
|
1448
|
+
payload["message"] = _("没有需要更新的内容。")
|
|
1368
1449
|
else: # migrate
|
|
1369
1450
|
names = set(latest.get("plugins", {}))
|
|
1370
1451
|
artifacts = legacy_artifacts(env, names)
|
|
@@ -1391,9 +1472,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1391
1472
|
print(payload["message"])
|
|
1392
1473
|
_print_steps(payload["steps"], args.apply)
|
|
1393
1474
|
for item in payload.get("legacy", []):
|
|
1394
|
-
print(
|
|
1475
|
+
print(_(" [旧安装] {0} {1}: {2}").format(item['kind'], item['name'], item['path']) + (_("(需手动处理)") if item.get("manual") else ""))
|
|
1395
1476
|
if payload.get("recovery"):
|
|
1396
|
-
print(
|
|
1477
|
+
print(_("旧安装已移入:{0}(含 migration.json 清单,可按清单恢复)").format(payload['recovery']))
|
|
1397
1478
|
return 0 if payload["ok"] else 1
|
|
1398
1479
|
|
|
1399
1480
|
|