@hupan56/wlkj 2.7.11 → 3.0.0
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/bin/cli.js +375 -78
- package/package.json +29 -29
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
- package/templates/.qoder/learning/eval-history.jsonl +28 -0
- package/templates/data/index/wiki-index.json +8 -0
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +159 -161
- package/templates/qoder/commands/optional/wl-report.md +4 -4
- package/templates/qoder/commands/optional/wl-status.md +2 -2
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +2 -2
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +2 -2
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +8 -8
- package/templates/qoder/commands/wl-task.md +17 -17
- package/templates/qoder/commands/wl-test.md +41 -15
- package/templates/qoder/config.yaml +17 -1
- package/templates/qoder/hooks/session-start.py +12 -22
- package/templates/qoder/nul +4 -0
- package/templates/qoder/rules/wl-pipeline.md +22 -48
- package/templates/qoder/scripts/README.md +139 -0
- package/templates/qoder/scripts/common/autotest_auth.py +109 -0
- package/templates/qoder/scripts/common/bootstrap.py +145 -0
- package/templates/qoder/scripts/common/check_publish.py +98 -0
- package/templates/qoder/scripts/common/cmd_registry.py +112 -0
- package/templates/qoder/scripts/common/config.py +187 -0
- package/templates/qoder/scripts/common/contract.py +317 -0
- package/templates/qoder/scripts/common/developer.py +2 -1
- package/templates/qoder/scripts/common/feishu.py +10 -9
- package/templates/qoder/scripts/common/guard.py +159 -0
- package/templates/qoder/scripts/common/identity.py +121 -2
- package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/common/mcp_base.py +268 -0
- package/templates/qoder/scripts/common/paths.py +187 -1
- package/templates/qoder/scripts/common/result.py +223 -0
- package/templates/qoder/scripts/common/roles.py +60 -0
- package/templates/qoder/scripts/common/task_utils.py +21 -9
- package/templates/qoder/scripts/common/test_extract.py +115 -0
- package/templates/qoder/scripts/kg/__init__.py +11 -0
- package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
- package/templates/qoder/scripts/kg/build_relations.py +127 -0
- package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
- package/templates/qoder/scripts/kg/build_workflows.py +144 -0
- package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
- package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
- package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
- package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
- package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
- package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
- package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
- package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
- package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
- package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
- package/templates/qoder/scripts/kg/prefetch.py +359 -0
- package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
- package/templates/qoder/scripts/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
- package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
- package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
- package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
- package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
- package/templates/qoder/scripts/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
- package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
- package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
- package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
- package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
- package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
- package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
- package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
- package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
- package/templates/qoder/scripts/report/req.py +222 -0
- package/templates/qoder/scripts/report/role.py +33 -0
- package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
- package/templates/qoder/scripts/setup/__init__.py +11 -0
- package/templates/qoder/scripts/setup/carriers.py +662 -0
- package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
- package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
- package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
- package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
- package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
- package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
- package/templates/qoder/scripts/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
- package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
- package/templates/qoder/scripts/task/task.py +221 -0
- package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
- package/templates/qoder/scripts/task/task_query.py +161 -0
- package/templates/qoder/scripts/task/task_relations.py +424 -0
- package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
- package/templates/qoder/scripts/test/__init__.py +11 -0
- package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
- package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
- package/templates/qoder/scripts/test/autotest_data.py +675 -0
- package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
- package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
- package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
- package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
- package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
- package/templates/qoder/scripts/wlkj.py +116 -0
- package/templates/qoder/settings.json +1 -10
- package/templates/qoder/skills/design-import/SKILL.md +226 -226
- package/templates/qoder/skills/design-review/SKILL.md +82 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
- package/templates/qoder/skills/prd-review/SKILL.md +5 -5
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
- package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
- package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
- package/templates/qoder/skills/test-generator/SKILL.md +5 -5
- package/templates/qoder/skills/wl-code/SKILL.md +4 -4
- package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
- package/templates/qoder/skills/wl-design/SKILL.md +3 -3
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
- package/templates/qoder/skills/wl-report/SKILL.md +7 -7
- package/templates/qoder/skills/wl-search/SKILL.md +13 -13
- package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
- package/templates/qoder/skills/wl-status/SKILL.md +5 -5
- package/templates/qoder/skills/wl-task/SKILL.md +6 -6
- package/templates/qoder/skills/wl-test/SKILL.md +102 -39
- package/templates/root/AGENTS.md +39 -40
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 子包初始化: 注入 scripts/根 + 所有子包到 sys.path
|
|
2
|
+
# 让跨包 import (旧式不带前缀) 仍可用, 兼容现有代码
|
|
3
|
+
import os as _os, sys as _sys
|
|
4
|
+
_root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
|
|
5
|
+
if _root not in _sys.path:
|
|
6
|
+
_sys.path.insert(0, _root)
|
|
7
|
+
# 注入所有子包目录 (兼容旧式 import xxx)
|
|
8
|
+
for _sub in ('kg', 'build', 'test', 'setup', 'io'):
|
|
9
|
+
_subdir = _os.path.join(_root, _sub)
|
|
10
|
+
if _os.path.isdir(_subdir) and _subdir not in _sys.path:
|
|
11
|
+
_sys.path.insert(0, _subdir)
|
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""carriers.py - 四载体一致性 (Facade 外观模式)
|
|
4
|
+
|
|
5
|
+
v3.0 合并自: check_carriers + check_qoderwork_consistency + sync_carriers。
|
|
6
|
+
四载体: Qoder IDE / Quest / CLI / QoderWork。三者都是"四载体"这一个领域,
|
|
7
|
+
对外应是 1 个 carriers 命令的子操作 (Facade: 多个细粒度检查统一入口)。
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python carriers.py check 全套一致性 (内容+路径)
|
|
11
|
+
python carriers.py check-paths 只查裸相对路径
|
|
12
|
+
python carriers.py sync [--dry-run] 同步 description
|
|
13
|
+
"""
|
|
14
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
15
|
+
import os as _o, sys as _s
|
|
16
|
+
_cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
|
|
17
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
18
|
+
from bootstrap import setup; setup()
|
|
19
|
+
|
|
20
|
+
import os, sys, re, json, argparse, difflib
|
|
21
|
+
try:
|
|
22
|
+
import yaml
|
|
23
|
+
except ImportError:
|
|
24
|
+
yaml=None
|
|
25
|
+
from common.paths import get_repo_root
|
|
26
|
+
BASE = str(get_repo_root())
|
|
27
|
+
COMMANDS_DIR = os.path.join(BASE, '.qoder', 'commands')
|
|
28
|
+
SKILLS_DIR = os.path.join(BASE, '.qoder', 'skills')
|
|
29
|
+
AGENTS_MD = os.path.join(BASE, 'AGENTS.md')
|
|
30
|
+
|
|
31
|
+
# ==================== 来自 check_carriers.py ====================
|
|
32
|
+
from common.paths import get_repo_root
|
|
33
|
+
|
|
34
|
+
BASE = get_repo_root()
|
|
35
|
+
COMMANDS_DIR = BASE / '.qoder' / 'commands'
|
|
36
|
+
SKILLS_DIR = BASE / '.qoder' / 'skills'
|
|
37
|
+
AGENTS_MD = BASE / 'AGENTS.md'
|
|
38
|
+
|
|
39
|
+
# description 相似度阈值 (改了标点/措辞不算漂移, 实质改变才算)
|
|
40
|
+
DESC_SIMILAR_THRESHOLD = 0.6
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_yaml_frontmatter(path):
|
|
44
|
+
"""读 .md 的 YAML frontmatter, 返回 dict (无则空)。"""
|
|
45
|
+
try:
|
|
46
|
+
text = path.read_text(encoding='utf-8', errors='ignore')
|
|
47
|
+
except OSError:
|
|
48
|
+
return {}
|
|
49
|
+
if not text.startswith('---'):
|
|
50
|
+
return {}
|
|
51
|
+
parts = text.split('---', 2)
|
|
52
|
+
if len(parts) < 3:
|
|
53
|
+
return {}
|
|
54
|
+
fm = {}
|
|
55
|
+
for line in parts[1].splitlines():
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if ':' in line:
|
|
58
|
+
k, _, v = line.partition(':')
|
|
59
|
+
fm[k.strip()] = v.strip().strip('"\'')
|
|
60
|
+
return fm
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _similarity(a, b):
|
|
64
|
+
"""两字符串的相似度 (0~1), 用SequenceRatio。None/空 = 0。"""
|
|
65
|
+
if not a or not b:
|
|
66
|
+
return 0.0
|
|
67
|
+
from difflib import SequenceMatcher
|
|
68
|
+
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _prefix_consistent(cmd_desc, skill_desc):
|
|
72
|
+
"""判断 command description 是否是 skill description 的"精简前缀版本"。
|
|
73
|
+
|
|
74
|
+
设计意图: sync_carriers 把 skill 的 description 截断到触发词前, 写进 command。
|
|
75
|
+
所以 command = skill 的核心前缀, skill 后面挂触发词。这不是漂移, 是设计如此。
|
|
76
|
+
|
|
77
|
+
判定: command 是 skill 的前缀 (忽略大小写), 或 command 覆盖了 skill 的核心部分。
|
|
78
|
+
返回 True = 一致 (设计如此), False = 需进一步用相似度判断。
|
|
79
|
+
"""
|
|
80
|
+
if not cmd_desc or not skill_desc:
|
|
81
|
+
return False
|
|
82
|
+
c, s = cmd_desc.lower().strip(), skill_desc.lower().strip()
|
|
83
|
+
# 1. command 是 skill 的前缀 (最常见: sync 后 command = skill 截断版)
|
|
84
|
+
if s.startswith(c):
|
|
85
|
+
return True
|
|
86
|
+
# 2. command 覆盖了 skill 的核心 (skill 去掉触发词后 == command)
|
|
87
|
+
# 触发词标记 (和 sync_carriers._condense_description 对齐)
|
|
88
|
+
for marker in ['。用户说', ';用户说', ' 用户说', '。触发:', ' 触发:', ';触发', '时触发']:
|
|
89
|
+
idx = s.find(marker)
|
|
90
|
+
if idx > 0:
|
|
91
|
+
skill_core = s[:idx + 1] # 含句号/分号
|
|
92
|
+
if skill_core.startswith(c) or c.startswith(skill_core):
|
|
93
|
+
return True
|
|
94
|
+
# 核心部分相似度高也算 (措辞微调)
|
|
95
|
+
if _similarity(c, skill_core) >= 0.85:
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def check_command_skill_pairing():
|
|
101
|
+
"""校验 commands/wl-X.md 和 skills/wl-X/SKILL.md 的 description 是否一致。
|
|
102
|
+
返回 (errors, warnings)。
|
|
103
|
+
"""
|
|
104
|
+
errors = []
|
|
105
|
+
warnings = []
|
|
106
|
+
|
|
107
|
+
# 收集所有 command (含 optional/ 子目录)
|
|
108
|
+
commands = {}
|
|
109
|
+
if COMMANDS_DIR.is_dir():
|
|
110
|
+
for path in COMMANDS_DIR.rglob('*.md'):
|
|
111
|
+
name = path.stem # wl-test
|
|
112
|
+
commands[name] = path
|
|
113
|
+
|
|
114
|
+
# 收集所有 skill
|
|
115
|
+
skills = {}
|
|
116
|
+
if SKILLS_DIR.is_dir():
|
|
117
|
+
for d in SKILLS_DIR.iterdir():
|
|
118
|
+
if d.is_dir() and (d / 'SKILL.md').is_file():
|
|
119
|
+
skills[d.name] = d / 'SKILL.md'
|
|
120
|
+
|
|
121
|
+
# 配对校验 (command 和 skill 同名才校验, 不同名是设计如此)
|
|
122
|
+
common_names = set(commands) & set(skills)
|
|
123
|
+
for name in sorted(common_names):
|
|
124
|
+
cmd_fm = _read_yaml_frontmatter(commands[name])
|
|
125
|
+
skill_fm = _read_yaml_frontmatter(skills[name])
|
|
126
|
+
cmd_desc = cmd_fm.get('description', '')
|
|
127
|
+
skill_desc = skill_fm.get('description', '')
|
|
128
|
+
if not cmd_desc or not skill_desc:
|
|
129
|
+
continue # 没写的跳过
|
|
130
|
+
# 一致性判断: 不只用字符相似度, 还判断"command 是否是 skill 的精简前缀版本"
|
|
131
|
+
# (sync_carriers 产生的就是这个关系: command = skill 去掉触发词后的核心描述)
|
|
132
|
+
# 这样度量才符合设计意图, 不会把"故意精简"误报成漂移。
|
|
133
|
+
if _prefix_consistent(cmd_desc, skill_desc):
|
|
134
|
+
continue # command 是 skill 的精简版 = 一致, 通过
|
|
135
|
+
sim = _similarity(cmd_desc, skill_desc)
|
|
136
|
+
if sim < DESC_SIMILAR_THRESHOLD:
|
|
137
|
+
errors.append((
|
|
138
|
+
name,
|
|
139
|
+
'description 严重不一致 (相似度 %.0f%%, 阈值 %d%%)' % (sim * 100, DESC_SIMILAR_THRESHOLD * 100),
|
|
140
|
+
'command: ' + cmd_desc[:60],
|
|
141
|
+
'skill: ' + skill_desc[:60],
|
|
142
|
+
))
|
|
143
|
+
elif sim < 0.85:
|
|
144
|
+
warnings.append((
|
|
145
|
+
name,
|
|
146
|
+
'description 有差异 (相似度 %.0f%%), 建议核对' % (sim * 100),
|
|
147
|
+
))
|
|
148
|
+
|
|
149
|
+
return errors, warnings
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def check_agents_md_command_table():
|
|
153
|
+
"""校验 AGENTS.md 的 7 核心命令表与实际文件是否对得上。
|
|
154
|
+
表里列了但文件不存在 = 漂移 (改了命令没改文档, 或反之)。
|
|
155
|
+
"""
|
|
156
|
+
errors = []
|
|
157
|
+
if not AGENTS_MD.is_file():
|
|
158
|
+
return [('AGENTS.md', 'AGENTS.md 不存在 (CLI 载体缺失)', '', '')]
|
|
159
|
+
text = AGENTS_MD.read_text(encoding='utf-8', errors='ignore')
|
|
160
|
+
|
|
161
|
+
# 提取"7 Core Commands"表里的命令名 (形如 `/wl-init`)
|
|
162
|
+
# 表格行格式: | `/wl-init` | ... |
|
|
163
|
+
table_cmds = set()
|
|
164
|
+
in_core_table = False
|
|
165
|
+
for line in text.splitlines():
|
|
166
|
+
s = line.strip()
|
|
167
|
+
if '7 Core' in line or '核心命令' in line:
|
|
168
|
+
in_core_table = True
|
|
169
|
+
if in_core_table:
|
|
170
|
+
for m in re.finditer(r'`(/wl-[a-z-]+)`', s):
|
|
171
|
+
table_cmds.add(m.group(1)[1:]) # 去掉 / 前缀
|
|
172
|
+
if in_core_table and s.startswith('|') and '工序站' in text and s.count('|') >= 2:
|
|
173
|
+
# 表还在继续
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# 校验每个表里的命令文件存在
|
|
177
|
+
for cmd in sorted(table_cmds):
|
|
178
|
+
cmd_file = COMMANDS_DIR / (cmd + '.md')
|
|
179
|
+
skill_file = SKILLS_DIR / cmd / 'SKILL.md'
|
|
180
|
+
if not cmd_file.is_file() and not skill_file.is_file():
|
|
181
|
+
errors.append((
|
|
182
|
+
cmd,
|
|
183
|
+
'AGENTS.md 命令表列了 /%s 但 commands/ 和 skills/ 都没文件' % cmd,
|
|
184
|
+
'',
|
|
185
|
+
'',
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
return errors
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main_check_carriers():
|
|
192
|
+
ap = argparse.ArgumentParser(description='四载体一致性守卫 (防漂移)')
|
|
193
|
+
ap.add_argument('--strict', action='store_true', help='警告也算失败')
|
|
194
|
+
args = ap.parse_args()
|
|
195
|
+
|
|
196
|
+
print('=' * 64)
|
|
197
|
+
print('四载体一致性检查 (IDE commands / Quest skills / CLI AGENTS.md / QoderWork)')
|
|
198
|
+
print('=' * 64)
|
|
199
|
+
|
|
200
|
+
pair_errors, pair_warnings = check_command_skill_pairing()
|
|
201
|
+
table_errors = check_agents_md_command_table()
|
|
202
|
+
|
|
203
|
+
all_errors = pair_errors + table_errors
|
|
204
|
+
|
|
205
|
+
if pair_warnings:
|
|
206
|
+
print('\n🟡 描述差异 (建议核对, --strict 才算失败):')
|
|
207
|
+
for name, msg, *_ in pair_warnings:
|
|
208
|
+
print(' %s: %s' % (name, msg))
|
|
209
|
+
|
|
210
|
+
if all_errors:
|
|
211
|
+
print('\n🔴 载体漂移 (必修):')
|
|
212
|
+
for item in all_errors:
|
|
213
|
+
name = item[0]
|
|
214
|
+
print(' %s' % name)
|
|
215
|
+
for detail in item[1:]:
|
|
216
|
+
if detail:
|
|
217
|
+
print(' %s' % detail)
|
|
218
|
+
|
|
219
|
+
if not all_errors and not pair_warnings:
|
|
220
|
+
print('✅ 四载体一致: 同名命令描述对得上, AGENTS.md 命令表齐全。')
|
|
221
|
+
|
|
222
|
+
print('-' * 64)
|
|
223
|
+
print('统计: 漂移 %d / 差异 %d' % (len(all_errors), len(pair_warnings)))
|
|
224
|
+
print('提示: 与 check_qoderwork_consistency.py 互补 ——')
|
|
225
|
+
print(' 那个查裸相对路径, 这个查内容一致性。')
|
|
226
|
+
|
|
227
|
+
if all_errors:
|
|
228
|
+
return 1
|
|
229
|
+
if args.strict and pair_warnings:
|
|
230
|
+
return 1
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ==================== 来自 check_qoderwork_consistency.py ====================
|
|
237
|
+
from common.paths import get_repo_root # noqa: E402
|
|
238
|
+
|
|
239
|
+
# 扫描目标: commands 全部 .md + skills 的 SKILL.md
|
|
240
|
+
SCAN_GLOBS = [
|
|
241
|
+
('commands', '.qoder/commands', '*.md'),
|
|
242
|
+
('skills', '.qoder/skills', 'SKILL.md'),
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
# 裸相对路径调用: `python .qoder/scripts/xxx` 但前面没有 cd "$R" 或 "$R/ 前缀
|
|
246
|
+
# 匹配 `python .qoder/scripts/` 或 `python3 .qoder/scripts/`, 不带 $R 引号包裹
|
|
247
|
+
BARE_PAT = re.compile(r'(?<![\"\$])\bpython3?\s+\.qoder/scripts/([A-Za-z0-9_./-]+)')
|
|
248
|
+
|
|
249
|
+
# 已安全模式: cd "$R" && python .qoder/scripts/...
|
|
250
|
+
SAFE_CD_PAT = re.compile(r'cd\s+["\']?\$R["\']?\s*&&\s*python3?\s+\.qoder/scripts/')
|
|
251
|
+
|
|
252
|
+
# 仓库根定位块存在的标志
|
|
253
|
+
ROOT_LOCATOR_PAT = re.compile(r'repo_root\.py|R=\$\(python.*repo_root')
|
|
254
|
+
|
|
255
|
+
# 在 install_qoderwork.py 提示行里的相对路径(说明性文字, 非真实调用), 豁免
|
|
256
|
+
HINT_CTX = 'install_qoderwork.py'
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def scan_file(path, strict):
|
|
260
|
+
"""扫一个文件, 返回 (errors, warnings, root_block_present)。
|
|
261
|
+
errors = 裸相对路径真实调用(必修); warnings = 无 R 块等(建议修)。
|
|
262
|
+
"""
|
|
263
|
+
errors = []
|
|
264
|
+
warnings = []
|
|
265
|
+
try:
|
|
266
|
+
with open(path, encoding='utf-8') as f:
|
|
267
|
+
lines = f.readlines()
|
|
268
|
+
except (OSError, IOError):
|
|
269
|
+
return errors, warnings, True
|
|
270
|
+
|
|
271
|
+
root_block = bool(ROOT_LOCATOR_PAT.search(''.join(lines)))
|
|
272
|
+
|
|
273
|
+
for i, line in enumerate(lines, 1):
|
|
274
|
+
# 豁免: 说明性文字里提到 install_qoderwork.py 的相对路径(这是"找不到时跑这个"的指引)
|
|
275
|
+
if HINT_CTX in line:
|
|
276
|
+
continue
|
|
277
|
+
# 豁免: 已安全的 cd "$R" && python 模式
|
|
278
|
+
if SAFE_CD_PAT.search(line):
|
|
279
|
+
continue
|
|
280
|
+
# 找裸相对路径
|
|
281
|
+
for m in BARE_PAT.finditer(line):
|
|
282
|
+
script = m.group(1)
|
|
283
|
+
# 过滤掉明显是文字描述(行里既无 bash 代码块上下文也无命令感)
|
|
284
|
+
# 简化: 只要匹配到就算(宁可误报让 reviewer 看一眼)
|
|
285
|
+
errors.append((i, line.rstrip(), script))
|
|
286
|
+
|
|
287
|
+
# 无 R 块但文件里有 $R 引用 → 警告
|
|
288
|
+
if not root_block and '$R' in ''.join(lines):
|
|
289
|
+
warnings.append((0, '(无 R 定位块, 但文件里用了 $R)', ''))
|
|
290
|
+
|
|
291
|
+
return errors, warnings, root_block
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def main_check_paths():
|
|
295
|
+
ap = argparse.ArgumentParser(description='检查命令/skills 是否含裸相对路径(防 ZCode/QoderWork 割裂)')
|
|
296
|
+
ap.add_argument('--strict', action='store_true', help='警告也算失败')
|
|
297
|
+
args = ap.parse_args()
|
|
298
|
+
|
|
299
|
+
repo = get_repo_root()
|
|
300
|
+
total_errors = 0
|
|
301
|
+
total_warnings = 0
|
|
302
|
+
files_with_errors = []
|
|
303
|
+
files_with_warnings = []
|
|
304
|
+
|
|
305
|
+
for label, rel_dir, pattern in SCAN_GLOBS:
|
|
306
|
+
scan_dir = repo / rel_dir
|
|
307
|
+
if not scan_dir.is_dir():
|
|
308
|
+
continue
|
|
309
|
+
for dirpath, dirnames, filenames in os.walk(str(scan_dir)):
|
|
310
|
+
for fn in filenames:
|
|
311
|
+
import fnmatch
|
|
312
|
+
if not fnmatch.fnmatch(fn, pattern):
|
|
313
|
+
continue
|
|
314
|
+
path = os.path.join(dirpath, fn)
|
|
315
|
+
errors, warnings, root_block = scan_file(path, args.strict)
|
|
316
|
+
rel = os.path.relpath(path, str(repo))
|
|
317
|
+
if errors:
|
|
318
|
+
files_with_errors.append((rel, errors))
|
|
319
|
+
total_errors += len(errors)
|
|
320
|
+
if warnings:
|
|
321
|
+
files_with_warnings.append((rel, warnings))
|
|
322
|
+
total_warnings += len(warnings)
|
|
323
|
+
|
|
324
|
+
# 输出
|
|
325
|
+
print('=' * 64)
|
|
326
|
+
print('QoderWork 一致性检查 (防 ZCode 行 / QoderWork 挂 的割裂)')
|
|
327
|
+
print('=' * 64)
|
|
328
|
+
|
|
329
|
+
if files_with_errors:
|
|
330
|
+
print()
|
|
331
|
+
print('🔴 裸相对路径调用 (QoderWork 必挂, 必修):')
|
|
332
|
+
for rel, errs in files_with_errors:
|
|
333
|
+
print(' ' + rel)
|
|
334
|
+
for lineno, line, script in errs:
|
|
335
|
+
snippet = line.strip()[:80]
|
|
336
|
+
print(' L%d: %s' % (lineno, snippet))
|
|
337
|
+
|
|
338
|
+
if files_with_warnings:
|
|
339
|
+
print()
|
|
340
|
+
print('🟡 警告 (建议修, --strict 才算失败):')
|
|
341
|
+
for rel, ws in files_with_warnings:
|
|
342
|
+
print(' ' + rel)
|
|
343
|
+
for lineno, line, script in ws:
|
|
344
|
+
print(' %s' % line)
|
|
345
|
+
|
|
346
|
+
if not files_with_errors and not files_with_warnings:
|
|
347
|
+
print('✅ 全部通过: 命令/skills 无裸相对路径调用, ZCode ≡ QoderWork 一致。')
|
|
348
|
+
|
|
349
|
+
print('-' * 64)
|
|
350
|
+
print('统计: 错误 %d (文件 %d) / 警告 %d (文件 %d)'
|
|
351
|
+
% (total_errors, len(files_with_errors), total_warnings, len(files_with_warnings)))
|
|
352
|
+
print()
|
|
353
|
+
print('规约: 脚本调用必须二选一:')
|
|
354
|
+
print(' ① python "$R/.qoder/scripts/xxx.py" ... (R = repo_root.py 输出)')
|
|
355
|
+
print(' ② cd "$R" && python .qoder/scripts/xxx.py ...')
|
|
356
|
+
|
|
357
|
+
if total_errors > 0:
|
|
358
|
+
return 1
|
|
359
|
+
if args.strict and total_warnings > 0:
|
|
360
|
+
return 1
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ==================== 来自 sync_carriers.py ====================
|
|
367
|
+
from common.paths import get_repo_root
|
|
368
|
+
|
|
369
|
+
BASE = get_repo_root()
|
|
370
|
+
COMMANDS_DIR = BASE / '.qoder' / 'commands'
|
|
371
|
+
SKILLS_DIR = BASE / '.qoder' / 'skills'
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _read_frontmatter(path):
|
|
375
|
+
"""读 .md 的 frontmatter, 返回 (fm_dict, body, raw_fm_text)。
|
|
376
|
+
raw_fm_text 用于精确替换 (保留原始格式)。
|
|
377
|
+
"""
|
|
378
|
+
text = path.read_text(encoding='utf-8', errors='ignore')
|
|
379
|
+
if not text.startswith('---'):
|
|
380
|
+
return {}, text, ''
|
|
381
|
+
parts = text.split('---', 2)
|
|
382
|
+
if len(parts) < 3:
|
|
383
|
+
return {}, text, ''
|
|
384
|
+
fm_raw = parts[1]
|
|
385
|
+
body = parts[2]
|
|
386
|
+
return _parse_fm(fm_raw), body, fm_raw
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _parse_fm(fm_text):
|
|
390
|
+
"""解析 YAML frontmatter (简化版, 不引外部库)。返回 dict。
|
|
391
|
+
处理带引号的值和裸值。
|
|
392
|
+
"""
|
|
393
|
+
fm = {}
|
|
394
|
+
for line in fm_text.splitlines():
|
|
395
|
+
line = line.rstrip()
|
|
396
|
+
if not line.strip() or line.strip().startswith('#'):
|
|
397
|
+
continue
|
|
398
|
+
if ':' not in line:
|
|
399
|
+
continue
|
|
400
|
+
key, _, val = line.partition(':')
|
|
401
|
+
key = key.strip()
|
|
402
|
+
val = val.strip()
|
|
403
|
+
# 去引号
|
|
404
|
+
if (val.startswith('"') and val.endswith('"')) or \
|
|
405
|
+
(val.startswith("'") and val.endswith("'")):
|
|
406
|
+
val = val[1:-1]
|
|
407
|
+
fm[key] = val
|
|
408
|
+
return fm
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _condense_description(desc):
|
|
412
|
+
"""skill 的 description 往往很长 (含触发词列举), command 的该简洁。
|
|
413
|
+
截取策略: 保留到"用户说"或"触发"或第一个句号之前的核心描述。
|
|
414
|
+
保留中英双语部分 (英文描述通常是核心)。
|
|
415
|
+
"""
|
|
416
|
+
if not desc:
|
|
417
|
+
return desc
|
|
418
|
+
# 找截断点: "用户说" / "触发" / "时触发"
|
|
419
|
+
for marker in ['。用户说', ';用户说', ' 用户说', '。触发:', ' 触发:', ';触发', '。时触发', ' 时触发']:
|
|
420
|
+
idx = desc.find(marker)
|
|
421
|
+
if idx > 0:
|
|
422
|
+
return desc[:idx + 1] # 含句号
|
|
423
|
+
# 没找到截断点 → 原样返回 (可能本就简洁)
|
|
424
|
+
return desc
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _find_command_file(name):
|
|
428
|
+
"""在 commands/ 和 commands/optional/ 下找命令文件。返回 Path 或 None。"""
|
|
429
|
+
for cand in [COMMANDS_DIR / (name + '.md'),
|
|
430
|
+
COMMANDS_DIR / 'optional' / (name + '.md')]:
|
|
431
|
+
if cand.is_file():
|
|
432
|
+
return cand
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _find_skill_file(name):
|
|
437
|
+
"""skills/wl-X/SKILL.md。"""
|
|
438
|
+
p = SKILLS_DIR / name / 'SKILL.md'
|
|
439
|
+
return p if p.is_file() else None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def sync_one(name, dry_run=False):
|
|
443
|
+
"""同步一个命令。返回 ('synced'/'skipped'/'error', 详情)。"""
|
|
444
|
+
skill_path = _find_skill_file(name)
|
|
445
|
+
cmd_path = _find_command_file(name)
|
|
446
|
+
if not skill_path:
|
|
447
|
+
return 'error', 'skill 不存在: %s' % skill_path
|
|
448
|
+
if not cmd_path:
|
|
449
|
+
return 'error', 'command 不存在: %s' % cmd_path
|
|
450
|
+
|
|
451
|
+
skill_fm, _, _ = _read_frontmatter(skill_path)
|
|
452
|
+
skill_desc = skill_fm.get('description', '')
|
|
453
|
+
if not skill_desc:
|
|
454
|
+
return 'skipped', 'skill 无 description 字段'
|
|
455
|
+
|
|
456
|
+
cmd_fm, cmd_body, cmd_fm_raw = _read_frontmatter(cmd_path)
|
|
457
|
+
cmd_desc = cmd_fm.get('description', '')
|
|
458
|
+
|
|
459
|
+
# 精简 skill desc (command 不该塞一堆触发词)
|
|
460
|
+
condensed = _condense_description(skill_desc)
|
|
461
|
+
|
|
462
|
+
if cmd_desc == condensed:
|
|
463
|
+
return 'skipped', '已一致'
|
|
464
|
+
|
|
465
|
+
# dry-run 只预览
|
|
466
|
+
if dry_run:
|
|
467
|
+
return 'would-sync', ' 旧: %s\n 新: %s' % (cmd_desc[:70], condensed[:70])
|
|
468
|
+
|
|
469
|
+
# 精确替换 frontmatter 里的 description 行
|
|
470
|
+
new_fm = _replace_desc_in_fm(cmd_fm_raw, condensed)
|
|
471
|
+
if new_fm is None:
|
|
472
|
+
return 'error', '无法解析 command frontmatter 的 description 行'
|
|
473
|
+
|
|
474
|
+
new_text = '---' + new_fm + '---' + cmd_body
|
|
475
|
+
cmd_path.write_text(new_text, encoding='utf-8')
|
|
476
|
+
return 'synced', condensed[:70]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _replace_desc_in_fm(fm_raw, new_desc):
|
|
480
|
+
"""在 frontmatter 原文里替换 description 行。保留其他行原样。
|
|
481
|
+
返回新的 fm_raw 或 None (无法替换)。
|
|
482
|
+
"""
|
|
483
|
+
# 匹配 description: 开头的行 (可能带引号)
|
|
484
|
+
# 用 json.dumps 保证引号转义正确 (description 含特殊字符)
|
|
485
|
+
escaped = _yaml_escape(new_desc)
|
|
486
|
+
pat = re.compile(r'^(\s*description:\s*)(.*)$', re.MULTILINE)
|
|
487
|
+
new_fm, n = pat.subn(lambda m: m.group(1) + escaped, fm_raw, count=1)
|
|
488
|
+
if n == 0:
|
|
489
|
+
# 没有 description 行 → 插一行 (在 name: 之后)
|
|
490
|
+
if re.search(r'^\s*name:', fm_raw, re.MULTILINE):
|
|
491
|
+
new_fm = re.sub(
|
|
492
|
+
r'^(\s*name:\s*.*)$',
|
|
493
|
+
r'\1\ndescription: ' + escaped,
|
|
494
|
+
fm_raw, count=1, flags=re.MULTILINE)
|
|
495
|
+
else:
|
|
496
|
+
return None
|
|
497
|
+
return new_fm
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _yaml_escape(s):
|
|
501
|
+
"""YAML 字符串转义: 含特殊字符(: # 等)用双引号包裹, 内部双引号转义。
|
|
502
|
+
简化: 一律用双引号 + JSON 转义 (最安全)。
|
|
503
|
+
"""
|
|
504
|
+
import json
|
|
505
|
+
return json.dumps(s, ensure_ascii=False)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def find_drifted_commands():
|
|
509
|
+
"""找出所有 command↔skill 配对且 description 漂移的命令名。
|
|
510
|
+
(复用 check_carriers 的相似度判断)
|
|
511
|
+
"""
|
|
512
|
+
from difflib import SequenceMatcher
|
|
513
|
+
drifted = []
|
|
514
|
+
if not SKILLS_DIR.is_dir():
|
|
515
|
+
return drifted
|
|
516
|
+
for d in sorted(SKILLS_DIR.iterdir()):
|
|
517
|
+
if not d.is_dir() or not (d / 'SKILL.md').is_file():
|
|
518
|
+
continue
|
|
519
|
+
name = d.name
|
|
520
|
+
cmd = _find_command_file(name)
|
|
521
|
+
if not cmd:
|
|
522
|
+
continue
|
|
523
|
+
skill_fm, _, _ = _read_frontmatter(d / 'SKILL.md')
|
|
524
|
+
cmd_fm, _, _ = _read_frontmatter(cmd)
|
|
525
|
+
sd, cd = skill_fm.get('description', ''), cmd_fm.get('description', '')
|
|
526
|
+
if not sd or not cd:
|
|
527
|
+
continue
|
|
528
|
+
sim = SequenceMatcher(None, sd.lower(), cd.lower()).ratio()
|
|
529
|
+
if sim < 0.85:
|
|
530
|
+
drifted.append(name)
|
|
531
|
+
return drifted
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def main_sync():
|
|
535
|
+
ap = argparse.ArgumentParser(description='四载体 description 自动同步 (skill -> command)')
|
|
536
|
+
ap.add_argument('command', nargs='?', default=None,
|
|
537
|
+
help='只同步指定命令 (如 wl-search); 不传则同步所有漂移的')
|
|
538
|
+
ap.add_argument('--dry-run', action='store_true', help='只预览不写文件')
|
|
539
|
+
args = ap.parse_args()
|
|
540
|
+
|
|
541
|
+
print('=' * 56)
|
|
542
|
+
print('四载体 description 同步 (skill -> command)')
|
|
543
|
+
print('=' * 56)
|
|
544
|
+
|
|
545
|
+
if args.command:
|
|
546
|
+
names = [args.command]
|
|
547
|
+
else:
|
|
548
|
+
names = find_drifted_commands()
|
|
549
|
+
if not names:
|
|
550
|
+
print('✅ 无漂移, 所有 command/skill description 已一致。')
|
|
551
|
+
return 0
|
|
552
|
+
print('发现 %d 个漂移命令: %s' % (len(names), ', '.join(names)))
|
|
553
|
+
print()
|
|
554
|
+
|
|
555
|
+
synced, skipped, errors = [], [], []
|
|
556
|
+
for name in names:
|
|
557
|
+
status, detail = sync_one(name, dry_run=args.dry_run)
|
|
558
|
+
if status == 'synced':
|
|
559
|
+
synced.append(name)
|
|
560
|
+
print(' [✓] %s' % name)
|
|
561
|
+
print(' %s' % detail)
|
|
562
|
+
elif status == 'would-sync':
|
|
563
|
+
synced.append(name)
|
|
564
|
+
print(' [~] %s (dry-run)' % name)
|
|
565
|
+
print('%s' % detail)
|
|
566
|
+
elif status == 'skipped':
|
|
567
|
+
skipped.append(name)
|
|
568
|
+
else:
|
|
569
|
+
errors.append((name, detail))
|
|
570
|
+
print(' [✗] %s: %s' % (name, detail))
|
|
571
|
+
|
|
572
|
+
print()
|
|
573
|
+
verb = '将同步' if args.dry_run else '已同步'
|
|
574
|
+
print('%s: %d 同步 / %d 跳过 / %d 错误'
|
|
575
|
+
% (verb, len(synced), len(skipped), len(errors)))
|
|
576
|
+
if not args.dry_run and synced:
|
|
577
|
+
print('提示: 重跑 check_carriers.py 验证一致性。')
|
|
578
|
+
if errors:
|
|
579
|
+
return 1
|
|
580
|
+
return 0
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ============================================================
|
|
586
|
+
# Facade: 统一入口分发
|
|
587
|
+
# ============================================================
|
|
588
|
+
def main_all():
|
|
589
|
+
"""M4.2: 全系列同步 (一处改→4载体全生效)。
|
|
590
|
+
|
|
591
|
+
串联现有3个同步动作:
|
|
592
|
+
1. sync description (skill→command)
|
|
593
|
+
2. sync_templates (npm 镜像 .qoder→packages/wlkj/templates)
|
|
594
|
+
3. install_qoderwork (投影 skills/commands→~/.qoderwork/)
|
|
595
|
+
|
|
596
|
+
用法: python carriers.py all
|
|
597
|
+
改了规则/技能后跑一次, 4载体全同步。
|
|
598
|
+
"""
|
|
599
|
+
import subprocess, os
|
|
600
|
+
SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
601
|
+
BASE = os.path.dirname(os.path.dirname(SCRIPTS))
|
|
602
|
+
print('=' * 56)
|
|
603
|
+
print('全系列同步 (4载体: IDE/Quest/CLI/QoderWork)')
|
|
604
|
+
print('=' * 56)
|
|
605
|
+
|
|
606
|
+
# 1. sync description (skill→command)
|
|
607
|
+
print('\n[1/3] 同步 description (skill→command)...')
|
|
608
|
+
sys.argv = [sys.argv[0]] # 清参数
|
|
609
|
+
try:
|
|
610
|
+
main_sync()
|
|
611
|
+
except SystemExit:
|
|
612
|
+
pass
|
|
613
|
+
|
|
614
|
+
# 2. sync_templates (npm 镜像)
|
|
615
|
+
print('\n[2/3] 同步 npm 镜像 (.qoder→packages/wlkj/templates)...')
|
|
616
|
+
sync_script = os.path.join(BASE, 'packages', 'wlkj', 'sync_templates.py')
|
|
617
|
+
if os.path.isfile(sync_script):
|
|
618
|
+
r = subprocess.run([sys.executable, sync_script], capture_output=True, text=True, encoding='utf-8')
|
|
619
|
+
print(r.stdout.strip().split('\n')[-1] if r.stdout else ' (无输出)')
|
|
620
|
+
else:
|
|
621
|
+
print(' 跳过 (sync_templates.py 不存在)')
|
|
622
|
+
|
|
623
|
+
# 3. install_qoderwork (投影 ~/.qoderwork/)
|
|
624
|
+
print('\n[3/3] 投影 QoderWork (skills/commands→~/.qoderwork/)...')
|
|
625
|
+
install_script = os.path.join(SCRIPTS, 'setup', 'install_qoderwork.py')
|
|
626
|
+
if os.path.isfile(install_script):
|
|
627
|
+
r = subprocess.run([sys.executable, install_script, '--force-commands'],
|
|
628
|
+
capture_output=True, text=True, encoding='utf-8', timeout=60)
|
|
629
|
+
print(' QoderWork 投影完成' if r.returncode == 0 else ' ⚠️ 投影失败: %s' % r.stderr[:80])
|
|
630
|
+
else:
|
|
631
|
+
print(' 跳过 (install_qoderwork.py 不存在)')
|
|
632
|
+
|
|
633
|
+
print('\n✓ 全系列同步完成。4载体已对齐:')
|
|
634
|
+
print(' IDE/Quest: .qoder/rules + skills + commands')
|
|
635
|
+
print(' CLI: AGENTS.md (需手动同步规则正文, 或 M5 自动化)')
|
|
636
|
+
print(' QoderWork: ~/.qoderwork/skills + commands')
|
|
637
|
+
print(' npm: packages/wlkj/templates/')
|
|
638
|
+
return 0
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def main():
|
|
642
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else 'check'
|
|
643
|
+
rest = sys.argv[2:]
|
|
644
|
+
# 所有分支: 把子命令从 argv 去掉, 让各 main() 的 argparse 正确解析剩余参数
|
|
645
|
+
sys.argv = [sys.argv[0]] + rest
|
|
646
|
+
if cmd == 'check':
|
|
647
|
+
sys.exit(main_check_carriers())
|
|
648
|
+
elif cmd in ('check-paths', 'paths'):
|
|
649
|
+
sys.exit(main_check_paths())
|
|
650
|
+
elif cmd == 'sync':
|
|
651
|
+
sys.exit(main_sync())
|
|
652
|
+
elif cmd == 'all':
|
|
653
|
+
sys.exit(main_all())
|
|
654
|
+
else:
|
|
655
|
+
print('用法: carriers.py [check|check-paths|sync|all]')
|
|
656
|
+
print(' check 全套一致性检查 (内容)')
|
|
657
|
+
print(' check-paths 查裸相对路径')
|
|
658
|
+
print(' sync 同步 description (支持 --dry-run)')
|
|
659
|
+
print(' all 全系列同步 (4载体一键对齐)')
|
|
660
|
+
|
|
661
|
+
if __name__ == '__main__':
|
|
662
|
+
main()
|