349 lines
17 KiB
Python
349 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
||||
|
|
"""workspace_mirror —— **工作区副本的同步/漂移自检**(2026-10-04 建)
|
|||
|
|
|
|||
|
|
一句话:**全局技能改了,工作区副本怎么跟上、怎么证明没残留。**
|
|||
|
|
|
|||
|
|
🔴 为什么必须有这个工具(不是"又写个脚本")
|
|||
|
|
· 副本是**整目录拷贝**出去的 ⇒ 全局删掉/新增的文件,副本**不知道**,会静默留着
|
|||
|
|
· 副本的 `roots.env` 是**工作区专属**的 ⇒ ⛔ 不能盲目整目录覆盖(会把工作区指向冲回别人)
|
|||
|
|
· 于是每次同步都变成"人肉算差异+人肉挑例外" ⇒ **迟早漏**(实测已漏过:mut_run.py 差点没补上)
|
|||
|
|
⇒ 正确解法:**让"例外的例外"由工具自己知道**,并且**能自证没漂移**。
|
|||
|
|
|
|||
|
|
用法(三条,都只读或幂等)
|
|||
|
|
1) 查漂移(⛔ 纯只读,⛔ 不写任何文件)—— 用它当体检项:
|
|||
|
|
python workspace_mirror.py --check <工作区副本目录>
|
|||
|
|
2) 同步(会先备份 ⛔ 备份落在 <WS>/交付物/,⛔ 绝不留在包内):
|
|||
|
|
python workspace_mirror.py --sync <工作区副本目录>
|
|||
|
|
3) 只列出该保留什么(自测要断言这个表):
|
|||
|
|
python workspace_mirror.py --list-keep
|
|||
|
|
|
|||
|
|
🔴 三条白名单(**有意保留副本自己的东西,不是残留**)
|
|||
|
|
1. `roots.env` —— 工作区专属键。被覆盖 ⇒ 副本会去读写**另一个仓库**
|
|||
|
|
(`setdefault` 兜底:宿主 env 没设时注入本文件的值)
|
|||
|
|
2. `副本使用说明.md` —— 副本独有的口径文档
|
|||
|
|
3. `install.log` —— 副本自己的安装历史(追加式)
|
|||
|
|
⚠️ 白名单是**按文件名**判的 ⇒ 换副本(别的文件)要改 `KEEP`,
|
|||
|
|
别靠"我记得跳过哪个"—— 那正是会漏的地方。
|
|||
|
|
"""
|
|||
|
|
import argparse
|
|||
|
|
import hashlib
|
|||
|
|
import io
|
|||
|
|
import os
|
|||
|
|
import shutil
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 🔴 副本里"这个包自己的"目录,永远不算漂移
|
|||
|
|
SELF_DIRS = {"__pycache__", ".git", ".pytest_cache"}
|
|||
|
|
SELF_SUFFIX = (".pyc", ".pyo", ".tmp", ".swp", ".mirtmp")
|
|||
|
|
# 🔴🔴 身份标记:`do_sync` 靠它判断"这像不像技能副本"。⛔ **绝不能**把它当残留删掉 ——
|
|||
|
|
# 删了副本就**再也同步不了**(每次都被判"不像副本"直接退出,rc=2),
|
|||
|
|
# 而用户看到的现象只是"工具忽然不干活了",极难往这儿想。
|
|||
|
|
# (2026-10-04 实测踩到:夹具 src 侧没放 SKILL.md ⇒ sync 把它当残留删了 ⇒ 后续全卡死。)
|
|||
|
|
# ⚠️ 它**故意不在** KEEP 里:KEEP 是"工作区专属、不可被全局覆盖"的内容;
|
|||
|
|
# 而 SKILL.md 应当**跟着全局走**(它是版本的一部分)⇒ 只需在残留判定里豁免。
|
|||
|
|
IDENT_MARK = "SKILL.md"
|
|||
|
|
NEVER_DELETE = (IDENT_MARK,) # ⛔ 算残留时跳过 ⇒ 删了副本就废了
|
|||
|
|
KEEP = ("roots.env", "副本使用说明.md", "install.log")
|
|||
|
|
# 备份目录名前缀(`_find_in_prior_backups` 靠它认出"这是历史备份" ⇒ ⛔ 改名要同步改那里)
|
|||
|
|
BK_PREFIX = "session-mechanism-副本备份-"
|
|||
|
|
|
|||
|
|
GLOBAL_PKG = Path(__file__).resolve().parent.parent # 本包根
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _files(root: Path, keep: bool = True):
|
|||
|
|
"""列相对路径 → 绝对路径。⛔ **不用 glob**(P0-49:`**` 不穿透点目录,会零命中)。"""
|
|||
|
|
out = {}
|
|||
|
|
root = Path(root)
|
|||
|
|
for dp, dns, fns in os.walk(str(root)):
|
|||
|
|
dns[:] = [d for d in dns if d not in SELF_DIRS]
|
|||
|
|
for fn in fns:
|
|||
|
|
if fn.endswith(SELF_SUFFIX):
|
|||
|
|
continue
|
|||
|
|
rel = (Path(dp) / fn).relative_to(root).as_posix()
|
|||
|
|
if keep and rel in KEEP:
|
|||
|
|
continue
|
|||
|
|
out[rel] = Path(dp) / fn
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _md5(p: Path) -> str:
|
|||
|
|
return hashlib.md5(p.read_bytes()).hexdigest()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _keep_state(copy_dir: Path, backup_dir=None):
|
|||
|
|
"""覆盖/删除**前**抓白名单文件的现状,供 ④ 兜底写回。
|
|||
|
|
|
|||
|
|
🔴 2026-10-04 修(变异实测发现的死代码):早先只从 `copy_dir` 现抓 ⇒
|
|||
|
|
**文件一旦已经丢了就抓不到** ⇒ 兜底层永远救不了"白名单已丢"这个
|
|||
|
|
**最需要救**的场景(现抓时它已经不在了)。
|
|||
|
|
⇒ 加 `backup_dir` 兜底源:上一次备份里若有,一并收进来。
|
|||
|
|
"""
|
|||
|
|
st = {}
|
|||
|
|
for k in KEEP:
|
|||
|
|
p = copy_dir / k
|
|||
|
|
if p.exists():
|
|||
|
|
st[k] = p.read_bytes()
|
|||
|
|
elif backup_dir:
|
|||
|
|
b = Path(backup_dir) / k
|
|||
|
|
if b.exists():
|
|||
|
|
st[k] = b.read_bytes()
|
|||
|
|
return st
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _find_in_prior_backups(keep_lost, bk_root: Path):
|
|||
|
|
"""🔴🔴 2026-10-04 修(**上一版修法的病根**):在**历次备份**里找已丢的白名单。
|
|||
|
|
|
|||
|
|
⚠️ **原写法是死代码,判据因此报红**(`selftest` 那条「有备份时能捞回」FAIL 1):
|
|||
|
|
原实现把**本次刚建的备份**当兜底源 ⇒ 但 `copytree` 发生在**丢失之后**
|
|||
|
|
⇒ 那份备份里**必然也没有**这个文件 ⇒ 兜底永远捞不到。
|
|||
|
|
⇒ 正解=翻**先前**留下的备份,按时间**从新到旧**找第一份命中的。
|
|||
|
|
⛔⛔ **绝不拿全局那份顶替** —— 全局 `roots.env` 指向**别的仓库**,
|
|||
|
|
填进去 = 把"副本悄悄指错仓库"变成"副本明确指错仓库",⛔ 更坏。
|
|||
|
|
"""
|
|||
|
|
found = {}
|
|||
|
|
if not bk_root.is_dir():
|
|||
|
|
return found
|
|||
|
|
# 备份目录名带秒级时间戳 ⇒ 字典序即时间序(新→旧)
|
|||
|
|
cands = sorted((d for d in bk_root.iterdir()
|
|||
|
|
if d.is_dir() and d.name.startswith(BK_PREFIX)),
|
|||
|
|
key=lambda d: d.name, reverse=True)
|
|||
|
|
for d in cands:
|
|||
|
|
for k in keep_lost:
|
|||
|
|
if k in found:
|
|||
|
|
continue
|
|||
|
|
f = d / k
|
|||
|
|
if f.is_file():
|
|||
|
|
found[k] = f.read_bytes()
|
|||
|
|
print(" ↺ 白名单兜底源=**历史备份** %s" % d.name)
|
|||
|
|
if len(found) == len(keep_lost):
|
|||
|
|
break
|
|||
|
|
return found
|
|||
|
|
|
|||
|
|
|
|||
|
|
def diff(gpkg: Path, copy_dir: Path):
|
|||
|
|
g, c = _files(gpkg), _files(copy_dir)
|
|||
|
|
# 🔴 `NEVER_DELETE`(身份标记 SKILL.md)豁免:⛔ 删了副本就再也同步不了
|
|||
|
|
only_c = sorted(x for x in (set(c) - set(g)) if x not in NEVER_DELETE) # 真残留
|
|||
|
|
only_g = sorted(set(g) - set(c)) # 待补
|
|||
|
|
changed = sorted(k for k in set(g) & set(c) if _md5(g[k]) != _md5(c[k]))
|
|||
|
|
return g, c, only_c, only_g, changed
|
|||
|
|
|
|||
|
|
|
|||
|
|
def do_check(gpkg: Path, copy_dir: Path) -> int:
|
|||
|
|
g, c, only_c, only_g, changed = diff(gpkg, copy_dir)
|
|||
|
|
print("=== 副本漂移体检(⛔ 只读,⛔ 不写任何文件)===")
|
|||
|
|
print("全局 %s |副本 %s" % (gpkg, copy_dir))
|
|||
|
|
for k in only_c:
|
|||
|
|
print(" ⛔ 残留 %-48s %d B" % (k, c[k].stat().st_size))
|
|||
|
|
for k in only_g:
|
|||
|
|
print(" + 待补 %-48s %d B" % (k, g[k].stat().st_size))
|
|||
|
|
for k in changed:
|
|||
|
|
print(" ✎ 待更新 %-48s 全局 %d B / 副本 %d B" % (k, g[k].stat().st_size, c[k].stat().st_size))
|
|||
|
|
drift = len(only_c) + len(only_g) + len(changed)
|
|||
|
|
print("\n漂移合计:%d(残留 %d |待补 %d |待更新 %d |白名单 %s)"
|
|||
|
|
% (drift, len(only_c), len(only_g), len(changed), ", ".join(KEEP)))
|
|||
|
|
if drift:
|
|||
|
|
print("⇒ 有漂移,跑 `--sync %s`" % copy_dir)
|
|||
|
|
else:
|
|||
|
|
print("✅ 无漂移")
|
|||
|
|
return 1 if drift else 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _ws_guess(copy_dir: Path) -> Path:
|
|||
|
|
"""备份落点:`<工作区>/交付物/`(⛔ 绝不放包内 —— t_pkg_hygiene 会判红)。
|
|||
|
|
|
|||
|
|
🔴 判据:**副本自己就在 `.workbuddy/skills/` 下**才往上找(这才是真副本);
|
|||
|
|
找不到(自建夹具/自建目录)⇒ 落**副本的父目录**。
|
|||
|
|
⚠️ 早先无条件往上找 `.workbuddy` ⇒ 夹具落在 `tmp/` 下时,备份会被
|
|||
|
|
写进**真工作区的 `交付物/`**(自测污染真工作区,今天实测踩到)。
|
|||
|
|
"""
|
|||
|
|
p = copy_dir
|
|||
|
|
for up in range(1, 5):
|
|||
|
|
p = p.parent
|
|||
|
|
if p.name == ".workbuddy" and p.parent.name:
|
|||
|
|
return p.parent # …/<工作区>/.workbuddy/skills/<包> ⇒ 落 <工作区>
|
|||
|
|
return copy_dir.parent
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _latest_backup(copy_dir: Path):
|
|||
|
|
"""副本**最近一次**备份(⛔ 不存在返回 None)。
|
|||
|
|
|
|||
|
|
🔴 为什么要它:判「白名单是丢了」还是「**从来没有过**」,
|
|||
|
|
光看副本现状**分不出来** —— 只能看副本自己**历史备份里有没有**。
|
|||
|
|
"""
|
|||
|
|
d = _ws_guess(copy_dir) / "交付物"
|
|||
|
|
if not d.is_dir():
|
|||
|
|
return None
|
|||
|
|
cands = []
|
|||
|
|
for e in d.iterdir():
|
|||
|
|
if e.is_dir() and e.name.startswith("session-mechanism-副本备份-"):
|
|||
|
|
cands.append(e)
|
|||
|
|
if not cands:
|
|||
|
|
return None
|
|||
|
|
# 名字尾缀 `_2`/`_3` 也要能比:去掉后缀按字典序(时间戳定长)
|
|||
|
|
return sorted(cands, key=lambda p: p.name.replace("_2", "").replace("_3", ""))[-1]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def do_sync(gpkg: Path, copy_dir: Path) -> int:
|
|||
|
|
if not (copy_dir / IDENT_MARK).exists():
|
|||
|
|
print("⛔ %s 不像技能副本(没有 SKILL.md)⇒ 不动" % copy_dir)
|
|||
|
|
return 2
|
|||
|
|
# 🔴 2026-10-04 修:白名单**缺失**也算漂移。
|
|||
|
|
# `diff()` 刻意排除 KEEP(免得每次都报它们)⇒ 副本的 `roots.env` 没了
|
|||
|
|
# ⇒ 差异表里**一个字段都不动** ⇒ 早先直接走"无漂移,幂等返回",
|
|||
|
|
# 白名单兜底层**永远没机会跑**。而这恰恰是最该救的场景:
|
|||
|
|
# `roots.env` 丢了 ⇒ 副本按位置推导 ⇒ 去读写**别的仓库**(静默指错)。
|
|||
|
|
# ⇒ 这里单独查一遍。
|
|||
|
|
# 🔴🔴 修第二版:「**全局侧有**」⛔ 不等于「副本**该**有」。
|
|||
|
|
# 实测踩到:全新副本本来就没有 `install.log`(从未安装过)⇒ 却被判成
|
|||
|
|
# "白名单已丢" ⇒ 每次 sync 都刷一条假告警,**假告警多了就没人看了**。
|
|||
|
|
# ⇒ 判据收紧成「**副本自己的历史备份里有过**」=「曾经有过,现在没了」=真丢。
|
|||
|
|
# 副作用(可接受):真丢且**从未备份过**时只告警不阻断,并提示人工恢复。
|
|||
|
|
_hist = _latest_backup(copy_dir)
|
|||
|
|
keep_lost = []
|
|||
|
|
for k in KEEP:
|
|||
|
|
if (copy_dir / k).exists():
|
|||
|
|
continue
|
|||
|
|
if _hist is not None and (_hist / k).exists():
|
|||
|
|
keep_lost.append(k) # 副本自己有过 ⇒ 确定是丢
|
|||
|
|
keep_state = _keep_state(copy_dir)
|
|||
|
|
_, _, only_c, only_g, changed = diff(gpkg, copy_dir)
|
|||
|
|
if keep_lost:
|
|||
|
|
print("⛔ **白名单缺失**(副本历史里有过,现在没了):%s" % "、".join(keep_lost)
|
|||
|
|
+ ("(⛔ `roots.env` 丢 ⇒ 副本会按位置推导去读写**别的仓库**)"
|
|||
|
|
if "roots.env" in keep_lost else ""))
|
|||
|
|
if not (only_c or only_g or changed or keep_lost):
|
|||
|
|
print("✅ 无漂移,⛔ 什么也不做(幂等)")
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
# 🔴 破坏前预检:⛔ 先确认每个待写源文件都真能读出来。
|
|||
|
|
# 理由:②③ 是破坏性动作,读到一半炸掉 ⇒ 副本停在**半同步态**,
|
|||
|
|
# 那是比"不同步"更难查的状态(2026-10-04 实测踩到一次 KeyError 崩在半路)。
|
|||
|
|
gpre = _files(gpkg)
|
|||
|
|
for k in sorted(gpre):
|
|||
|
|
try:
|
|||
|
|
gpre[k].read_bytes()
|
|||
|
|
except Exception as e:
|
|||
|
|
print("⛔ 源文件读不出 ⇒ 拒绝动手:%s(%s)" % (k, e))
|
|||
|
|
return 2
|
|||
|
|
|
|||
|
|
# ① 备份(⛔ 落在 <WS>/交付物/,⛔ 绝不放包内 —— t_pkg_hygiene 会判红)
|
|||
|
|
ws_guess = _ws_guess(copy_dir)
|
|||
|
|
# 🔴 备份目录名**秒级**会撞:同一秒跑第二次 `--sync` ⇒ `copytree` 抛
|
|||
|
|
# `FileExistsError`(2026-10-04 自测撞到)。⇒ 加 `_2`/`_3` 后缀避让。
|
|||
|
|
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|||
|
|
bk = ws_guess / "交付物" / (BK_PREFIX + stamp)
|
|||
|
|
_n = 2
|
|||
|
|
while bk.exists():
|
|||
|
|
bk = ws_guess / "交付物" / ("%s%s_%d" % (BK_PREFIX, stamp, _n))
|
|||
|
|
_n += 1
|
|||
|
|
bk.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
if copy_dir.exists():
|
|||
|
|
shutil.copytree(str(copy_dir), str(bk), ignore=shutil.ignore_patterns(*SELF_DIRS))
|
|||
|
|
print("① 已备份 ⇒ %s" % bk)
|
|||
|
|
|
|||
|
|
# 🔴🔴 2026-10-04 修(**这才是能救回来的那版**):兜底源必须是**别的**备份。
|
|||
|
|
# ⚠️ 原来这段只认 `bk`(**本次刚建的**那份)⇒ `copytree` 发生在**丢失之后**
|
|||
|
|
# ⇒ 那份备份里必然也没有 ⇒ 「有备份时能捞回」这条判据**恒红**
|
|||
|
|
# (实测:全局 selftest `PASS 77 / FAIL 1`,独立复现 rc=0 但文件没回来)。
|
|||
|
|
# ⇒ 正解=扫 `交付物/` 下**历次**备份,按时间从新到旧找第一份命中的。
|
|||
|
|
# ⛔⛔ 但**绝不**拿全局那份来填 —— 全局 `roots.env` 指向**别的仓库**,
|
|||
|
|
# 填进去 = 把「副本悄悄指错仓库」变成「副本明确指错仓库」,⛔ 更坏。
|
|||
|
|
# ⇒ 只能从**副本自己的历史备份**里捞;捞不到就**明说**,⛔ 不静默、⛔ 不代填。
|
|||
|
|
if keep_lost:
|
|||
|
|
_need = [k for k in keep_lost if k not in keep_state]
|
|||
|
|
if _need:
|
|||
|
|
keep_state.update(_find_in_prior_backups(_need, bk.parent))
|
|||
|
|
for k in keep_lost:
|
|||
|
|
if k not in keep_state:
|
|||
|
|
print("⛔⛔ 白名单 **%s 已丢且所有历史备份里都没有** ⇒ 无法自动恢复。" % k)
|
|||
|
|
print(" ⛔ **绝不拿全局那份顶替**(全局指向别的仓库)。")
|
|||
|
|
print(" ⇒ 请人工恢复 %s ——⛔ 不代填。" % (copy_dir / k))
|
|||
|
|
|
|||
|
|
# ② 删残留(副本有、全局已删)
|
|||
|
|
for k in only_c:
|
|||
|
|
_files(copy_dir)[k].unlink()
|
|||
|
|
print("② 删残留 %s" % k)
|
|||
|
|
|
|||
|
|
# ③ 覆盖 + 补入
|
|||
|
|
# 🔴 只遍历**全局有的**。⛔ 残留(只在副本)已在 ② 删掉,⛔ 绝不能进这个循环
|
|||
|
|
# —— 它在 `g` 里不存在,`g[k]` 会 KeyError(2026-10-04 实测踩到:sync 崩在半路)
|
|||
|
|
g = _files(gpkg)
|
|||
|
|
n_ov = n_new = 0
|
|||
|
|
for k in sorted(g):
|
|||
|
|
if k in KEEP:
|
|||
|
|
continue
|
|||
|
|
src = g[k]
|
|||
|
|
dst = copy_dir / k
|
|||
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
data = src.read_bytes()
|
|||
|
|
if dst.exists() and dst.read_bytes() == data:
|
|||
|
|
continue
|
|||
|
|
tmp = Path(str(dst) + ".mirtmp")
|
|||
|
|
io.open(str(tmp), "wb").write(data)
|
|||
|
|
os.replace(str(tmp), str(dst)) # 原子替换
|
|||
|
|
if k in only_g:
|
|||
|
|
n_new += 1
|
|||
|
|
else:
|
|||
|
|
n_ov += 1
|
|||
|
|
print("③ 覆盖 %d |补入 %d" % (n_ov, n_new))
|
|||
|
|
|
|||
|
|
# ④ 白名单兜底:**核对+缺则补回**(⛔ 这是"兜底层",不是主防线)
|
|||
|
|
# 主防线是 ③ 循环里那句 `if k in KEEP: continue`(判据锚在那句上)。
|
|||
|
|
# 这层的作用:万一 ③ 的守卫被改坏/将来有人加新白名单忘了守卫,
|
|||
|
|
# 这里仍能把副本自己的工作区指向**捞回来** ⇒ 失败模式从"静默指错仓库"
|
|||
|
|
# 降级成"这次跑完就修好"。
|
|||
|
|
# ⚠️ 故意的设计:**恒真无害**(内容没被动就零动作),⛔ 不做"逐字重写",
|
|||
|
|
# 否则会给 install.log / roots.env 换来无谓的 mtime 抖动。
|
|||
|
|
for k, data in keep_state.items():
|
|||
|
|
p = copy_dir / k
|
|||
|
|
if p.exists() and p.read_bytes() == data:
|
|||
|
|
continue
|
|||
|
|
tmp = Path(str(p) + ".mirtmp")
|
|||
|
|
tmp.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
io.open(str(tmp), "wb").write(data)
|
|||
|
|
os.replace(str(tmp), str(p)) # 缺文件时 p.parent 可能还没建
|
|||
|
|
print("④ 白名单兜底恢复 %s" % k)
|
|||
|
|
|
|||
|
|
# ⑤ 复核(⛔ 复核工具自身出错 ⇒ 结论作废,必须让它抛而不是吞)
|
|||
|
|
g2, c2, only_c2, only_g2, changed2 = diff(gpkg, copy_dir)
|
|||
|
|
bad = [k for k in g2 if k not in c2 or _md5(g2[k]) != _md5(c2[k])]
|
|||
|
|
print("⑤ 复核:应共享 %d,逐字不一致 = %d %s" % (len(g2), len(bad), bad if bad else "✅"))
|
|||
|
|
print(" 副本额外 = %s" % (sorted(set(c2) - set(g2)) or "(无)✅"))
|
|||
|
|
if bad or only_c2:
|
|||
|
|
return 1
|
|||
|
|
print("\n✅ 同步完成。⛔ 还没完 —— **必须**接着跑:\n"
|
|||
|
|
" 1. 副本 selftest(须 FAIL 0):python <副本>/scripts/selftest.py\n"
|
|||
|
|
" 2. env_check(看 deploy 段):python <副本>/scripts/hooks/_env.py <工作区>")
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
ap = argparse.ArgumentParser(description="工作区副本同步/漂移自检")
|
|||
|
|
ap.add_argument("--check", metavar="副本目录")
|
|||
|
|
ap.add_argument("--sync", metavar="副本目录")
|
|||
|
|
ap.add_argument("--global", dest="gpkg", default=str(GLOBAL_PKG), help="全局包根(默认本包)")
|
|||
|
|
ap.add_argument("--list-keep", action="store_true")
|
|||
|
|
a = ap.parse_args()
|
|||
|
|
if a.list_keep:
|
|||
|
|
for k in KEEP:
|
|||
|
|
print(k)
|
|||
|
|
return 0
|
|||
|
|
gpkg = Path(a.gpkg).resolve()
|
|||
|
|
if a.check:
|
|||
|
|
return do_check(gpkg, Path(a.check).resolve())
|
|||
|
|
if a.sync:
|
|||
|
|
return do_sync(gpkg, Path(a.sync).resolve())
|
|||
|
|
ap.print_help()
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(main())
|