master-skill 0.12.13 → 0.12.15
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/README_EN.md +2 -0
- package/SKILL.md +6 -4
- package/bin/cli.mjs +54 -0
- package/gemini-extension.json +1 -1
- package/hooks/run-hook.cmd +42 -42
- package/hooks/session-start +25 -3
- package/package.json +1 -1
- package/tools/check_deps.py +95 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "master-skill",
|
|
11
11
|
"description": "FoJin-powered Buddhist AI persona framework — source-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 prebuilt masters across 印度/汉传/藏传/南传 plus compare, debate, and curriculum meta-skills.",
|
|
12
|
-
"version": "0.12.
|
|
12
|
+
"version": "0.12.15",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "xr843",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "master-skill",
|
|
3
3
|
"description": "FoJin-powered Buddhist AI persona framework — source-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 prebuilt masters across 印度/汉传/藏传/南传 plus compare, debate, and curriculum meta-skills.",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.15",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "xr843",
|
|
7
7
|
"email": "xr843@users.noreply.github.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "master-skill",
|
|
3
3
|
"displayName": "Master Skill",
|
|
4
4
|
"description": "FoJin-powered Buddhist AI persona framework — source-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 prebuilt masters across 印度/汉传/藏传/南传.",
|
|
5
|
-
"version": "0.12.
|
|
5
|
+
"version": "0.12.15",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "xr843",
|
|
8
8
|
"email": "xr843@users.noreply.github.com"
|
package/README.md
CHANGED
|
@@ -172,6 +172,8 @@ Master-skill 的核心不是"角色扮演提示词集合",而是一个可验
|
|
|
172
172
|
|
|
173
173
|
**这一列不能推进 v1.0 门禁。** 门禁定义在 Anthropic(`claude-sonnet-4-6`)那一列上,两个模型是两把尺子,不混算。Anthropic 那一列至今只有 2026-08-18 的一次部分运行(84 / 211 条,因账户余额耗尽中断),当时的「零伪造引文」已于 2026-08-31 撤回——那次审计实际一条都没有运行。
|
|
174
174
|
|
|
175
|
+
**这些数字属于 `06b8142` 那棵树。** 此后人设内容与夹具本身都改过(见 CHANGELOG 的 0.12.x 各条:经文摘要的表述、路由指向、宗派名清洗等),没有重测——重测要再跑一次付费全量运行。夹具条数仍是 211,但那不代表内容没变。
|
|
176
|
+
|
|
175
177
|
这些是**关键词与引用字符串的覆盖检查,不是教义正确性,也不是 LLM 判分的答案质量**。详见 [BASELINE-deepseek.md](eval/reports/BASELINE-deepseek.md)(全量运行)、[ADJUDICATION.md](eval/reports/ADJUDICATION.md)(逐条裁定)、[BASELINE.md](eval/reports/BASELINE.md)(Anthropic 部分运行与撤回记录)与[元技能复测报告](eval/reports/0.11.0-e97ded0-deepseek-metaskills.json)。
|
|
176
178
|
|
|
177
179
|
---
|
package/README_EN.md
CHANGED
|
@@ -190,6 +190,8 @@ The 211 fixtures (`prebuilt/*/tests/fidelity.jsonl`) run mechanical checks again
|
|
|
190
190
|
|
|
191
191
|
**This column cannot advance the v1.0 gate.** The gate is defined on the Anthropic (`claude-sonnet-4-6`) column; two models are two instruments and are never pooled. That column still holds only one partial run, from 2026-08-18 (84 / 211, stopped when the account ran out of credit), whose "zero fabricated citations" was retracted on 2026-08-31 — the audit had not actually run on a single case.
|
|
192
192
|
|
|
193
|
+
**These numbers belong to the tree at `06b8142`.** Persona content and the fixtures themselves have changed since (see the 0.12.x entries in the CHANGELOG: how sutta summaries are framed, where routing points, lineage sanitization), and nothing has been re-measured — that needs another paid full run. The fixture count is still 211, which does not mean their content is unchanged.
|
|
194
|
+
|
|
193
195
|
These are **keyword and citation-string coverage checks, not doctrinal correctness and not LLM-judged answer quality**. Details: [BASELINE-deepseek.md](eval/reports/BASELINE-deepseek.md) (the full run), [ADJUDICATION.md](eval/reports/ADJUDICATION.md) (case-by-case rulings), [BASELINE.md](eval/reports/BASELINE.md) (the Anthropic partial run and its retraction), and the [meta-skill re-run report](eval/reports/0.11.0-e97ded0-deepseek-metaskills.json).
|
|
194
196
|
|
|
195
197
|
---
|
package/SKILL.md
CHANGED
|
@@ -5,13 +5,11 @@ argument-hint: <法师名称>
|
|
|
5
5
|
version: 1.0.0
|
|
6
6
|
user-invocable: true
|
|
7
7
|
allowed-tools:
|
|
8
|
-
- Bash
|
|
9
8
|
- Read
|
|
10
|
-
- Write
|
|
11
|
-
- Edit
|
|
12
9
|
- Glob
|
|
13
10
|
- Grep
|
|
14
|
-
-
|
|
11
|
+
- Bash(python3 ${CLAUDE_SKILL_DIR}/tools/*)
|
|
12
|
+
- Bash(python3 "${CLAUDE_SKILL_DIR}/tools/*)
|
|
15
13
|
---
|
|
16
14
|
|
|
17
15
|
# Master-skill — 佛教法师教学角色生成器
|
|
@@ -64,6 +62,10 @@ allowed-tools:
|
|
|
64
62
|
|
|
65
63
|
## 主流程(生成新法师)
|
|
66
64
|
|
|
65
|
+
### Step 0:检查运行环境
|
|
66
|
+
|
|
67
|
+
先运行 `python3 "${CLAUDE_SKILL_DIR}/tools/check_deps.py"`。生成器的每个工具启动时都要导入 `requests`、`pyyaml`、`pypinyin`,缺任何一个都会直接报 `ModuleNotFoundError`,连离线步骤也跑不了。退出码非 0 时停下,把它打印的安装方法原样告诉用户(系统 Python 拒绝 pip 时改用虚拟环境);**不要擅自安装**,那会改动用户的环境。
|
|
68
|
+
|
|
67
69
|
### Step 1:信息录入
|
|
68
70
|
|
|
69
71
|
加载 `${CLAUDE_SKILL_DIR}/prompts/intake.md`,3 问模式收集:①法师名称(FoJin KG 自动匹配) ②关注方面(教义/修行/讲解/全部) ③语言偏好(按传承默认)。
|
package/bin/cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import os from "os";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
7
8
|
|
|
8
9
|
// fileURLToPath (not new URL().pathname) — on Windows the URL pathname is
|
|
9
10
|
// "/C:/…", which fs cannot resolve, so every command saw an empty prebuilt/.
|
|
@@ -151,6 +152,14 @@ function cpR(src, dest) {
|
|
|
151
152
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
152
153
|
const s = path.join(src, entry.name);
|
|
153
154
|
const d = path.join(dest, entry.name);
|
|
155
|
+
// The package ships no links. One in the source is left over from running
|
|
156
|
+
// the clone instructions' `ln -sf` twice, which — with the target already a
|
|
157
|
+
// link to a directory — creates prebuilt/<skill>/<skill> pointing at its own
|
|
158
|
+
// parent. copyFileSync on it threw EISDIR and the install crashed.
|
|
159
|
+
if (entry.isSymbolicLink()) {
|
|
160
|
+
console.error(` (skipped link ${s} — not part of the package)`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
154
163
|
if (entry.isDirectory()) cpR(s, d);
|
|
155
164
|
else fs.copyFileSync(s, d);
|
|
156
165
|
}
|
|
@@ -365,6 +374,35 @@ function resolveMasterDir(input) {
|
|
|
365
374
|
|
|
366
375
|
// Returns the number of failures so main can set a non-zero exit code —
|
|
367
376
|
// scripted callers must be able to tell a typo from a clean install.
|
|
377
|
+
// create-master's tools import requests, pyyaml and pypinyin at startup and exit
|
|
378
|
+
// with ModuleNotFoundError without them. tools/check_deps.py (standard library
|
|
379
|
+
// only) reports which are missing and how to install them.
|
|
380
|
+
function pythonBin() {
|
|
381
|
+
return process.env.PYTHON || (process.platform === "win32" ? "python" : "python3");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function generatorDependencies(generatorDir) {
|
|
385
|
+
const script = path.join(generatorDir, "tools", "check_deps.py");
|
|
386
|
+
if (!fs.existsSync(script)) return null;
|
|
387
|
+
const result = spawnSync(pythonBin(), [script, "--json"], { encoding: "utf8", timeout: 30000 });
|
|
388
|
+
if (result.error) {
|
|
389
|
+
return { ok: false, pythonMissing: true, message: `${pythonBin()} was not found; create-master needs Python 3.9+` };
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const report = JSON.parse(result.stdout);
|
|
393
|
+
if (report.ok) return { ok: true };
|
|
394
|
+
const what = report.python_too_old
|
|
395
|
+
? `Python ${report.python} is older than 3.9`
|
|
396
|
+
: `missing Python package(s) ${report.missing.join(", ")}`;
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
message: `create-master: ${what} — run: ${pythonBin()} "${script}" for install steps`,
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
return { ok: false, message: `create-master: could not check Python dependencies (${(result.stderr || "").trim()})` };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
368
406
|
function cmdInstall(names) {
|
|
369
407
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
370
408
|
let failed = 0;
|
|
@@ -382,8 +420,10 @@ function cmdInstall(names) {
|
|
|
382
420
|
}
|
|
383
421
|
const src = path.join(PACKAGE_ROOT, skill.source);
|
|
384
422
|
const dest = path.join(SKILLS_DIR, skill.install_dir);
|
|
423
|
+
let deps = null;
|
|
385
424
|
if (skill.kind === "generator") {
|
|
386
425
|
replaceGeneratorInstall(skill, src, dest);
|
|
426
|
+
deps = generatorDependencies(dest);
|
|
387
427
|
} else {
|
|
388
428
|
// Clear any previous install first: files renamed or removed upstream
|
|
389
429
|
// must not linger as stale skill content under ~/.claude/skills/.
|
|
@@ -391,6 +431,7 @@ function cmdInstall(names) {
|
|
|
391
431
|
cpR(src, dest);
|
|
392
432
|
}
|
|
393
433
|
console.log(` ✓ ${name} → ${dest}`);
|
|
434
|
+
if (deps && !deps.ok) console.log(` ! ${deps.message}`);
|
|
394
435
|
}
|
|
395
436
|
return failed;
|
|
396
437
|
}
|
|
@@ -503,6 +544,10 @@ function expectedInstallFiles(skill) {
|
|
|
503
544
|
const walk = (rel) => {
|
|
504
545
|
const abs = path.join(src, rel);
|
|
505
546
|
if (!fs.existsSync(abs)) return;
|
|
547
|
+
// Same rule as cpR: install skips links, so they are not expected files.
|
|
548
|
+
// Following a self-referencing one walked the loop to the OS link limit and
|
|
549
|
+
// reported hundreds of phantom missing files.
|
|
550
|
+
if (fs.lstatSync(abs).isSymbolicLink()) return;
|
|
506
551
|
if (fs.statSync(abs).isDirectory()) {
|
|
507
552
|
if (path.basename(abs) === "__pycache__") return;
|
|
508
553
|
for (const entry of fs.readdirSync(abs)) {
|
|
@@ -565,6 +610,15 @@ function installedProblems() {
|
|
|
565
610
|
}
|
|
566
611
|
}
|
|
567
612
|
|
|
613
|
+
const generator = CATALOG.skills.find((skill) => skill.kind === "generator");
|
|
614
|
+
const generatorDir = generator && path.join(SKILLS_DIR, generator.install_dir);
|
|
615
|
+
if (generatorDir && fs.existsSync(path.join(generatorDir, "SKILL.md"))) {
|
|
616
|
+
const deps = generatorDependencies(generatorDir);
|
|
617
|
+
if (deps && !deps.ok) {
|
|
618
|
+
problems.push({ code: "generator-dependencies", name: generator.name, message: deps.message });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
568
622
|
// Personas registered by create-master are links into create-master/masters/;
|
|
569
623
|
// deleting or moving a generated persona by hand leaves its link pointing at
|
|
570
624
|
// nothing. (`uninstall create-master` refuses while personas exist, and with
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "master-skill",
|
|
3
3
|
"description": "FoJin-powered Buddhist AI persona framework — source-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 prebuilt masters across 印度/汉传/藏传/南传.",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.15",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/hooks/run-hook.cmd
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
: ; # Polyglot wrapper — runs as cmd.exe on Windows, bash on Unix.
|
|
2
|
-
: ; #
|
|
3
|
-
: ; #
|
|
4
|
-
: ; #
|
|
5
|
-
: ; #
|
|
6
|
-
: ; #
|
|
7
|
-
: ; #
|
|
8
|
-
: ;
|
|
9
|
-
: ;
|
|
10
|
-
: ;
|
|
11
|
-
: ; exec bash "$HOOK_DIR/$HOOK" "$@"
|
|
12
|
-
: ; exit 1
|
|
13
|
-
@echo off
|
|
14
|
-
setlocal enabledelayedexpansion
|
|
15
|
-
|
|
16
|
-
set "HOOK=%~1"
|
|
17
|
-
set "HOOK_DIR=%~dp0"
|
|
18
|
-
|
|
19
|
-
:: Try common Git for Windows bash locations
|
|
20
|
-
for %%B in (
|
|
21
|
-
"C:\Program Files\Git\bin\bash.exe"
|
|
22
|
-
"C:\Program Files (x86)\Git\bin\bash.exe"
|
|
23
|
-
"%LOCALAPPDATA%\Programs\Git\bin\bash.exe"
|
|
24
|
-
) do (
|
|
25
|
-
if exist %%B (
|
|
26
|
-
%%B "%HOOK_DIR%%HOOK%" %2 %3 %4 %5
|
|
27
|
-
rem !...!, not %...%: cmd.exe expands %ERRORLEVEL% while parsing the
|
|
28
|
-
rem whole parenthesized block, so it would freeze to the value from
|
|
29
|
-
rem before the loop and report every hook failure as success. This is
|
|
30
|
-
rem what `setlocal enabledelayedexpansion` above is for.
|
|
31
|
-
exit /b !ERRORLEVEL!
|
|
32
|
-
)
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
:: Fallback: try bash from PATH
|
|
36
|
-
where bash >nul 2>&1 && (
|
|
37
|
-
bash "%HOOK_DIR%%HOOK%" %2 %3 %4 %5
|
|
38
|
-
exit /b !ERRORLEVEL!
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
:: No bash found — exit silently
|
|
42
|
-
exit /b 0
|
|
1
|
+
: ; # Polyglot wrapper — runs as cmd.exe on Windows, bash on Unix. Dispatches $1
|
|
2
|
+
: ; # by name. This line once read `exec bash "$0" "$@"`, which re-exec'd this
|
|
3
|
+
: ; # same file forever; hooks.json runs the wrapper with "async": false, so every
|
|
4
|
+
: ; # session start spun until the harness hook timeout while 2>/dev/null hid the
|
|
5
|
+
: ; # cause. Keep this half POSIX — the file has no shebang, so /bin/sh interprets
|
|
6
|
+
: ; # it, and that is dash on Debian/Ubuntu where ${@:2} is a "Bad substitution".
|
|
7
|
+
: ; # It is ONE line, ending in a comment, because the file ships with CRLF: cmd.exe
|
|
8
|
+
: ; # mis-parses the LF original (measured — it echoed a comment as a command and
|
|
9
|
+
: ; # emitted no JSON), and under bash a CR would otherwise end up inside a command
|
|
10
|
+
: ; # word. Everything after the `#` — including that CR — is a comment.
|
|
11
|
+
: ; HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"; HOOK="${1:-}"; [ "$#" -gt 0 ] && shift; exec bash "$HOOK_DIR/$HOOK" "$@" #
|
|
12
|
+
: ; exit 1 #
|
|
13
|
+
@echo off
|
|
14
|
+
setlocal enabledelayedexpansion
|
|
15
|
+
|
|
16
|
+
set "HOOK=%~1"
|
|
17
|
+
set "HOOK_DIR=%~dp0"
|
|
18
|
+
|
|
19
|
+
:: Try common Git for Windows bash locations
|
|
20
|
+
for %%B in (
|
|
21
|
+
"C:\Program Files\Git\bin\bash.exe"
|
|
22
|
+
"C:\Program Files (x86)\Git\bin\bash.exe"
|
|
23
|
+
"%LOCALAPPDATA%\Programs\Git\bin\bash.exe"
|
|
24
|
+
) do (
|
|
25
|
+
if exist %%B (
|
|
26
|
+
%%B "%HOOK_DIR%%HOOK%" %2 %3 %4 %5
|
|
27
|
+
rem !...!, not %...%: cmd.exe expands %ERRORLEVEL% while parsing the
|
|
28
|
+
rem whole parenthesized block, so it would freeze to the value from
|
|
29
|
+
rem before the loop and report every hook failure as success. This is
|
|
30
|
+
rem what `setlocal enabledelayedexpansion` above is for.
|
|
31
|
+
exit /b !ERRORLEVEL!
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
:: Fallback: try bash from PATH
|
|
36
|
+
where bash >nul 2>&1 && (
|
|
37
|
+
bash "%HOOK_DIR%%HOOK%" %2 %3 %4 %5
|
|
38
|
+
exit /b !ERRORLEVEL!
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
:: No bash found — exit silently
|
|
42
|
+
exit /b 0
|
package/hooks/session-start
CHANGED
|
@@ -68,13 +68,35 @@ emit_modes_only() {
|
|
|
68
68
|
fi
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
# Find an interpreter that actually runs. On Windows `python3` is a Microsoft
|
|
72
|
+
# Store alias stub: it exists, so `command -v python3` succeeds, and running it
|
|
73
|
+
# exits without executing anything. Measured 2026-09-18 through the cmd.exe
|
|
74
|
+
# wrapper on Windows 11 — every session fell back to "listing modes only", so
|
|
75
|
+
# the fifteen masters never reached the model there. The real interpreter is
|
|
76
|
+
# python.exe or the py launcher; desktop/src/cli.rs already knew this.
|
|
77
|
+
PY_BIN=""
|
|
78
|
+
PY_ARG=""
|
|
79
|
+
for candidate in ${MASTER_SKILL_PYTHON:-} python3 python py; do
|
|
80
|
+
command -v "$candidate" >/dev/null 2>&1 || continue
|
|
81
|
+
if [ "$candidate" = "py" ]; then
|
|
82
|
+
if py -3 -c "import sys" >/dev/null 2>&1; then
|
|
83
|
+
PY_BIN="py"
|
|
84
|
+
PY_ARG="-3"
|
|
85
|
+
break
|
|
86
|
+
fi
|
|
87
|
+
elif "$candidate" -c "import sys" >/dev/null 2>&1; then
|
|
88
|
+
PY_BIN="$candidate"
|
|
89
|
+
break
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
|
|
93
|
+
if [ -z "$PY_BIN" ]; then
|
|
94
|
+
emit_modes_only "no working python3/python/py"
|
|
73
95
|
exit 0
|
|
74
96
|
fi
|
|
75
97
|
|
|
76
98
|
if [ -z "$PLUGIN_ROOT" ] || [ ! -f "$SCRIPT_DIR/session_start.py" ]; then
|
|
77
99
|
emit_modes_only "could not locate session_start.py"
|
|
78
|
-
elif !
|
|
100
|
+
elif ! "$PY_BIN" $PY_ARG "$SCRIPT_DIR/session_start.py" "$PLUGIN_ROOT"; then
|
|
79
101
|
emit_modes_only "session_start.py failed"
|
|
80
102
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "master-skill",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "FoJin-powered Buddhist AI persona framework — source-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 pre-built masters across 印度 / 汉传 / 藏传 / 南传, plus /compare-masters, /master-debate, and /master-curriculum.",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check the Python packages create-master's tools import at startup.
|
|
3
|
+
|
|
4
|
+
Every generator tool imports `fojin_bridge` (which imports `requests`) or
|
|
5
|
+
`skill_writer` (which imports `yaml` and `pypinyin`) at module level. Without those
|
|
6
|
+
packages each one exits with `ModuleNotFoundError` before doing anything — even
|
|
7
|
+
`master_builder.py --offline-smoke`, which never touches the network. Measured
|
|
8
|
+
2026-09-17 in a clean venv: `rag_query.py`, `sutra_collector.py` and
|
|
9
|
+
`master_builder.py` all failed that way.
|
|
10
|
+
|
|
11
|
+
Nothing told an npx user to install them. The only mention was the clone guide's
|
|
12
|
+
`pip install -r requirements.txt`, and on recent Debian, Ubuntu and Homebrew Pythons
|
|
13
|
+
that command is itself refused as an externally-managed environment (PEP 668).
|
|
14
|
+
|
|
15
|
+
Standard library only, so it runs where the tools cannot.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python3 tools/check_deps.py # exit 1 and print how to install if missing
|
|
19
|
+
python3 tools/check_deps.py --json
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import importlib.util
|
|
25
|
+
import json
|
|
26
|
+
import platform
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# import name -> distribution name in requirements.txt
|
|
31
|
+
REQUIRED = {"requests": "requests", "yaml": "pyyaml", "pypinyin": "pypinyin"}
|
|
32
|
+
MIN_PYTHON = (3, 9)
|
|
33
|
+
REQUIREMENTS = Path(__file__).resolve().parent.parent / "requirements.txt"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def missing(find_spec=importlib.util.find_spec) -> list[str]:
|
|
37
|
+
"""Distributions whose import name cannot be found."""
|
|
38
|
+
return [dist for module, dist in REQUIRED.items() if find_spec(module) is None]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def guidance(missing_dists: list[str], requirements: Path = REQUIREMENTS) -> str:
|
|
42
|
+
venv_python = (
|
|
43
|
+
r"%USERPROFILE%\.venvs\master-skill\Scripts\python"
|
|
44
|
+
if sys.platform == "win32"
|
|
45
|
+
else "~/.venvs/master-skill/bin/python"
|
|
46
|
+
)
|
|
47
|
+
return "\n".join(
|
|
48
|
+
[
|
|
49
|
+
"create-master needs Python packages that are not installed: "
|
|
50
|
+
+ ", ".join(missing_dists),
|
|
51
|
+
"",
|
|
52
|
+
f' python3 -m pip install -r "{requirements}"',
|
|
53
|
+
"",
|
|
54
|
+
'If pip refuses with "externally-managed-environment" (PEP 668), use a virtual environment:',
|
|
55
|
+
"",
|
|
56
|
+
" python3 -m venv ~/.venvs/master-skill",
|
|
57
|
+
f' {venv_python} -m pip install -r "{requirements}"',
|
|
58
|
+
"",
|
|
59
|
+
"and start Claude Code with that environment active, so that `python3` is its interpreter.",
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main(argv: list[str] | None = None) -> int:
|
|
65
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
66
|
+
parser.add_argument("--json", action="store_true", help="print a JSON result")
|
|
67
|
+
args = parser.parse_args(argv)
|
|
68
|
+
|
|
69
|
+
too_old = sys.version_info < MIN_PYTHON
|
|
70
|
+
absent = missing()
|
|
71
|
+
ok = not absent and not too_old
|
|
72
|
+
if args.json:
|
|
73
|
+
print(
|
|
74
|
+
json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"ok": ok,
|
|
77
|
+
"python": platform.python_version(),
|
|
78
|
+
"python_too_old": too_old,
|
|
79
|
+
"missing": absent,
|
|
80
|
+
"requirements": str(REQUIREMENTS),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
elif ok:
|
|
85
|
+
print(f"OK: Python {platform.python_version()} with {', '.join(REQUIRED.values())}")
|
|
86
|
+
else:
|
|
87
|
+
if too_old:
|
|
88
|
+
print(f"create-master needs Python {'.'.join(map(str, MIN_PYTHON))}+; this is {platform.python_version()}.")
|
|
89
|
+
if absent:
|
|
90
|
+
print(guidance(absent))
|
|
91
|
+
return 0 if ok else 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
sys.exit(main())
|