foolproof-operation 1.1.1__py3-none-any.whl
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.
- foolproof_operation/__init__.py +18 -0
- foolproof_operation/__main__.py +31 -0
- foolproof_operation/cli.py +488 -0
- foolproof_operation/core/__init__.py +56 -0
- foolproof_operation/core/audit.py +179 -0
- foolproof_operation/core/context.py +548 -0
- foolproof_operation/core/events.py +119 -0
- foolproof_operation/core/loader.py +256 -0
- foolproof_operation/core/runtime.py +129 -0
- foolproof_operation/core/tasklog.py +157 -0
- foolproof_operation/flows.py +213 -0
- foolproof_operation/foolproof.composition.json +58 -0
- foolproof_operation/gui.py +1252 -0
- foolproof_operation/index/__init__.py +62 -0
- foolproof_operation/index/paths.py +264 -0
- foolproof_operation/index/probe.py +324 -0
- foolproof_operation/index/service.py +373 -0
- foolproof_operation/index/store.py +516 -0
- foolproof_operation/index/usn.py +685 -0
- foolproof_operation/panels.py +197 -0
- foolproof_operation/plugins/__init__.py +12 -0
- foolproof_operation/plugins/categories.py +252 -0
- foolproof_operation/plugins/installer.py +166 -0
- foolproof_operation/plugins/pip.py +276 -0
- foolproof_operation/plugins/reqtxt.py +129 -0
- foolproof_operation/plugins/scanner.py +368 -0
- foolproof_operation/plugins/search.py +497 -0
- foolproof_operation/requirements_categories.json +816 -0
- foolproof_operation/tools/everything/LICENSE +21 -0
- foolproof_operation/tools/everything/README.md +39 -0
- foolproof_operation/tools/everything/es.exe +0 -0
- foolproof_operation-1.1.1.dist-info/METADATA +113 -0
- foolproof_operation-1.1.1.dist-info/RECORD +37 -0
- foolproof_operation-1.1.1.dist-info/WHEEL +5 -0
- foolproof_operation-1.1.1.dist-info/entry_points.txt +2 -0
- foolproof_operation-1.1.1.dist-info/licenses/LICENSE +21 -0
- foolproof_operation-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""foolproof-operation:Python 依赖库安装器(tkinter GUI + CLI)+ 环境发现(NTFS USN/MFT 索引)。
|
|
2
|
+
|
|
3
|
+
包内结构:
|
|
4
|
+
|
|
5
|
+
- ``core``:Cordis 式可逆插件内核(Context / effect 逆序回收 / fork / 事务化清单装配)
|
|
6
|
+
+ ``runtime``(装配入口)、``tasklog``(JSONL 操作流水)、``audit``(启动审计);
|
|
7
|
+
- ``index``:只读 USN/MFT 索引(ctypes 直调;**不创建 USN journal**);
|
|
8
|
+
- ``plugins``:功能插件行(categories / pip / reqtxt / installer / search / scanner);
|
|
9
|
+
- ``cli``:命令行接口;``gui``:tkinter 界面。
|
|
10
|
+
|
|
11
|
+
随包分发的资源:``requirements_categories.json``(分类清单)、
|
|
12
|
+
``foolproof.composition.json``(插件清单)、``tools/everything/``(内置 es.exe,MIT © voidtools)。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
__version__ = "1.1.1"
|
|
18
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""``python -m foolproof_operation`` 的统一入口。
|
|
2
|
+
|
|
3
|
+
- 无参数 / 未知参数 → 打开图形界面(与历史行为一致);
|
|
4
|
+
- ``cli <子命令> …`` → 命令行模式(等价于 ``foolproof-operation cli …``);
|
|
5
|
+
- ``--python <exe>`` → 指定 GUI 操作的目标解释器(由 ``gui`` 模块读取 ``sys.argv`` 推导)。
|
|
6
|
+
|
|
7
|
+
仓库根的 ``requirements.py`` 是同一入口的兼容 shim(老命令继续可用)。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
__all__ = ["main"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
19
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
20
|
+
if args and args[0] == "cli":
|
|
21
|
+
from .cli import run_cli
|
|
22
|
+
|
|
23
|
+
return run_cli(args[1:])
|
|
24
|
+
from .gui import InstallerApp
|
|
25
|
+
|
|
26
|
+
InstallerApp().mainloop()
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""foolproof-operation 命令行接口(命令提示符直接使用)
|
|
3
|
+
|
|
4
|
+
单一入口保持 `python requirements.py`:
|
|
5
|
+
- 无参数 / 未知参数 → 打开原有图形界面(向后兼容)
|
|
6
|
+
- `python requirements.py cli <子命令> [选项]` → 命令行模式
|
|
7
|
+
|
|
8
|
+
子命令:
|
|
9
|
+
list [--category 分类名] 列出分类与库名
|
|
10
|
+
install <库名...|--category 名|--file req.txt|--all> [--python exe]
|
|
11
|
+
uninstall <库名...|--category 名> [--python exe]
|
|
12
|
+
check [库名...] [--category 名] [--python exe] 版本与更新检查
|
|
13
|
+
scan [--backend walk|index|auto] 扫描系统中的 Python 环境(默认 walk=原口径)
|
|
14
|
+
envs [--backend auto|index|es|walk|legacy] [--build] [--deep] [--json f]
|
|
15
|
+
环境发现(插件化:USN 索引驱动,覆盖深层 venv)
|
|
16
|
+
es [ES 参数...] Everything 搜索(内置 es.exe,MIT © voidtools)
|
|
17
|
+
|
|
18
|
+
说明:
|
|
19
|
+
- install/uninstall/check 的 pip 操作经 [--python 指定的解释器或本工具解释器] -m pip 执行;
|
|
20
|
+
- `es` 依赖 Everything 主程序或服务在运行(es.exe 会连接本机 Everything 实例,
|
|
21
|
+
未运行时给出明确提示);完整语法见 tools/everything/README.md;
|
|
22
|
+
- `scan` 是历史口径(glob + 目录名启发式,会漏深层 venv),`envs` 走
|
|
23
|
+
foolproof.composition.json 装配的插件(index → es → walk 依次回退,并打印是谁答的)。
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import shlex
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from .plugins.categories import (
|
|
36
|
+
all_packages as _all_packages_of,
|
|
37
|
+
find_category as _find_category_of,
|
|
38
|
+
is_local_spec as _is_local_spec,
|
|
39
|
+
pip_query_name as _pip_query_name,
|
|
40
|
+
resolve_packages as _resolve_packages_of,
|
|
41
|
+
split_spec as _split_spec,
|
|
42
|
+
target_of as _target_of,
|
|
43
|
+
load_categories,
|
|
44
|
+
)
|
|
45
|
+
from .plugins.pip import PipService
|
|
46
|
+
|
|
47
|
+
HERE = Path(__file__).resolve().parent
|
|
48
|
+
# 分类清单经插件层加载(含结构校验 fail-loud);CATEGORIES 保留为模块级名字供既有调用方使用
|
|
49
|
+
CATEGORIES: dict = load_categories(str(HERE / "requirements_categories.json"))
|
|
50
|
+
ES_EXE = HERE / "tools" / "everything" / "es.exe"
|
|
51
|
+
|
|
52
|
+
#: pip 服务实例(懒建,供 CLI 复用;GUI 与插件行各自持有自己的实例)
|
|
53
|
+
_PIP_SERVICE = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _pip_service() -> PipService:
|
|
57
|
+
global _PIP_SERVICE
|
|
58
|
+
if _PIP_SERVICE is None:
|
|
59
|
+
_PIP_SERVICE = PipService(None, {})
|
|
60
|
+
return _PIP_SERVICE
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---- 本地 / 自研包支持 ----
|
|
64
|
+
# requirements_categories.json 的 value 通常是 PyPI 包名(可带 extras),
|
|
65
|
+
# 但「🏠 本地 / 自研包」里自研包的 value 是本地路径(pip install <path>)。
|
|
66
|
+
# 安装用 value;而 pip show / pip uninstall 要的是包名 —— 路径型 value 回退到显示名。
|
|
67
|
+
# 实现已迁移到 plugins/categories.py(此处保留同名包装,语义逐字一致)。
|
|
68
|
+
_LOCAL_SPEC = re.compile(r"^(?:[A-Za-z]:[\\/]|[\\/]|\.\.?[\\/]|-)")
|
|
69
|
+
_VALUE_INDEX: dict = {k: v for libs in CATEGORIES.values() for k, v in libs.items()}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_local_spec(pkg: str) -> bool:
|
|
73
|
+
"""value 是否为本地路径 / pip 选项形式(而非 PyPI 包名)。"""
|
|
74
|
+
return _is_local_spec(pkg)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def target_of(name: str) -> str:
|
|
78
|
+
"""库名 → pip 安装目标(value);非清单项原样返回。"""
|
|
79
|
+
return _VALUE_INDEX.get(name, name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def pip_query_name(name: str, pkg: str) -> str:
|
|
83
|
+
"""pip show / pip uninstall 需要的包名:去掉 extras;本地路径回落显示名。"""
|
|
84
|
+
return _pip_query_name(name, pkg)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def split_spec(pkg: str) -> list[str]:
|
|
88
|
+
"""把分类 value 拆成 pip 参数列表,支持 `-e <路径>` 这类选项。"""
|
|
89
|
+
return _split_spec(pkg)
|
|
90
|
+
|
|
91
|
+
# 全盘扫描时跳过的大目录(避免误入系统/备份区)
|
|
92
|
+
_SCAN_SKIP = {"Windows", "ProgramData", "$Recycle.Bin", "System Volume Information",
|
|
93
|
+
"node_modules", ".git", "__pycache__"}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _all_packages() -> list[str]:
|
|
97
|
+
return _all_packages_of(CATEGORIES)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _find_category(name: str) -> str:
|
|
101
|
+
"""按精确键或唯一子串匹配分类(分类名带 emoji 前缀,允许用户省略)。"""
|
|
102
|
+
return _find_category_of(CATEGORIES, name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_packages(sel, category, file_path, all_) -> list[str]:
|
|
106
|
+
"""按选择参数汇总目标库名。"""
|
|
107
|
+
return _resolve_packages_of(CATEGORIES, sel, category, file_path, all_)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _pip(args: list[str], python_exe: str | None) -> int:
|
|
111
|
+
"""pip 调用(继承 stdio:pip 进度实时可见,语义与迁移前一致)。"""
|
|
112
|
+
return _pip_service().call_live(args, python_exe=python_exe)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#: 任务日志(懒建;写文件、不打印,故不影响任何命令的输出)
|
|
116
|
+
_TASKLOG = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _tasklog():
|
|
120
|
+
global _TASKLOG
|
|
121
|
+
if _TASKLOG is None:
|
|
122
|
+
from .core.tasklog import TaskLog
|
|
123
|
+
|
|
124
|
+
_TASKLOG = TaskLog()
|
|
125
|
+
return _TASKLOG
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_tasks(args) -> int:
|
|
129
|
+
"""查看任务日志(JSONL 操作流水):事件计数、失败计数与最近记录。"""
|
|
130
|
+
from .core.tasklog import TaskLog
|
|
131
|
+
|
|
132
|
+
log = TaskLog(getattr(args, "path", None) or None)
|
|
133
|
+
if args.clear:
|
|
134
|
+
log.clear()
|
|
135
|
+
print(f"已清空任务日志:{log.path}")
|
|
136
|
+
return 0
|
|
137
|
+
events = log.events(limit=args.limit)
|
|
138
|
+
summary = log.summary(limit=args.limit)
|
|
139
|
+
if args.json:
|
|
140
|
+
print(json.dumps(
|
|
141
|
+
{"path": log.path, "summary": summary, "events": events},
|
|
142
|
+
ensure_ascii=False, indent=2))
|
|
143
|
+
return 0
|
|
144
|
+
print(f"任务日志:{log.path}")
|
|
145
|
+
if not events:
|
|
146
|
+
print("(暂无记录)")
|
|
147
|
+
return 0
|
|
148
|
+
print(f"共 {summary['total']} 条(首 {summary['first']} / 末 {summary['last']}),"
|
|
149
|
+
f"失败计数 {summary['failures']}")
|
|
150
|
+
for name, count in sorted(summary["events"].items()):
|
|
151
|
+
print(f" {name}: {count}")
|
|
152
|
+
print("最近记录:")
|
|
153
|
+
for row in (events[-args.limit:] if args.limit else events[-10:]):
|
|
154
|
+
extra = {k: v for k, v in row.items() if k not in ("ts", "event")}
|
|
155
|
+
print(f" {row.get('ts', '')} {row.get('event', '')} {extra}")
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cmd_doctor(args) -> int:
|
|
160
|
+
"""启动审计:清单 / 分类 / 索引 / 检索后端 / 任务日志一次核完(只读)。"""
|
|
161
|
+
from .core.audit import format_report
|
|
162
|
+
from .core.runtime import build_runtime
|
|
163
|
+
|
|
164
|
+
with build_runtime({"fileindex": {"volumes": getattr(args, "volumes", None) or None}}) as rt:
|
|
165
|
+
audit = rt.ctx.get("audit")
|
|
166
|
+
if audit is None:
|
|
167
|
+
print("审计服务未装配(audit 行缺失)。", file=sys.stderr)
|
|
168
|
+
return 1
|
|
169
|
+
items = audit.run()
|
|
170
|
+
if args.json:
|
|
171
|
+
print(json.dumps([item.as_dict() for item in items], ensure_ascii=False, indent=2))
|
|
172
|
+
else:
|
|
173
|
+
print(format_report(items))
|
|
174
|
+
return 1 if any(item.level == "error" for item in items) else 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def cmd_list(args) -> int:
|
|
178
|
+
cats = {_find_category(args.category): CATEGORIES[_find_category(args.category)]} \
|
|
179
|
+
if args.category else CATEGORIES
|
|
180
|
+
for name, pkgs in cats.items():
|
|
181
|
+
print(f"[{name}]({len(pkgs)})")
|
|
182
|
+
for p in pkgs:
|
|
183
|
+
print(" -", p)
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def cmd_install(args) -> int:
|
|
188
|
+
pkgs = _resolve_packages(args.packages, args.category, args.file, args.all)
|
|
189
|
+
# 值可能是本地路径或带选项(-e ...),拆成 pip 参数
|
|
190
|
+
targets = [a for p in pkgs for a in split_spec(target_of(p))]
|
|
191
|
+
print(f"将安装 {len(pkgs)} 个库:{', '.join(targets[:8])}{'…' if len(targets) > 8 else ''}")
|
|
192
|
+
cmd = ["install", "-U"] if args.upgrade else ["install"]
|
|
193
|
+
log = _tasklog()
|
|
194
|
+
log.record("install.start", count=len(pkgs), upgrade=bool(args.upgrade),
|
|
195
|
+
python=args.python or sys.executable)
|
|
196
|
+
rc = _pip(cmd + targets, args.python)
|
|
197
|
+
log.record("install.done", count=len(pkgs), rc=rc, ok=(rc == 0))
|
|
198
|
+
return rc
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def cmd_uninstall(args) -> int:
|
|
202
|
+
pkgs = _resolve_packages(args.packages, args.category, None, False)
|
|
203
|
+
names = [pip_query_name(p, target_of(p)) for p in pkgs]
|
|
204
|
+
print(f"将卸载 {len(names)} 个库:{', '.join(names)}")
|
|
205
|
+
log = _tasklog()
|
|
206
|
+
log.record("uninstall.start", count=len(names), python=args.python or sys.executable)
|
|
207
|
+
rc = _pip(["uninstall", "-y", *names], args.python)
|
|
208
|
+
log.record("uninstall.done", count=len(names), rc=rc, ok=(rc == 0))
|
|
209
|
+
return rc
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def cmd_check(args) -> int:
|
|
213
|
+
"""版本与更新检查(编排经 plugins/installer,打印格式与迁移前逐字一致)。"""
|
|
214
|
+
from .core.context import Context
|
|
215
|
+
from .plugins.categories import CategoriesService
|
|
216
|
+
from .plugins.installer import InstallerService
|
|
217
|
+
|
|
218
|
+
pkgs = _resolve_packages(args.packages, args.category, None, False) or _all_packages()
|
|
219
|
+
py = args.python or sys.executable
|
|
220
|
+
ctx = Context()
|
|
221
|
+
pip_service = ctx.provide("pip", PipService(None, {"python_exe": py}))
|
|
222
|
+
ctx.provide("categories", CategoriesService(None, {"path": str(HERE / "requirements_categories.json")}))
|
|
223
|
+
installer = InstallerService(ctx, {"python_exe": py})
|
|
224
|
+
log = _tasklog()
|
|
225
|
+
log.record("check.start", count=len(pkgs), python=py)
|
|
226
|
+
results = installer.check(pkgs)
|
|
227
|
+
if pip_service.last_outdated_error is not None:
|
|
228
|
+
print("(pip list --outdated 失败,仅展示已安装版本)")
|
|
229
|
+
for row in results:
|
|
230
|
+
if row.installed is None:
|
|
231
|
+
print(f"{row.name}: 未安装")
|
|
232
|
+
continue
|
|
233
|
+
if row.latest:
|
|
234
|
+
print(f"{row.name}: 已装 {row.installed} → 最新 {row.latest}(可更新)")
|
|
235
|
+
else:
|
|
236
|
+
print(f"{row.name}: 已装 {row.installed}(最新)")
|
|
237
|
+
log.record("check.done", count=len(pkgs),
|
|
238
|
+
outdated=sum(1 for row in results if row.status == "outdated"),
|
|
239
|
+
missing=sum(1 for row in results if row.status == "missing"),
|
|
240
|
+
ok=pip_service.last_outdated_error is None)
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def cmd_scan(args) -> int:
|
|
245
|
+
"""扫描常见 Python 安装位置,列出解释器与包数。
|
|
246
|
+
|
|
247
|
+
``--backend walk``(默认)保持原有 glob 口径不变;``index`` / ``auto`` 则改走
|
|
248
|
+
插件化环境发现(USN 索引优先、按需回退),二者的差异会在结果里显式标注。
|
|
249
|
+
"""
|
|
250
|
+
backend = getattr(args, "backend", "walk") or "walk"
|
|
251
|
+
if backend != "walk":
|
|
252
|
+
return _scan_via_plugins(args, backend)
|
|
253
|
+
import os
|
|
254
|
+
roots = []
|
|
255
|
+
for base in ("C:/", "F:/", "D:/", "E:/"):
|
|
256
|
+
roots.append(Path(base))
|
|
257
|
+
found: dict[str, Path] = {}
|
|
258
|
+
for base in roots:
|
|
259
|
+
if not base.exists():
|
|
260
|
+
continue
|
|
261
|
+
for pat in ("Python*/python.exe", "Python*/*/python.exe",
|
|
262
|
+
"Users/*/AppData/Local/Programs/Python/*/python.exe"):
|
|
263
|
+
for p in base.glob(pat):
|
|
264
|
+
if not p.exists():
|
|
265
|
+
continue
|
|
266
|
+
key = str(p).lower()
|
|
267
|
+
if any(seg.lower() in _SCAN_SKIP for seg in p.parts):
|
|
268
|
+
continue
|
|
269
|
+
found.setdefault(key, p)
|
|
270
|
+
if not found:
|
|
271
|
+
print("未发现 Python 环境(或在非常规路径)。")
|
|
272
|
+
return 0
|
|
273
|
+
print(f"发现 {len(found)} 个 Python 环境:")
|
|
274
|
+
for i, p in enumerate(sorted(found.values()), 1):
|
|
275
|
+
try:
|
|
276
|
+
n = subprocess.check_output(
|
|
277
|
+
[str(p), "-m", "pip", "list", "--format=freeze"],
|
|
278
|
+
text=True, errors="replace", timeout=60).strip().count("\n") + 1
|
|
279
|
+
except Exception:
|
|
280
|
+
n = "?"
|
|
281
|
+
print(f"{i}. {p}(已装包约 {n})")
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _scan_via_plugins(args, backend: str) -> int:
|
|
286
|
+
"""走插件化环境发现(fileindex/search/scanner)列出环境。"""
|
|
287
|
+
from .core.runtime import build_runtime
|
|
288
|
+
|
|
289
|
+
overrides = {
|
|
290
|
+
"fileindex": {"volumes": getattr(args, "volumes", None) or None},
|
|
291
|
+
"search": {"walk_allow_whole_drives": bool(getattr(args, "allow_whole_drives", False))},
|
|
292
|
+
}
|
|
293
|
+
with build_runtime(overrides) as rt:
|
|
294
|
+
scanner = rt.ctx.get("scanner")
|
|
295
|
+
if scanner is None:
|
|
296
|
+
print("环境发现服务未装配(scanner 行缺失);已回退旧口径。", file=sys.stderr)
|
|
297
|
+
args.backend = "walk"
|
|
298
|
+
return cmd_scan(args)
|
|
299
|
+
environments = scanner.discover(backend=backend, roots=getattr(args, "volumes", None) or None)
|
|
300
|
+
result = scanner.last_result
|
|
301
|
+
if result is not None:
|
|
302
|
+
for name, outcome in result.attempts:
|
|
303
|
+
print(f"[后端] {name}: {outcome}")
|
|
304
|
+
print(f"[作答] {result.backend or '(无)'}")
|
|
305
|
+
if not environments:
|
|
306
|
+
print("未发现 Python 环境。")
|
|
307
|
+
return 0
|
|
308
|
+
print(f"发现 {len(environments)} 个 Python 环境(来源:插件化发现 / backend={backend}):")
|
|
309
|
+
for i, env in enumerate(environments, 1):
|
|
310
|
+
kind = env.kind
|
|
311
|
+
version = env.version or "?"
|
|
312
|
+
exe = env.python_exe or "(未找到解释器)"
|
|
313
|
+
print(f"{i}. [{kind}] {env.root} 版本={version}")
|
|
314
|
+
print(f" 解释器: {exe}")
|
|
315
|
+
return 0
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def cmd_envs(args) -> int:
|
|
319
|
+
"""环境发现(插件化):证据优先、可选深验证、可输出 JSON。
|
|
320
|
+
|
|
321
|
+
与 ``scan`` 的区别:``scan`` 是历史口径(glob/目录名启发式),``envs`` 是索引驱动,
|
|
322
|
+
覆盖深层 venv;默认 ``--backend auto``(索引 → Everything → 遍历),
|
|
323
|
+
``--build`` 会在索引缺失时先建索引(只读卷、不改卷状态)。
|
|
324
|
+
"""
|
|
325
|
+
from .core.runtime import build_runtime
|
|
326
|
+
|
|
327
|
+
volumes = args.volumes or None
|
|
328
|
+
deep = bool(args.deep)
|
|
329
|
+
status: list = []
|
|
330
|
+
if args.build:
|
|
331
|
+
with build_runtime({"fileindex": {"volumes": volumes}}) as rt:
|
|
332
|
+
index = rt.ctx.get("fileindex")
|
|
333
|
+
if index is None:
|
|
334
|
+
print("索引服务未装配(fileindex 行缺失)。", file=sys.stderr)
|
|
335
|
+
return 1
|
|
336
|
+
for volume in index.detected_volumes():
|
|
337
|
+
print(f"[建库] {volume} …", flush=True)
|
|
338
|
+
state = index.build(volume, force=bool(args.force))
|
|
339
|
+
status.append(state)
|
|
340
|
+
records = state.get("records", 0)
|
|
341
|
+
seconds = state.get("seconds", 0)
|
|
342
|
+
print(f" -> {state.get('status')} 记录={records} 耗时={seconds}s {state.get('detail', '')}")
|
|
343
|
+
|
|
344
|
+
overrides = {
|
|
345
|
+
"fileindex": {"volumes": volumes},
|
|
346
|
+
"scanner": {"deep": deep},
|
|
347
|
+
"search": {"walk_allow_whole_drives": bool(args.allow_whole_drives)},
|
|
348
|
+
}
|
|
349
|
+
with build_runtime(overrides) as rt:
|
|
350
|
+
scanner = rt.ctx.get("scanner")
|
|
351
|
+
if scanner is None:
|
|
352
|
+
print("环境发现服务未装配(scanner 行缺失)。", file=sys.stderr)
|
|
353
|
+
return 1
|
|
354
|
+
environments = scanner.discover(
|
|
355
|
+
backend=args.backend, roots=volumes, deep=deep
|
|
356
|
+
)
|
|
357
|
+
result = scanner.last_result
|
|
358
|
+
# 注意:SearchResult 有空 __len__,空结果**为假**,故一律用 `is not None` 判存在,
|
|
359
|
+
# 否则会把「索引不可用而回退」误报成「legacy(旧口径)」。
|
|
360
|
+
print(f"[作答] {result.backend or '(无)'}" if result is not None else "[作答] legacy(旧口径)")
|
|
361
|
+
if result is not None:
|
|
362
|
+
for name, outcome in result.attempts:
|
|
363
|
+
print(f"[后端] {name}: {outcome}")
|
|
364
|
+
payload = {
|
|
365
|
+
"count": len(environments),
|
|
366
|
+
"backend": (result.backend if result is not None else "legacy"),
|
|
367
|
+
"environments": [env.as_dict() for env in environments],
|
|
368
|
+
"build": status,
|
|
369
|
+
}
|
|
370
|
+
if args.json:
|
|
371
|
+
Path(args.json).write_text(
|
|
372
|
+
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
373
|
+
)
|
|
374
|
+
print(f"JSON 已写入:{args.json}")
|
|
375
|
+
print(f"共 {len(environments)} 个环境候选:")
|
|
376
|
+
for i, env in enumerate(environments, 1):
|
|
377
|
+
version = env.version or "?"
|
|
378
|
+
valid = "" if env.valid is None else ("✓" if env.valid else "✗")
|
|
379
|
+
print(f"{i:>4}. [{env.kind}{valid}] {env.root} {version}")
|
|
380
|
+
return 0
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def cmd_es(args) -> int:
|
|
384
|
+
if not ES_EXE.exists():
|
|
385
|
+
raise SystemExit(f"未找到内置 es.exe:{ES_EXE}")
|
|
386
|
+
if not args.es_args:
|
|
387
|
+
raise SystemExit("es 需要搜索词或参数。例:python requirements.py cli es -n 20 *.py")
|
|
388
|
+
rc = subprocess.call([str(ES_EXE), *args.es_args])
|
|
389
|
+
if rc == 8:
|
|
390
|
+
print("\n[提示] Everything 未运行:请先启动 Everything.exe(或 Everything 服务),再执行 es。",
|
|
391
|
+
file=sys.stderr)
|
|
392
|
+
return rc
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
396
|
+
ap = argparse.ArgumentParser(prog="python requirements.py cli",
|
|
397
|
+
description="Python 依赖库安装器 · 命令行接口")
|
|
398
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
399
|
+
|
|
400
|
+
p = sub.add_parser("list", help="列出分类与库名")
|
|
401
|
+
p.add_argument("--category")
|
|
402
|
+
p.set_defaults(fn=cmd_list)
|
|
403
|
+
|
|
404
|
+
p = sub.add_parser("install", help="安装库")
|
|
405
|
+
p.add_argument("packages", nargs="*")
|
|
406
|
+
p.add_argument("--category")
|
|
407
|
+
p.add_argument("--file", help="requirements.txt 路径")
|
|
408
|
+
p.add_argument("--all", action="store_true", help="安装全部分类的库")
|
|
409
|
+
p.add_argument("--upgrade", action="store_true", help="等价 pip install -U")
|
|
410
|
+
p.add_argument("--python", help="目标解释器(默认本工具解释器)")
|
|
411
|
+
p.set_defaults(fn=cmd_install)
|
|
412
|
+
|
|
413
|
+
p = sub.add_parser("uninstall", help="卸载库")
|
|
414
|
+
p.add_argument("packages", nargs="*")
|
|
415
|
+
p.add_argument("--category")
|
|
416
|
+
p.add_argument("--python")
|
|
417
|
+
p.set_defaults(fn=cmd_uninstall)
|
|
418
|
+
|
|
419
|
+
p = sub.add_parser("check", help="版本与更新检查")
|
|
420
|
+
p.add_argument("packages", nargs="*")
|
|
421
|
+
p.add_argument("--category")
|
|
422
|
+
p.add_argument("--python")
|
|
423
|
+
p.set_defaults(fn=cmd_check)
|
|
424
|
+
|
|
425
|
+
p = sub.add_parser("scan", help="扫描系统中的 Python 环境(默认保持原 glob 口径)")
|
|
426
|
+
p.add_argument("--backend", choices=["walk", "index", "auto"], default="walk",
|
|
427
|
+
help="walk=原口径(默认);index/auto=插件化发现(USN 索引优先)")
|
|
428
|
+
p.add_argument("--volumes", nargs="*", help="限定卷(如 G: E:)")
|
|
429
|
+
p.add_argument("--allow-whole-drives", action="store_true",
|
|
430
|
+
help="许可整盘遍历(默认拒绝:整卷遍历为分钟~小时级)")
|
|
431
|
+
p.set_defaults(fn=cmd_scan)
|
|
432
|
+
|
|
433
|
+
p = sub.add_parser("envs", help="环境发现(插件化:索引驱动,覆盖深层 venv)")
|
|
434
|
+
p.add_argument("--backend", choices=["auto", "index", "es", "walk", "legacy"], default="auto")
|
|
435
|
+
p.add_argument("--volumes", nargs="*", help="限定卷(如 G: E:);walk 后端下应为具体目录")
|
|
436
|
+
p.add_argument("--build", action="store_true", help="索引缺失时先建索引(只读卷)")
|
|
437
|
+
p.add_argument("--force", action="store_true", help="配合 --build:强制重建")
|
|
438
|
+
p.add_argument("--deep", action="store_true", help="对每个环境真跑 python --version 验证")
|
|
439
|
+
p.add_argument("--allow-whole-drives", action="store_true",
|
|
440
|
+
help="许可整盘遍历(默认拒绝:整卷遍历为分钟~小时级)")
|
|
441
|
+
p.add_argument("--json", help="结果落 JSON 文件")
|
|
442
|
+
p.set_defaults(fn=cmd_envs)
|
|
443
|
+
|
|
444
|
+
p = sub.add_parser("es", help="Everything 搜索(内置 es.exe)")
|
|
445
|
+
p.add_argument("es_args", nargs=argparse.REMAINDER)
|
|
446
|
+
p.set_defaults(fn=cmd_es)
|
|
447
|
+
|
|
448
|
+
p = sub.add_parser("tasks", help="查看任务日志(JSONL 操作流水)")
|
|
449
|
+
p.add_argument("--limit", type=int, default=None, help="只看最后 N 条")
|
|
450
|
+
p.add_argument("--path", default=None, help="日志路径(默认在 LOCALAPPDATA 下的 foolproof-operation/logs)")
|
|
451
|
+
p.add_argument("--json", action="store_true")
|
|
452
|
+
p.add_argument("--clear", action="store_true", help="清空日志后退出")
|
|
453
|
+
p.set_defaults(fn=cmd_tasks)
|
|
454
|
+
|
|
455
|
+
p = sub.add_parser("doctor", help="启动审计:清单/分类/索引/后端/日志(只读)")
|
|
456
|
+
p.add_argument("--volumes", nargs="*", help="限定卷(如 G: E:)")
|
|
457
|
+
p.add_argument("--json", action="store_true")
|
|
458
|
+
p.set_defaults(fn=cmd_doctor)
|
|
459
|
+
return ap
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _relax_output_encoding() -> None:
|
|
463
|
+
"""放宽 stdout/stderr 的错误策略,避免 GBK 控制台下 emoji 直接崩命令。
|
|
464
|
+
|
|
465
|
+
实测(2026-09-23,默认代码页 936):`cli list` 打印分类名里的 emoji 会抛
|
|
466
|
+
``UnicodeEncodeError: 'gbk' codec can't encode character '\\U0001f3ae'``,
|
|
467
|
+
**源码版与 PyInstaller 冻结版都会中招**,且是既有缺陷(与本次重构无关)。
|
|
468
|
+
这里只放宽**错误策略**、不改编码:中文照常显示,emoji 退化为 `?`。
|
|
469
|
+
想完整显示 emoji 可先 `set PYTHONIOENCODING=utf-8`(README 已注明)。
|
|
470
|
+
"""
|
|
471
|
+
for stream in (sys.stdout, sys.stderr):
|
|
472
|
+
try:
|
|
473
|
+
stream.reconfigure(errors="replace")
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def run_cli(argv: list[str]) -> int:
|
|
479
|
+
_relax_output_encoding()
|
|
480
|
+
# es 子命令的参数是 ES 原生旗标(-n/-s/-csv…),直接透传、不经 argparse
|
|
481
|
+
if argv and argv[0] == "es":
|
|
482
|
+
return cmd_es(argparse.Namespace(es_args=argv[1:]))
|
|
483
|
+
args = build_parser().parse_args(argv)
|
|
484
|
+
return args.fn(args)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
if __name__ == "__main__":
|
|
488
|
+
sys.exit(run_cli(sys.argv[1:]))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""foolproof-operation 内核(Cordis 式可逆插件内核,**零第三方依赖**)。
|
|
2
|
+
|
|
3
|
+
对外导出:
|
|
4
|
+
|
|
5
|
+
- ``Context`` / ``Service``:服务注册表 + 可逆副作用栈 + 类式插件基类;
|
|
6
|
+
- ``EventBus`` / ``bus_plugin``:事件总线与其装配插件;
|
|
7
|
+
- ``Loader`` / ``Row`` / ``parse_rows`` / ``import_plugin`` / ``load_composition``:清单装配;
|
|
8
|
+
- 错误类型:``CordisError`` / ``DuplicateServiceError`` / ``MissingDependencyError`` /
|
|
9
|
+
``DuplicateScopeError`` / ``DisposedError`` / ``CompositionError``。
|
|
10
|
+
|
|
11
|
+
设计与实测依据见 ``docs/usn-indexer.md``;规划见
|
|
12
|
+
``G:\\docs\\md\\allin主工作区\\USN索引与Cordis内核规划_2026-09-23.md``。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .context import (
|
|
18
|
+
Context,
|
|
19
|
+
CordisError,
|
|
20
|
+
DisposedError,
|
|
21
|
+
DuplicateScopeError,
|
|
22
|
+
DuplicateServiceError,
|
|
23
|
+
MissingDependencyError,
|
|
24
|
+
Service,
|
|
25
|
+
resolve_plugin,
|
|
26
|
+
)
|
|
27
|
+
from .events import EventBus, bus_plugin
|
|
28
|
+
from .loader import (
|
|
29
|
+
DEFAULT_ROWS,
|
|
30
|
+
CompositionError,
|
|
31
|
+
Loader,
|
|
32
|
+
Row,
|
|
33
|
+
import_plugin,
|
|
34
|
+
load_composition,
|
|
35
|
+
parse_rows,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Context",
|
|
40
|
+
"Service",
|
|
41
|
+
"resolve_plugin",
|
|
42
|
+
"EventBus",
|
|
43
|
+
"bus_plugin",
|
|
44
|
+
"Loader",
|
|
45
|
+
"Row",
|
|
46
|
+
"DEFAULT_ROWS",
|
|
47
|
+
"parse_rows",
|
|
48
|
+
"import_plugin",
|
|
49
|
+
"load_composition",
|
|
50
|
+
"CordisError",
|
|
51
|
+
"DuplicateServiceError",
|
|
52
|
+
"MissingDependencyError",
|
|
53
|
+
"DuplicateScopeError",
|
|
54
|
+
"DisposedError",
|
|
55
|
+
"CompositionError",
|
|
56
|
+
]
|