@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
|
@@ -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"
|
|
@@ -73,6 +75,10 @@ def write_json_atomic(path: Path, data: Any) -> None:
|
|
|
73
75
|
os.replace(temporary, path)
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
def sha256_text(value: str) -> str:
|
|
79
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
80
|
+
|
|
81
|
+
|
|
76
82
|
def sha256_file(path: Path) -> str:
|
|
77
83
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
78
84
|
|
|
@@ -99,6 +105,11 @@ class Env:
|
|
|
99
105
|
base = self.get("XDG_CACHE_HOME")
|
|
100
106
|
return (Path(base) if base else self.home / ".cache") / "agenthelm"
|
|
101
107
|
|
|
108
|
+
@property
|
|
109
|
+
def state_dir(self) -> Path:
|
|
110
|
+
base = self.get("XDG_STATE_HOME")
|
|
111
|
+
return Path(base) if base else self.home / ".local" / "state"
|
|
112
|
+
|
|
102
113
|
@property
|
|
103
114
|
def config_file(self) -> Path:
|
|
104
115
|
base = self.get("XDG_CONFIG_HOME")
|
|
@@ -165,14 +176,14 @@ def load_latest(env: Env, source: str | None = None, *, offline: bool = False,
|
|
|
165
176
|
if cached and (offline or time.time() - cached.get("fetched_at", 0) < max_age):
|
|
166
177
|
return cached["latest"], {"source": cached.get("url"), "cached": True, "fetched_at": cached.get("fetched_at")}
|
|
167
178
|
if offline:
|
|
168
|
-
raise CheckError("离线且没有缓存的 latest.json")
|
|
179
|
+
raise CheckError(_("离线且没有缓存的 latest.json"))
|
|
169
180
|
failure_file = env.cache_dir / "last_failure.json"
|
|
170
181
|
if backoff:
|
|
171
182
|
try:
|
|
172
183
|
if time.time() - read_json(failure_file).get("at", 0) < FAILURE_BACKOFF_SECONDS:
|
|
173
184
|
if cached:
|
|
174
185
|
return cached["latest"], {"source": cached.get("url"), "cached": True, "stale": True}
|
|
175
|
-
raise CheckError("最近联网失败,稍后再试")
|
|
186
|
+
raise CheckError(_("最近联网失败,稍后再试"))
|
|
176
187
|
except (OSError, ValueError, AttributeError):
|
|
177
188
|
pass
|
|
178
189
|
errors = []
|
|
@@ -194,7 +205,7 @@ def load_latest(env: Env, source: str | None = None, *, offline: bool = False,
|
|
|
194
205
|
if cached:
|
|
195
206
|
return cached["latest"], {"source": cached.get("url"), "cached": True, "stale": True,
|
|
196
207
|
"fetched_at": cached.get("fetched_at")}
|
|
197
|
-
raise CheckError("无法获取 latest.json:" + "; ".join(errors))
|
|
208
|
+
raise CheckError(_("无法获取 latest.json:") + "; ".join(errors))
|
|
198
209
|
|
|
199
210
|
|
|
200
211
|
# ---------------------------------------------------------------- installed plugins
|
|
@@ -213,7 +224,7 @@ def installed_plugins(env: Env, names: set[str]) -> list[dict]:
|
|
|
213
224
|
except (OSError, ValueError, AttributeError):
|
|
214
225
|
record = {}
|
|
215
226
|
for key, entries in sorted(record.items()):
|
|
216
|
-
plugin,
|
|
227
|
+
plugin, _sep, market = key.partition("@")
|
|
217
228
|
if market != MARKETPLACE or plugin not in names:
|
|
218
229
|
continue
|
|
219
230
|
for entry in entries:
|
|
@@ -241,14 +252,18 @@ def installed_plugins(env: Env, names: set[str]) -> list[dict]:
|
|
|
241
252
|
|
|
242
253
|
|
|
243
254
|
def migrations_between(latest: dict, plugin: str, current: str, target: str) -> list[dict]:
|
|
244
|
-
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] = {}
|
|
245
258
|
for entry in latest.get("migrations", []):
|
|
246
259
|
if entry.get("plugin") != plugin:
|
|
247
260
|
continue
|
|
248
261
|
to = str(entry.get("to"))
|
|
249
262
|
if is_newer(to, current) and not is_newer(to, target):
|
|
250
|
-
steps.
|
|
251
|
-
|
|
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,))
|
|
252
267
|
|
|
253
268
|
|
|
254
269
|
def plugin_findings(latest: dict, installed: list[dict]) -> list[dict]:
|
|
@@ -292,7 +307,7 @@ def content_findings(project: Path, latest: dict) -> list[dict]:
|
|
|
292
307
|
try:
|
|
293
308
|
lock = read_json(lock_path)
|
|
294
309
|
except (OSError, ValueError) as exc:
|
|
295
|
-
return [{"pack": None, "status": "error", "error":
|
|
310
|
+
return [{"pack": None, "status": "error", "error": _("{0} 无法解析:{1}").format(CONTENT_LOCK, exc)}]
|
|
296
311
|
findings = []
|
|
297
312
|
for pack_id, entry in sorted(lock.get("packs", {}).items()):
|
|
298
313
|
plugin = entry.get("plugin") or pack_id.split("/", 1)[0]
|
|
@@ -333,11 +348,11 @@ def opinion_findings(project: Path, installed: list[dict]) -> list[dict]:
|
|
|
333
348
|
return []
|
|
334
349
|
manager = locate_companion("opinion-manager", installed)
|
|
335
350
|
if manager is None:
|
|
336
|
-
return [{"status": "unknown", "error": "未找到已安装的 opinion-manager"}]
|
|
351
|
+
return [{"status": "unknown", "error": _("未找到已安装的 opinion-manager")}]
|
|
337
352
|
try:
|
|
338
353
|
profile = read_json(lock_path)["profile"]
|
|
339
354
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
340
|
-
return [{"status": "error", "error":
|
|
355
|
+
return [{"status": "error", "error": _("{0} 无法解析:{1}").format(OPINION_LOCK, exc)}]
|
|
341
356
|
with tempfile.TemporaryDirectory() as temporary:
|
|
342
357
|
# macOS temporary paths live under /var, a symlink opinion-manager refuses.
|
|
343
358
|
profile_file = Path(temporary).resolve() / "profile.json"
|
|
@@ -352,14 +367,174 @@ def opinion_findings(project: Path, installed: list[dict]) -> list[dict]:
|
|
|
352
367
|
except ValueError:
|
|
353
368
|
return [{"status": "error", "error": (completed.stderr or completed.stdout).strip()[:300]}]
|
|
354
369
|
if not result.get("ok", False):
|
|
355
|
-
return [{"status": "error", "error": result.get("error", "opinion-manager updates 失败")}]
|
|
370
|
+
return [{"status": "error", "error": result.get("error", _("opinion-manager updates 失败"))}]
|
|
356
371
|
return [{"status": "update-available" if result["updates"] else "current", "updates": result["updates"]}]
|
|
357
372
|
|
|
358
373
|
|
|
374
|
+
# ---------------------------------------------------------------- project scaffold
|
|
375
|
+
|
|
376
|
+
PROFILE_FILE = Path(".agents/moe.sakanano.agent-pack/project.json")
|
|
377
|
+
_SCAFFOLD_CACHE: dict[str, Any] = {}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def profile_root(project: Path | None) -> Path | None:
|
|
381
|
+
"""The project root that holds the Project Profile, searching upwards from ``project``."""
|
|
382
|
+
if project is None:
|
|
383
|
+
return None
|
|
384
|
+
base = project.expanduser().resolve()
|
|
385
|
+
return next((path for path in [base, *base.parents] if (path / PROFILE_FILE).is_file()), None)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def scaffold_module(env: Env) -> Any | None:
|
|
389
|
+
"""agents-init's scaffold module (the sibling copy first, then the newest installed one); None when too old."""
|
|
390
|
+
installed = installed_plugins(env, {"agents-init"})
|
|
391
|
+
copies = [PLUGIN_ROOT.parent / "agents-init", *(Path(item["path"]) for item in installed)]
|
|
392
|
+
usable = [path for path in copies if (path / "skills/agents-init/scripts/scaffold.py").is_file()]
|
|
393
|
+
if not usable:
|
|
394
|
+
return None
|
|
395
|
+
sibling = PLUGIN_ROOT.parent / "agents-init"
|
|
396
|
+
root = sibling if sibling in usable else max(usable, key=lambda p: version_key(_manifest_version(p) or "") or (0,))
|
|
397
|
+
scripts = root / "skills/agents-init/scripts"
|
|
398
|
+
key = str(scripts)
|
|
399
|
+
if key not in _SCAFFOLD_CACHE:
|
|
400
|
+
import importlib.util
|
|
401
|
+
spec = importlib.util.spec_from_file_location(f"agenthelm_scaffold_{len(_SCAFFOLD_CACHE)}", scripts / "scaffold.py")
|
|
402
|
+
module = importlib.util.module_from_spec(spec)
|
|
403
|
+
sys.modules[spec.name] = module
|
|
404
|
+
spec.loader.exec_module(module) # type: ignore[union-attr]
|
|
405
|
+
_SCAFFOLD_CACHE[key] = module
|
|
406
|
+
return _SCAFFOLD_CACHE[key]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def scaffold_report(env: Env, project: Path | None, *, reprompt: bool = False) -> dict | None:
|
|
410
|
+
"""Read-only comparison of the project with the expected scaffold; None outside a project."""
|
|
411
|
+
root = profile_root(project)
|
|
412
|
+
if root is None:
|
|
413
|
+
return None
|
|
414
|
+
base = {"available": False, "root": str(root), "findings": [], "skipped": [], "error": ""}
|
|
415
|
+
module = scaffold_module(env)
|
|
416
|
+
if module is None:
|
|
417
|
+
return {**base, "error": _("已安装的 agents-init 没有脚手架检查(更新 agents-init 后再检查)")}
|
|
418
|
+
try:
|
|
419
|
+
return {**module.check(root, state_dir=env.state_dir, reprompt=reprompt), "available": True,
|
|
420
|
+
"root": str(root), "error": ""}
|
|
421
|
+
except Exception as exc: # noqa: BLE001 - a broken Profile must not break plugin and content checks
|
|
422
|
+
return {**base, "error": str(exc)}
|
|
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)
|
|
435
|
+
REBUILDABLE_DIRS = {"node_modules", ".venv", "venv", "__pycache__", ".pytest_cache", ".cache", "dist", "build", "target",
|
|
436
|
+
".tox", ".next", ".gradle"}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def sync_folder(root: Path) -> dict | None:
|
|
440
|
+
"""The sync service whose folder holds the project (by path), and the rebuildable directories inside it."""
|
|
441
|
+
resolved = root.resolve()
|
|
442
|
+
service = next((name for name, pattern in SYNC_SERVICES if any(pattern.match(part) for part in resolved.parts)), None)
|
|
443
|
+
if service is None:
|
|
444
|
+
return None
|
|
445
|
+
found: list[str] = []
|
|
446
|
+
for current, directories, _files in os.walk(resolved):
|
|
447
|
+
depth = len(Path(current).relative_to(resolved).parts)
|
|
448
|
+
descend = []
|
|
449
|
+
for name in directories:
|
|
450
|
+
if name in REBUILDABLE_DIRS:
|
|
451
|
+
found.append((Path(current) / name).relative_to(resolved).as_posix())
|
|
452
|
+
elif name != ".git" and depth < 1: # 只看根目录和它的下一层
|
|
453
|
+
descend.append(name)
|
|
454
|
+
directories[:] = descend
|
|
455
|
+
return {"service": service, "path": str(resolved), "rebuildable": sorted(found)}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def machine_findings(env: Env, project: Path | None, *, reprompt: bool = False, quick: bool = False) -> dict:
|
|
459
|
+
"""Findings about this machine only (no user-level runtime ...); their decisions live in the user state directory."""
|
|
460
|
+
root = profile_root(project)
|
|
461
|
+
result: dict = {"findings": [], "skipped": []}
|
|
462
|
+
if root is None:
|
|
463
|
+
return result
|
|
464
|
+
import agenthelm_runtime # noqa: PLC0415 - shipped next to this file
|
|
465
|
+
|
|
466
|
+
findings = []
|
|
467
|
+
data = agenthelm_runtime.report(env, None)
|
|
468
|
+
if data["current"] is None:
|
|
469
|
+
text = agenthelm_runtime.missing_text(data, agenthelm_runtime.command_status())
|
|
470
|
+
findings.append({"id": "local:runtime-missing", "kind": "runtime-missing", "level": "suggest", "scope": "machine",
|
|
471
|
+
"title": _("这台机器还没有安装用户级运行时"), "reason": text["reason"], "impact": text["impact"],
|
|
472
|
+
"fingerprint": "runtime-missing", "fix": "install-runtime", "next": text["label"]})
|
|
473
|
+
where = sync_folder(root)
|
|
474
|
+
if where is not None:
|
|
475
|
+
extra = _(";其中有可重建目录:{0}").format(_('、').join(where['rebuildable'][:6])) if where["rebuildable"] else ""
|
|
476
|
+
findings.append({"id": "local:sync-folder", "kind": "sync-folder", "level": "suggest", "scope": "machine",
|
|
477
|
+
"title": _("项目位于同步盘({0})").format(where['service']),
|
|
478
|
+
"reason": f"{where['path']}{extra}",
|
|
479
|
+
"impact": _("多台机器共用同一份项目时:项目文件随同步盘走,但运行时、缓存与 wf 的台账是每台机器各一份;"
|
|
480
|
+
"可重建目录(依赖、构建产物)同步很慢,也容易产生冲突副本"),
|
|
481
|
+
"fingerprint": sha256_text(where["service"]), "fix": "sync-folder",
|
|
482
|
+
"next": _("运行 agenthelm fix sync-folder 查看指引(只给方案,不改文件、不删内容);不需要就 fix --skip local:sync-folder"),
|
|
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 查看详情")})
|
|
494
|
+
module = scaffold_module(env)
|
|
495
|
+
if module is None or not findings:
|
|
496
|
+
result["findings"] = findings
|
|
497
|
+
return result
|
|
498
|
+
decisions = module.load_decisions(root, env.state_dir)
|
|
499
|
+
result["findings"], result["skipped"] = module.partition(findings, decisions, reprompt=reprompt)
|
|
500
|
+
return result
|
|
501
|
+
|
|
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
|
+
|
|
359
527
|
# ---------------------------------------------------------------- report
|
|
360
528
|
|
|
529
|
+
def scaffold_fingerprint(env: Env, scaffold: dict | None) -> dict | None:
|
|
530
|
+
module = scaffold_module(env)
|
|
531
|
+
if scaffold is None or module is None or not scaffold.get("available"):
|
|
532
|
+
return None
|
|
533
|
+
return module.fingerprint(scaffold)
|
|
534
|
+
|
|
535
|
+
|
|
361
536
|
def run_check(env: Env, project: Path | None, *, latest_source: str | None = None, offline: bool = False,
|
|
362
|
-
max_age: int = CACHE_TTL_SECONDS, hook_mode: bool = False) -> dict:
|
|
537
|
+
max_age: int = CACHE_TTL_SECONDS, hook_mode: bool = False, reprompt: bool = False) -> dict:
|
|
363
538
|
latest, origin = load_latest(
|
|
364
539
|
env, latest_source, offline=offline, max_age=max_age,
|
|
365
540
|
timeout=HOOK_FETCH_TIMEOUT_SECONDS if hook_mode else FETCH_TIMEOUT_SECONDS, backoff=hook_mode,
|
|
@@ -369,6 +544,10 @@ def run_check(env: Env, project: Path | None, *, latest_source: str | None = Non
|
|
|
369
544
|
plugins = plugin_findings(latest, installed)
|
|
370
545
|
content = content_findings(project, latest) if project else []
|
|
371
546
|
opinion = opinion_findings(project, installed) if project else []
|
|
547
|
+
scaffold = scaffold_report(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"))
|
|
550
|
+
scaffold_active = len(scaffold["findings"]) if scaffold else 0
|
|
372
551
|
behind = [item for item in plugins if item["status"] == "behind"]
|
|
373
552
|
legacy = [item for item in plugins if item["client"] == "legacy"]
|
|
374
553
|
content_updates = [item for item in content if item["status"] in {"update-available", "conflict"}]
|
|
@@ -380,14 +559,20 @@ def run_check(env: Env, project: Path | None, *, latest_source: str | None = Non
|
|
|
380
559
|
"plugins": plugins,
|
|
381
560
|
"content": content,
|
|
382
561
|
"opinion": opinion,
|
|
562
|
+
"scaffold": scaffold,
|
|
563
|
+
"machine": machine,
|
|
383
564
|
"summary": {
|
|
565
|
+
"scaffold_findings": scaffold_active,
|
|
566
|
+
"scaffold_skipped": len(scaffold["skipped"]) if scaffold else 0,
|
|
567
|
+
"local_only_attention": local_attention,
|
|
384
568
|
"plugins_behind": len(behind),
|
|
385
569
|
"breaking": any(item["breaking"] for item in behind),
|
|
386
570
|
"content_updates": len(content_updates),
|
|
387
571
|
"content_conflicts": sum(1 for item in content if item["status"] == "conflict"),
|
|
388
572
|
"opinion_updates": len(opinion_updates),
|
|
389
573
|
"legacy_installs": len(legacy),
|
|
390
|
-
"up_to_date": not (behind or content_updates or opinion_updates or legacy
|
|
574
|
+
"up_to_date": not (behind or content_updates or opinion_updates or legacy or scaffold_active
|
|
575
|
+
or local_attention),
|
|
391
576
|
},
|
|
392
577
|
}
|
|
393
578
|
|
|
@@ -406,66 +591,110 @@ def one_line(report: dict) -> str | None:
|
|
|
406
591
|
return None
|
|
407
592
|
parts = []
|
|
408
593
|
if summary["plugins_behind"]:
|
|
409
|
-
parts.append(
|
|
594
|
+
parts.append(_("{0} 个插件可更新").format(summary['plugins_behind']) + (_("(含不兼容变更)") if summary["breaking"] else ""))
|
|
410
595
|
if summary["content_updates"]:
|
|
411
|
-
parts.append(
|
|
412
|
-
+ (
|
|
596
|
+
parts.append(_("{0} 个模板/样例包可更新").format(summary['content_updates'])
|
|
597
|
+
+ (_("({0} 个本地有改动,更新时逐文件确认)").format(summary['content_conflicts']) if summary["content_conflicts"] else ""))
|
|
413
598
|
if summary["opinion_updates"]:
|
|
414
|
-
parts.append("Opinion 有新版本模板或规则")
|
|
599
|
+
parts.append(_("Opinion 有新版本模板或规则"))
|
|
415
600
|
if summary["legacy_installs"]:
|
|
416
|
-
parts.append(
|
|
417
|
-
|
|
601
|
+
parts.append(_("{0} 个旧方式安装待迁移").format(summary['legacy_installs']))
|
|
602
|
+
if summary["scaffold_findings"]:
|
|
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)
|
|
418
607
|
|
|
419
608
|
|
|
420
|
-
def
|
|
609
|
+
def render_scaffold(report: dict, show_skipped: bool) -> list[str]:
|
|
610
|
+
scaffold = report.get("scaffold")
|
|
611
|
+
lines = []
|
|
612
|
+
if scaffold is None:
|
|
613
|
+
return lines
|
|
614
|
+
if scaffold.get("error"):
|
|
615
|
+
lines.append(_(" [脚手架] 无法检查:{0}").format(scaffold['error']))
|
|
616
|
+
return lines
|
|
617
|
+
for item in scaffold["findings"]:
|
|
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))
|
|
621
|
+
for message in scaffold["decisions"]["errors"]:
|
|
622
|
+
lines.append(_(" [脚手架] 决定文件有问题:{0}").format(message))
|
|
623
|
+
if scaffold["decisions"]["conflict_copies"]:
|
|
624
|
+
lines.append(_(" [脚手架] 同步服务留下了决定文件的冲突副本:")
|
|
625
|
+
+ _("、").join(scaffold["decisions"]["conflict_copies"]) + _("(合并到 check-decisions.json 后删除)"))
|
|
626
|
+
if not scaffold["findings"] and not scaffold["decisions"]["errors"]:
|
|
627
|
+
lines.append(_(" [脚手架] 与期望一致") + (_("({0} 项已跳过)").format(len(scaffold['skipped'])) if scaffold["skipped"] else ""))
|
|
628
|
+
skipped = [*scaffold["skipped"], *report.get("machine", {}).get("skipped", [])]
|
|
629
|
+
if show_skipped:
|
|
630
|
+
for item in skipped:
|
|
631
|
+
decision = item["decision"]
|
|
632
|
+
lines.append(_(" [已跳过·{0}] {1}:{2} {3}").format(decision['decision'], item['id'], decision.get('reason') or _('(未写理由)'), decision.get('decided_at', '')))
|
|
633
|
+
if not skipped:
|
|
634
|
+
lines.append(_(" 没有被跳过的项。"))
|
|
635
|
+
elif skipped:
|
|
636
|
+
lines.append(_(" (另有 {0} 项已被你跳过:check --show-skipped 查看)").format(len(skipped)))
|
|
637
|
+
return lines
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def render_machine(report: dict) -> list[str]:
|
|
641
|
+
lines = []
|
|
642
|
+
for item in report.get("machine", {}).get("findings", []):
|
|
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']))
|
|
644
|
+
return lines
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def render_text(report: dict, *, show_skipped: bool = False) -> str:
|
|
421
648
|
lines = []
|
|
422
649
|
origin = report["latest"]["source"]
|
|
423
|
-
lines.append(
|
|
650
|
+
lines.append(_("云端版本源:{0}").format(origin.get('source')) + (_("(缓存)") if origin.get("cached") else ""))
|
|
424
651
|
if not report["plugins"]:
|
|
425
|
-
lines.append("未发现已安装的 agenthelm 插件。")
|
|
652
|
+
lines.append(_("未发现已安装的 agenthelm 插件。"))
|
|
426
653
|
for item in report["plugins"]:
|
|
427
|
-
label = {"behind": "可更新", "current": "最新", "ahead": "高于云端", "unknown": "版本未知",
|
|
428
|
-
"legacy": "旧方式安装,待迁移"}[item["status"]]
|
|
654
|
+
label = {"behind": _("可更新"), "current": _("最新"), "ahead": _("高于云端"), "unknown": _("版本未知"),
|
|
655
|
+
"legacy": _("旧方式安装,待迁移")}[item["status"]]
|
|
429
656
|
if item["status"] == "legacy":
|
|
430
|
-
lines.append(
|
|
657
|
+
lines.append(_(" [旧方式] {0} {1} {2}").format(item['plugin'], item.get('version'), label))
|
|
431
658
|
continue
|
|
432
659
|
line = f" [{item['client']}] {item['plugin']} {item.get('version')} → {item.get('latest')} {label}"
|
|
433
660
|
if item["status"] == "behind":
|
|
434
|
-
kinds = {"code": "代码", "content": "仅模板/样例", "both": "代码与模板"}
|
|
661
|
+
kinds = {"code": _("代码"), "content": _("仅模板/样例"), "both": _("代码与模板")}
|
|
435
662
|
if item["change_kinds"]:
|
|
436
|
-
line += "(" + "、".join(kinds[kind] for kind in item["change_kinds"]) + ")"
|
|
663
|
+
line += _("(") + _("、").join(kinds[kind] for kind in item["change_kinds"]) + _(")")
|
|
437
664
|
if item["breaking"]:
|
|
438
|
-
line += "(含不兼容变更)"
|
|
665
|
+
line += _("(含不兼容变更)")
|
|
439
666
|
for step in item["steps"]:
|
|
440
667
|
if step.get("breaking"):
|
|
441
|
-
line +=
|
|
668
|
+
line += _("\n {0} 不兼容:{1}").format(step['to'], step.get('summary'))
|
|
442
669
|
line += "".join(f"\n - {action}" for action in step.get("actions") or [])
|
|
443
670
|
hint = UPDATE_HINTS.get(item["client"])
|
|
444
671
|
if hint:
|
|
445
|
-
line +=
|
|
672
|
+
line += _("\n 更新:{0}").format(hint.format(plugin=item['plugin']))
|
|
446
673
|
lines.append(line)
|
|
447
674
|
for item in report["content"]:
|
|
448
675
|
if item.get("pack") is None:
|
|
449
|
-
lines.append(
|
|
676
|
+
lines.append(_(" 内容锁:{0}").format(item.get('error')))
|
|
450
677
|
continue
|
|
451
|
-
label = {"current": "最新", "update-available": "可更新(本地未改动)",
|
|
452
|
-
"conflict": "可更新(本地有改动,更新时逐文件确认)", "unknown": "云端未列出"}[item["status"]]
|
|
453
|
-
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)
|
|
454
681
|
if item["locally_changed"]:
|
|
455
|
-
line += "\n 本地已改动:" + "、".join(item["locally_changed"])
|
|
682
|
+
line += _("\n 本地已改动:") + _("、").join(item["locally_changed"])
|
|
456
683
|
lines.append(line)
|
|
457
684
|
for item in report["opinion"]:
|
|
458
685
|
if item["status"] == "update-available":
|
|
459
|
-
refs = ";".join(f"{update['current']} → {update['available'][-1]}" for update in item["updates"])
|
|
460
|
-
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))
|
|
461
688
|
elif item["status"] in {"error", "unknown"}:
|
|
462
689
|
lines.append(f" [Opinion] {item.get('error')}")
|
|
690
|
+
lines += render_scaffold(report, show_skipped)
|
|
691
|
+
lines += render_machine(report)
|
|
463
692
|
if not report["plugins"]:
|
|
464
|
-
lines.append(
|
|
693
|
+
lines.append(_("运行 `{0} install` 预览安装。").format(CLI))
|
|
465
694
|
else:
|
|
466
695
|
if any(item["status"] == "legacy" for item in report["plugins"]):
|
|
467
|
-
lines.append(
|
|
468
|
-
lines.append(one_line(report) or "全部为最新。")
|
|
696
|
+
lines.append(_("旧方式安装可用 `{0}` 预览迁移。").format(UPDATE_HINTS['legacy']))
|
|
697
|
+
lines.append(one_line(report) or _("全部为最新。"))
|
|
469
698
|
return "\n".join(lines) + "\n"
|
|
470
699
|
|
|
471
700
|
|
|
@@ -513,7 +742,7 @@ CLIENTS = ("claude", "codex", "cursor")
|
|
|
513
742
|
def github_slug(latest: dict) -> str:
|
|
514
743
|
match = re.match(r"https://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$", str(latest.get("repository", "")))
|
|
515
744
|
if not match:
|
|
516
|
-
raise CheckError(
|
|
745
|
+
raise CheckError(_("latest.json 的 repository 不是 GitHub 仓库:{0}").format(latest.get('repository')))
|
|
517
746
|
return match.group(1)
|
|
518
747
|
|
|
519
748
|
|
|
@@ -533,9 +762,9 @@ def _safe_extract(archive: Path, destination: Path) -> None:
|
|
|
533
762
|
for member in bundle.getmembers():
|
|
534
763
|
target = (destination / member.name).resolve()
|
|
535
764
|
if destination.resolve() not in target.parents and target != destination.resolve():
|
|
536
|
-
raise CheckError(
|
|
765
|
+
raise CheckError(_("发布包含越界路径:{0}").format(member.name))
|
|
537
766
|
if not (member.isfile() or member.isdir()):
|
|
538
|
-
raise CheckError(
|
|
767
|
+
raise CheckError(_("发布包含不支持的条目:{0}").format(member.name))
|
|
539
768
|
try:
|
|
540
769
|
bundle.extractall(destination, filter="data") # noqa: S202 - members validated above
|
|
541
770
|
except TypeError: # Python without extraction filters
|
|
@@ -566,25 +795,25 @@ def release_source(latest: dict, workdir: Path, source: Path | None = None) -> P
|
|
|
566
795
|
if source is not None:
|
|
567
796
|
root = source.expanduser().resolve()
|
|
568
797
|
if not (root / "plugins").is_dir():
|
|
569
|
-
raise CheckError(
|
|
798
|
+
raise CheckError(_("{0} 不含 plugins/ 目录").format(root))
|
|
570
799
|
return root
|
|
571
800
|
release = latest.get("release") or github_release(latest)
|
|
572
801
|
tag, digest = release.get("tag"), release.get("tarball_sha256")
|
|
573
802
|
if not tag or not digest:
|
|
574
|
-
raise CheckError("云端 latest.json 没有发布 tag 与摘要;请用 --source 指定本地仓库")
|
|
803
|
+
raise CheckError(_("云端 latest.json 没有发布 tag 与摘要;请用 --source 指定本地仓库"))
|
|
575
804
|
url = release.get("tarball_url") or f"https://github.com/{github_slug(latest)}/releases/download/{tag}/agenthelm-{tag}.tar.gz"
|
|
576
805
|
archive = workdir / "release.tar.gz"
|
|
577
806
|
request = urllib.request.Request(url, headers={"User-Agent": "agenthelm"})
|
|
578
807
|
with urllib.request.urlopen(request, timeout=60) as response, archive.open("wb") as handle: # noqa: S310
|
|
579
808
|
shutil.copyfileobj(response, handle)
|
|
580
809
|
if sha256_file(archive) != digest:
|
|
581
|
-
raise CheckError(
|
|
810
|
+
raise CheckError(_("发布包摘要不符:{0}").format(url))
|
|
582
811
|
extracted = workdir / "release"
|
|
583
812
|
extracted.mkdir()
|
|
584
813
|
_safe_extract(archive, extracted)
|
|
585
814
|
roots = [path for path in extracted.iterdir() if path.is_dir()]
|
|
586
815
|
if len(roots) != 1 or not (roots[0] / "plugins").is_dir():
|
|
587
|
-
raise CheckError("发布包结构不符合预期")
|
|
816
|
+
raise CheckError(_("发布包结构不符合预期"))
|
|
588
817
|
return roots[0]
|
|
589
818
|
|
|
590
819
|
|
|
@@ -642,7 +871,7 @@ def find_cli(env: Env, name: str) -> str | None:
|
|
|
642
871
|
def _cli(env: Env, name: str, *args: str) -> tuple[list[str], str | None]:
|
|
643
872
|
"""The command (using the resolved executable) and why it cannot run, if it cannot."""
|
|
644
873
|
found = find_cli(env, name)
|
|
645
|
-
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()))
|
|
646
875
|
|
|
647
876
|
|
|
648
877
|
def install_steps(env: Env, latest: dict, clients: list[str], plugins: list[str], source: Path | None,
|
|
@@ -652,18 +881,18 @@ def install_steps(env: Env, latest: dict, clients: list[str], plugins: list[str]
|
|
|
652
881
|
for client in clients:
|
|
653
882
|
if client == "claude":
|
|
654
883
|
command, blocked = _cli(env, "claude", "plugin", "marketplace", "add", slug)
|
|
655
|
-
steps.append(Step("install", "claude",
|
|
884
|
+
steps.append(Step("install", "claude", _("Claude Code 添加 agenthelm 市场({0})").format(slug), command, blocked=blocked))
|
|
656
885
|
for plugin in plugins:
|
|
657
886
|
command, blocked = _cli(env, "claude", "plugin", "install", f"{plugin}@{MARKETPLACE}")
|
|
658
|
-
steps.append(Step("install", "claude",
|
|
887
|
+
steps.append(Step("install", "claude", _("Claude Code 安装 {0}").format(plugin), command, blocked=blocked))
|
|
659
888
|
elif client == "codex":
|
|
660
889
|
command, blocked = _cli(env, "codex", "plugin", "marketplace", "add", slug)
|
|
661
|
-
steps.append(Step("install", "codex",
|
|
890
|
+
steps.append(Step("install", "codex", _("Codex 添加 agenthelm 市场({0})").format(slug), command, blocked=blocked))
|
|
662
891
|
for plugin in plugins:
|
|
663
892
|
command, blocked = _cli(env, "codex", "plugin", "add", f"{plugin}@{MARKETPLACE}")
|
|
664
|
-
steps.append(Step("install", "codex",
|
|
893
|
+
steps.append(Step("install", "codex", _("Codex 安装 {0}").format(plugin), command, blocked=blocked))
|
|
665
894
|
elif client == "cursor":
|
|
666
|
-
steps.append(cursor_step(latest, plugins, source, home, "安装"))
|
|
895
|
+
steps.append(cursor_step(latest, plugins, source, home, _("安装")))
|
|
667
896
|
return steps
|
|
668
897
|
|
|
669
898
|
|
|
@@ -675,7 +904,7 @@ def cursor_step(latest: dict, plugins: list[str], source: Path | None, home: Pat
|
|
|
675
904
|
return [config.install_plugin(root / "plugins" / plugin, "cursor", home=home, apply=apply,
|
|
676
905
|
replace=True, recovery_root=recovery_root)
|
|
677
906
|
for plugin in plugins]
|
|
678
|
-
return Step("install", "cursor",
|
|
907
|
+
return Step("install", "cursor", _("Cursor {0} {1} 到 ~/.cursor/plugins/local(重启 Cursor 生效)").format(verb, _('、').join(plugins)),
|
|
679
908
|
action=run)
|
|
680
909
|
|
|
681
910
|
|
|
@@ -689,18 +918,18 @@ def update_steps(env: Env, report: dict, project: Path | None, source: Path | No
|
|
|
689
918
|
claude = sorted({item["plugin"] for item in behind if item["client"] == "claude"})
|
|
690
919
|
if claude:
|
|
691
920
|
command, blocked = _cli(env, "claude", "plugin", "marketplace", "update", MARKETPLACE)
|
|
692
|
-
steps.append(Step("update", "claude", "刷新 Claude Code 的 agenthelm 市场", command, blocked=blocked))
|
|
921
|
+
steps.append(Step("update", "claude", _("刷新 Claude Code 的 agenthelm 市场"), command, blocked=blocked))
|
|
693
922
|
for item in [entry for entry in behind if entry["client"] == "claude"]:
|
|
694
923
|
extra = ["--scope", item["scope"]] if item.get("scope") and item["scope"] != "user" else []
|
|
695
924
|
command, blocked = _cli(env, "claude", "plugin", "update", f"{item['plugin']}@{MARKETPLACE}", *extra)
|
|
696
|
-
steps.append(Step("update", "claude",
|
|
925
|
+
steps.append(Step("update", "claude", _("Claude Code 更新 {0} → {1}").format(item['plugin'], item['latest']),
|
|
697
926
|
command, blocked=blocked))
|
|
698
927
|
if any(item["client"] == "codex" for item in behind):
|
|
699
928
|
command, blocked = _cli(env, "codex", "plugin", "marketplace", "upgrade", MARKETPLACE)
|
|
700
|
-
steps.append(Step("update", "codex", "Codex 升级 agenthelm 市场与已装插件", command, blocked=blocked))
|
|
929
|
+
steps.append(Step("update", "codex", _("Codex 升级 agenthelm 市场与已装插件"), command, blocked=blocked))
|
|
701
930
|
cursor = sorted({item["plugin"] for item in behind if item["client"] == "cursor"})
|
|
702
931
|
if cursor:
|
|
703
|
-
steps.append(cursor_step(latest, cursor, source, home, "更新"))
|
|
932
|
+
steps.append(cursor_step(latest, cursor, source, home, _("更新")))
|
|
704
933
|
if only in (None, "content") and project is not None:
|
|
705
934
|
for item in report["content"]:
|
|
706
935
|
if item.get("status") not in {"update-available", "conflict"}:
|
|
@@ -712,15 +941,15 @@ def update_steps(env: Env, report: dict, project: Path | None, source: Path | No
|
|
|
712
941
|
def content_step(env: Env, item: dict, report: dict, project: Path, accept: list[str], keep: list[str],
|
|
713
942
|
recovery_dir: Path | None) -> Step:
|
|
714
943
|
if item["plugin"] != "agents-init":
|
|
715
|
-
return Step("content", None,
|
|
944
|
+
return Step("content", None, _("{0}:没有可用的刷新工具").format(item['pack']), blocked="unsupported")
|
|
716
945
|
stale = any(entry["plugin"] == "agents-init" and entry["status"] == "behind" for entry in report["plugins"])
|
|
717
|
-
description =
|
|
946
|
+
description = _("重新渲染 {0} {1} → {2}").format(item['pack'], item['version'], item['latest'])
|
|
718
947
|
|
|
719
948
|
def run(apply: bool, _recovery_root: Path | None) -> dict:
|
|
720
949
|
installed = installed_plugins(env, {"agents-init"})
|
|
721
950
|
manager = locate_companion("agents-init", installed)
|
|
722
951
|
if manager is None:
|
|
723
|
-
raise CheckError("未找到已安装的 agents-init")
|
|
952
|
+
raise CheckError(_("未找到已安装的 agents-init"))
|
|
724
953
|
command = [sys.executable, str(manager / "skills/agents-init/scripts/init_project.py"),
|
|
725
954
|
"--project", str(project), "--mode", "refresh", "--output", "json"]
|
|
726
955
|
for path in accept:
|
|
@@ -738,7 +967,7 @@ def content_step(env: Env, item: dict, report: dict, project: Path, accept: list
|
|
|
738
967
|
except ValueError as exc:
|
|
739
968
|
raise CheckError((completed.stderr or completed.stdout).strip()[:300]) from exc
|
|
740
969
|
|
|
741
|
-
blocked = "先更新 agents-init 插件,再用新模板预览" if stale else None
|
|
970
|
+
blocked = _("先更新 agents-init 插件,再用新模板预览") if stale else None
|
|
742
971
|
return Step("content", None, description, action=run, blocked=blocked)
|
|
743
972
|
|
|
744
973
|
|
|
@@ -940,86 +1169,97 @@ def doctor(env: Env, report: dict) -> dict:
|
|
|
940
1169
|
clients = [item["client"] for item in items]
|
|
941
1170
|
if "legacy" in clients and any(client in {"claude", "codex"} for client in clients):
|
|
942
1171
|
issues.append({"plugin": plugin, "issue": "duplicate",
|
|
943
|
-
"detail":
|
|
1172
|
+
"detail": _("同时存在市场安装与旧方式安装,运行 {0} migrate").format(CLI)})
|
|
944
1173
|
versions = {item.get("version") for item in items if item["client"] != "legacy"}
|
|
945
1174
|
if len(versions) > 1:
|
|
946
1175
|
issues.append({"plugin": plugin, "issue": "version-skew",
|
|
947
|
-
"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)})
|
|
948
1177
|
names = set(report["_latest"].get("plugins", {}))
|
|
949
1178
|
installed_names = {item["plugin"] for item in report["plugins"] if item["client"] != "legacy"}
|
|
950
1179
|
if installed_names and installed_names != names:
|
|
951
1180
|
issues.append({"plugin": None, "issue": "incomplete",
|
|
952
|
-
"detail": "缺少:" + "、".join(sorted(names - installed_names))})
|
|
1181
|
+
"detail": _("缺少:") + _("、").join(sorted(names - installed_names))})
|
|
953
1182
|
if not update_check_enabled(env):
|
|
954
|
-
issues.append({"plugin": None, "issue": "update-check-disabled", "detail": "会话内更新检查已关闭"})
|
|
1183
|
+
issues.append({"plugin": None, "issue": "update-check-disabled", "detail": _("会话内更新检查已关闭")})
|
|
955
1184
|
return {"ok": not [issue for issue in issues if issue["issue"] != "update-check-disabled"],
|
|
956
1185
|
"issues": issues, "legacy": legacy_artifacts(env, names), "summary": report["summary"]}
|
|
957
1186
|
|
|
958
1187
|
|
|
959
1188
|
def parser() -> argparse.ArgumentParser:
|
|
960
|
-
result = argparse.ArgumentParser(prog="agenthelm", description=
|
|
1189
|
+
result = argparse.ArgumentParser(prog="agenthelm", description=_("agenthelm:在 Claude Code、Codex、Cursor 间安装、检查与更新 agenthelm 插件。"))
|
|
961
1190
|
sub = result.add_subparsers(dest="command", required=True)
|
|
962
1191
|
|
|
963
1192
|
def common(command: argparse.ArgumentParser, *, project: bool = True) -> None:
|
|
964
|
-
command.add_argument("--latest", help="latest.json 的 URL 或本地路径(测试、离线镜像)")
|
|
1193
|
+
command.add_argument("--latest", help=_("latest.json 的 URL 或本地路径(测试、离线镜像)"))
|
|
965
1194
|
command.add_argument("--output", choices=("text", "json"), default="text")
|
|
966
1195
|
command.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
967
1196
|
if project:
|
|
968
|
-
command.add_argument("--project", type=Path, default=None, help="项目目录;默认当前目录")
|
|
969
|
-
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=_("不检查项目内容"))
|
|
970
1199
|
|
|
971
|
-
check = sub.add_parser("check", help="只读检查插件、模板/样例与 Opinion 是否落后于云端")
|
|
1200
|
+
check = sub.add_parser("check", help=_("只读检查插件、模板/样例与 Opinion 是否落后于云端"))
|
|
972
1201
|
common(check)
|
|
973
|
-
check.add_argument("--offline", action="store_true", help="只用缓存,不联网")
|
|
974
|
-
check.add_argument("--refresh", action="store_true", help="忽略一天的缓存,立即联网")
|
|
975
|
-
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=_("会话开始时静默调用:有更新才输出一行"))
|
|
976
1205
|
check.add_argument("--hook-format", choices=("text", "claude-hook"), default="text")
|
|
977
|
-
check.add_argument("--strict", action="store_true", help="
|
|
978
|
-
|
|
979
|
-
|
|
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=_("忽略已记录的决定,重新评估全部脚手架差异"))
|
|
1209
|
+
check.add_argument("--fingerprint", action="store_true",
|
|
1210
|
+
help=_("输出脚手架指纹(骨架状态 + 受检 Profile 字段 + 内容锁 + 命名规则;不含 Opinion、自有字段、已跳过项)"))
|
|
1211
|
+
check.add_argument("--explain", nargs="?", const="-", metavar=_("对方的 JSON"),
|
|
1212
|
+
help=_("与 --fingerprint 合用:不带参数时逐项列出这台的明细;带另一台机器 check --fingerprint "
|
|
1213
|
+
"--output json 的输出文件时,指出两台差在哪里(有差异退出码 1)"))
|
|
1214
|
+
|
|
1215
|
+
install = sub.add_parser("install", help=_("经各客户端的原生市场安装 agenthelm 插件(默认预览)"))
|
|
980
1216
|
common(install, project=False)
|
|
981
|
-
install.add_argument("--client", action="append", choices=CLIENTS, help="默认安装到检测到的全部客户端")
|
|
982
|
-
install.add_argument("--plugin", action="append", help="默认安装全部插件")
|
|
983
|
-
install.add_argument("--source", type=Path, help="Cursor 安装用的本地仓库(默认下载发布包)")
|
|
984
|
-
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 仓库;可用镜像或本地路径)"))
|
|
985
1221
|
install.add_argument("--apply", action="store_true")
|
|
986
1222
|
|
|
987
|
-
update = sub.add_parser("update", help="更新落后的插件与模板/样例(默认预览,确认后 --apply)")
|
|
1223
|
+
update = sub.add_parser("update", help=_("更新落后的插件与模板/样例(默认预览,确认后 --apply)"))
|
|
988
1224
|
common(update)
|
|
989
1225
|
update.add_argument("--only", choices=("plugins", "content"))
|
|
990
|
-
update.add_argument("--accept", action="append", default=[], help="内容更新:用新模板替换这个本地已改动的文件")
|
|
991
|
-
update.add_argument("--keep", action="append", default=[], help="内容更新:保留这个文件的本地内容")
|
|
992
|
-
update.add_argument("--recovery-dir", type=Path, help="被替换文件的备份目录(项目外)")
|
|
993
|
-
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 更新用的本地仓库(默认下载发布包)"))
|
|
994
1230
|
update.add_argument("--apply", action="store_true")
|
|
995
1231
|
|
|
996
|
-
migrate = sub.add_parser("migrate", help="把旧方式安装(agent-pack 复制 + 软链)迁到市场安装(默认预览)")
|
|
1232
|
+
migrate = sub.add_parser("migrate", help=_("把旧方式安装(agent-pack 复制 + 软链)迁到市场安装(默认预览)"))
|
|
997
1233
|
common(migrate, project=False)
|
|
998
|
-
migrate.add_argument("--install", action="store_true", help="先经市场安装到检测到的客户端")
|
|
1234
|
+
migrate.add_argument("--install", action="store_true", help=_("先经市场安装到检测到的客户端"))
|
|
999
1235
|
migrate.add_argument("--client", action="append", choices=CLIENTS)
|
|
1000
1236
|
migrate.add_argument("--source", type=Path)
|
|
1001
1237
|
migrate.add_argument("--marketplace-source")
|
|
1002
1238
|
migrate.add_argument("--apply", action="store_true")
|
|
1003
1239
|
|
|
1004
|
-
doctor_parser = sub.add_parser("doctor", help="报告重复安装、版本不一致、旧方式残留与检查开关")
|
|
1240
|
+
doctor_parser = sub.add_parser("doctor", help=_("报告重复安装、版本不一致、旧方式残留与检查开关"))
|
|
1005
1241
|
common(doctor_parser)
|
|
1006
1242
|
|
|
1243
|
+
sub.add_parser("fix", add_help=False,
|
|
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)"))
|
|
1007
1247
|
sub.add_parser("runtime", add_help=False,
|
|
1008
|
-
help="用户级运行时:install / update / use / status(日常只需要 Python,不经过 npx)")
|
|
1248
|
+
help=_("用户级运行时:install / update / use / status(日常只需要 Python,不经过 npx)"))
|
|
1009
1249
|
sub.add_parser("project", add_help=False,
|
|
1010
|
-
help="项目自助入口:找资料、看进展、项目指南、检查(不需要 AI Agent;详见 project --help)")
|
|
1011
|
-
locate_parser = sub.add_parser("locate", help="输出插件的已安装位置(最新优先),供下游脚本查找")
|
|
1250
|
+
help=_("项目自助入口:找资料、看进展、项目指南、检查(不需要 AI Agent;详见 project --help)"))
|
|
1251
|
+
locate_parser = sub.add_parser("locate", help=_("输出插件的已安装位置(最新优先),供下游脚本查找"))
|
|
1012
1252
|
locate_parser.add_argument("plugin")
|
|
1013
|
-
locate_parser.add_argument("--version", help="只要这个版本")
|
|
1253
|
+
locate_parser.add_argument("--version", help=_("只要这个版本"))
|
|
1014
1254
|
locate_parser.add_argument("--include-legacy", action="store_true",
|
|
1015
|
-
help="也列出旧方式安装(默认不列,避免清理后才失效)")
|
|
1255
|
+
help=_("也列出旧方式安装(默认不列,避免清理后才失效)"))
|
|
1016
1256
|
locate_parser.add_argument("--output", choices=("text", "json"), default="json")
|
|
1017
1257
|
locate_parser.add_argument("--home", type=Path, help=argparse.SUPPRESS)
|
|
1018
1258
|
return result
|
|
1019
1259
|
|
|
1020
1260
|
|
|
1021
1261
|
def _print_steps(results: list[dict], apply: bool) -> None:
|
|
1022
|
-
labels = {"preview": "预览", "applied": "完成", "blocked": "阻塞", "failed": "失败"}
|
|
1262
|
+
labels = {"preview": _("预览"), "applied": _("完成"), "blocked": _("阻塞"), "failed": _("失败")}
|
|
1023
1263
|
for record in results:
|
|
1024
1264
|
line = f" [{labels.get(record['status'], record['status'])}] {record['description']}"
|
|
1025
1265
|
if record.get("command"):
|
|
@@ -1036,7 +1276,7 @@ def _print_steps(results: list[dict], apply: bool) -> None:
|
|
|
1036
1276
|
line += f"\n {result['hint']}"
|
|
1037
1277
|
print(line)
|
|
1038
1278
|
if not apply and results:
|
|
1039
|
-
print("以上为预览;确认后追加 --apply 执行。")
|
|
1279
|
+
print(_("以上为预览;确认后追加 --apply 执行。"))
|
|
1040
1280
|
|
|
1041
1281
|
|
|
1042
1282
|
def run_project(argv: list[str]) -> int:
|
|
@@ -1047,9 +1287,8 @@ def run_project(argv: list[str]) -> int:
|
|
|
1047
1287
|
usable = [path for path in copies if (path / "runtime" / "project_cli.py").is_file()]
|
|
1048
1288
|
if not usable:
|
|
1049
1289
|
stale = any((path / "plugin.json").is_file() for path in copies)
|
|
1050
|
-
found = "(已安装的 project-orchestrator 版本过旧)" if stale else ""
|
|
1051
|
-
print(
|
|
1052
|
-
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)
|
|
1053
1292
|
return 3
|
|
1054
1293
|
# Prefer the copy shipped next to this agent-pack (same release), then the newest installed one.
|
|
1055
1294
|
sibling = PLUGIN_ROOT.parent / "project-orchestrator"
|
|
@@ -1062,6 +1301,19 @@ def run_project(argv: list[str]) -> int:
|
|
|
1062
1301
|
return project_cli.main(argv)
|
|
1063
1302
|
|
|
1064
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
|
+
|
|
1065
1317
|
def run_runtime(argv: list[str]) -> int:
|
|
1066
1318
|
"""``agenthelm runtime``: the user-level runtime that project launchers use without any AI client."""
|
|
1067
1319
|
sys.path.insert(0, str(RUNTIME_DIR))
|
|
@@ -1070,12 +1322,70 @@ def run_runtime(argv: list[str]) -> int:
|
|
|
1070
1322
|
return agenthelm_runtime.main(argv, sys.modules[__name__])
|
|
1071
1323
|
|
|
1072
1324
|
|
|
1325
|
+
def print_fingerprint(env: Env, report: dict, args: argparse.Namespace, project: Path | None = None) -> int:
|
|
1326
|
+
"""``check --fingerprint [--explain [file]]``: the scaffold fingerprint, and where two machines differ."""
|
|
1327
|
+
result = scaffold_fingerprint(env, report.get("scaffold"))
|
|
1328
|
+
if result is None:
|
|
1329
|
+
reason = (report.get("scaffold") or {}).get("error") or _("当前目录不在 agenthelm 项目里")
|
|
1330
|
+
print(json.dumps({"ok": False, "error": reason}, ensure_ascii=False) if args.output == "json"
|
|
1331
|
+
else _("无法计算脚手架指纹:{0}").format(reason), file=sys.stderr if args.output != "json" else sys.stdout)
|
|
1332
|
+
return 4
|
|
1333
|
+
payload: dict = {"ok": True, "fingerprint": result["fingerprint"], "detail": result["detail"]}
|
|
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']))
|
|
1342
|
+
differences: list[str] = []
|
|
1343
|
+
if args.explain and args.explain != "-":
|
|
1344
|
+
try:
|
|
1345
|
+
other = read_json(Path(args.explain).expanduser())
|
|
1346
|
+
except (OSError, ValueError) as exc:
|
|
1347
|
+
print(_("读不了对方的输出:{0}").format(exc), file=sys.stderr)
|
|
1348
|
+
return 2
|
|
1349
|
+
theirs = other.get("detail") or (other.get("scaffold_fingerprint") or {}).get("detail") or other.get("fingerprint", {}).get("detail")
|
|
1350
|
+
if not isinstance(theirs, dict):
|
|
1351
|
+
print(_("对方的文件里没有指纹明细:请在对方机器运行 agenthelm check --fingerprint --output json 并保存输出"), file=sys.stderr)
|
|
1352
|
+
return 2
|
|
1353
|
+
module = scaffold_module(env)
|
|
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)
|
|
1363
|
+
payload["same"] = not differences
|
|
1364
|
+
payload["differences"] = differences
|
|
1365
|
+
lines.append(_("两台机器的脚手架一致。") if not differences else _("有 {0} 处不同:").format(len(differences)))
|
|
1366
|
+
lines += [f" {item}" for item in differences]
|
|
1367
|
+
elif args.explain:
|
|
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()]
|
|
1370
|
+
if args.output == "json":
|
|
1371
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
1372
|
+
else:
|
|
1373
|
+
print("\n".join(lines))
|
|
1374
|
+
return 1 if differences else 0
|
|
1375
|
+
|
|
1376
|
+
|
|
1073
1377
|
def main(argv: list[str] | None = None) -> int:
|
|
1074
1378
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
1075
1379
|
if argv[:1] == ["project"]:
|
|
1076
1380
|
return run_project(argv[1:])
|
|
1381
|
+
if argv[:1] == ["fix"]:
|
|
1382
|
+
return run_project(["fix", *argv[1:]])
|
|
1077
1383
|
if argv[:1] == ["runtime"]:
|
|
1078
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:])
|
|
1079
1389
|
args = parser().parse_args(argv)
|
|
1080
1390
|
env = current_env(args.home)
|
|
1081
1391
|
home = env.home
|
|
@@ -1094,12 +1404,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1094
1404
|
try:
|
|
1095
1405
|
if args.command == "check":
|
|
1096
1406
|
report = run_check(env, project, latest_source=args.latest, offline=args.offline,
|
|
1097
|
-
max_age=0 if args.refresh else CACHE_TTL_SECONDS)
|
|
1407
|
+
max_age=0 if args.refresh else CACHE_TTL_SECONDS, reprompt=args.reprompt)
|
|
1098
1408
|
report.pop("_latest")
|
|
1409
|
+
if args.fingerprint or args.explain:
|
|
1410
|
+
return print_fingerprint(env, report, args, project)
|
|
1099
1411
|
if args.output == "json":
|
|
1100
1412
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
1101
1413
|
else:
|
|
1102
|
-
sys.stdout.write(render_text(report))
|
|
1414
|
+
sys.stdout.write(render_text(report, show_skipped=args.show_skipped))
|
|
1103
1415
|
return 1 if args.strict and not report["summary"]["up_to_date"] else 0
|
|
1104
1416
|
if args.command == "doctor":
|
|
1105
1417
|
result = doctor(env, run_check(env, project, latest_source=args.latest))
|
|
@@ -1110,9 +1422,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1110
1422
|
print(f" [{issue['issue']}] {issue.get('plugin') or ''} {issue['detail']}".rstrip())
|
|
1111
1423
|
for item in result["legacy"]:
|
|
1112
1424
|
print(f" [legacy] {item['kind']} {item['name']}: {item['path']}")
|
|
1113
|
-
print("没有发现问题。" if result["ok"] and not result["legacy"] else "")
|
|
1425
|
+
print(_("没有发现问题。") if result["ok"] and not result["legacy"] else "")
|
|
1114
1426
|
return 0 if result["ok"] else 1
|
|
1115
|
-
latest,
|
|
1427
|
+
latest, _origin = load_latest(env, args.latest, max_age=0)
|
|
1116
1428
|
if args.command == "install":
|
|
1117
1429
|
clients = args.client or detect_clients(env)
|
|
1118
1430
|
plugins = args.plugin or sorted(latest.get("plugins", {}))
|
|
@@ -1133,7 +1445,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1133
1445
|
payload = {"ok": all(r["status"] != "failed" for r in results), "applied": args.apply,
|
|
1134
1446
|
"steps": results, "summary": report["summary"]}
|
|
1135
1447
|
if not steps:
|
|
1136
|
-
payload["message"] = "没有需要更新的内容。"
|
|
1448
|
+
payload["message"] = _("没有需要更新的内容。")
|
|
1137
1449
|
else: # migrate
|
|
1138
1450
|
names = set(latest.get("plugins", {}))
|
|
1139
1451
|
artifacts = legacy_artifacts(env, names)
|
|
@@ -1160,9 +1472,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1160
1472
|
print(payload["message"])
|
|
1161
1473
|
_print_steps(payload["steps"], args.apply)
|
|
1162
1474
|
for item in payload.get("legacy", []):
|
|
1163
|
-
print(
|
|
1475
|
+
print(_(" [旧安装] {0} {1}: {2}").format(item['kind'], item['name'], item['path']) + (_("(需手动处理)") if item.get("manual") else ""))
|
|
1164
1476
|
if payload.get("recovery"):
|
|
1165
|
-
print(
|
|
1477
|
+
print(_("旧安装已移入:{0}(含 migration.json 清单,可按清单恢复)").format(payload['recovery']))
|
|
1166
1478
|
return 0 if payload["ok"] else 1
|
|
1167
1479
|
|
|
1168
1480
|
|