cckit 0.1.0__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.
- cckit/__init__.py +8 -0
- cckit/__main__.py +7 -0
- cckit/cckit.schema.json +136 -0
- cckit/cli.py +294 -0
- cckit/config.py +93 -0
- cckit/doctor.py +185 -0
- cckit/env.py +54 -0
- cckit/errors.py +10 -0
- cckit/exec.py +104 -0
- cckit/installer.py +538 -0
- cckit/link.py +79 -0
- cckit/lint.py +309 -0
- cckit/manifest.py +113 -0
- cckit/registry.py +136 -0
- cckit/schema.py +42 -0
- cckit/state.py +484 -0
- cckit-0.1.0.dist-info/METADATA +9 -0
- cckit-0.1.0.dist-info/RECORD +21 -0
- cckit-0.1.0.dist-info/WHEEL +4 -0
- cckit-0.1.0.dist-info/entry_points.txt +2 -0
- cckit-0.1.0.dist-info/licenses/LICENSE +21 -0
cckit/__init__.py
ADDED
cckit/__main__.py
ADDED
cckit/cckit.schema.json
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/CCKitKit/schema/cckit.schema.json",
|
|
4
|
+
"title": "cckit.yaml",
|
|
5
|
+
"description": "CCKitKit kit manifest. 详见 Docs/03-manifest-spec.md",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["cckit", "kit", "version", "description", "skills"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"cckit": { "const": 1, "description": "manifest 格式版本" },
|
|
11
|
+
"kit": { "$ref": "#/$defs/slug" },
|
|
12
|
+
"version": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)*$"
|
|
15
|
+
},
|
|
16
|
+
"description": { "type": "string", "minLength": 1, "maxLength": 200 },
|
|
17
|
+
"author": { "type": "string" },
|
|
18
|
+
"homepage": { "type": "string", "format": "uri" },
|
|
19
|
+
"license": { "type": "string" },
|
|
20
|
+
"platforms": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"minItems": 1,
|
|
23
|
+
"uniqueItems": true,
|
|
24
|
+
"items": { "enum": ["windows", "linux", "macos"] },
|
|
25
|
+
"description": "缺省视为全平台支持"
|
|
26
|
+
},
|
|
27
|
+
"requires": { "$ref": "#/$defs/requires" },
|
|
28
|
+
"env": { "type": "array", "items": { "$ref": "#/$defs/envVar" } },
|
|
29
|
+
"skills": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"minItems": 1,
|
|
32
|
+
"items": { "$ref": "#/$defs/skill" }
|
|
33
|
+
},
|
|
34
|
+
"postinstall": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "$ref": "#/$defs/postinstallStep" }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"$defs": {
|
|
40
|
+
"slug": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
43
|
+
"maxLength": 64,
|
|
44
|
+
"description": "小写 kebab-case。大写会导致 Linux/Windows 行为分裂"
|
|
45
|
+
},
|
|
46
|
+
"requires": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"properties": {
|
|
50
|
+
"runtime": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"properties": {
|
|
54
|
+
"python": { "type": "string", "description": "PEP 440 约束,如 >=3.11" },
|
|
55
|
+
"node": { "type": "string", "description": "semver 约束,如 >=20" }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"system": { "type": "array", "items": { "$ref": "#/$defs/systemDep" } },
|
|
59
|
+
"python": { "$ref": "#/$defs/depFile" },
|
|
60
|
+
"node": { "$ref": "#/$defs/depFile" }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"depFile": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["file"],
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"file": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "相对 kit 根的标准依赖文件,如 requirements.txt"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"systemDep": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"required": ["bin", "hint"],
|
|
77
|
+
"additionalProperties": false,
|
|
78
|
+
"properties": {
|
|
79
|
+
"bin": { "type": "string", "minLength": 1 },
|
|
80
|
+
"hint": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"minProperties": 1,
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"description": "必须是分平台 map。单字符串是 Windows-only 错误,见 D-08",
|
|
85
|
+
"properties": {
|
|
86
|
+
"windows": { "type": "string" },
|
|
87
|
+
"linux": { "type": "string" },
|
|
88
|
+
"macos": { "type": "string" }
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"version": { "type": "string" },
|
|
92
|
+
"version_cmd": { "type": "string" }
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"envVar": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"required": ["name"],
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"properties": {
|
|
100
|
+
"name": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" },
|
|
101
|
+
"required": { "type": "boolean", "default": false },
|
|
102
|
+
"description": { "type": "string" }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"skill": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": ["name", "needs"],
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"properties": {
|
|
110
|
+
"name": {
|
|
111
|
+
"allOf": [{ "$ref": "#/$defs/slug" }],
|
|
112
|
+
"description": "必须与 skill 目录名完全一致。保留名由 lint 另行拒绝"
|
|
113
|
+
},
|
|
114
|
+
"needs": {
|
|
115
|
+
"type": "array",
|
|
116
|
+
"uniqueItems": true,
|
|
117
|
+
"items": { "type": "string" },
|
|
118
|
+
"description": "python/node 触发环境安装;其余值须为 requires.system[].bin"
|
|
119
|
+
},
|
|
120
|
+
"scripts": { "type": "array", "items": { "type": "string" } }
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"postinstallStep": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"required": ["run"],
|
|
126
|
+
"additionalProperties": false,
|
|
127
|
+
"properties": {
|
|
128
|
+
"run": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"description": "相对 kit 根。只允许操作 kit 自身目录与其 env"
|
|
131
|
+
},
|
|
132
|
+
"when": { "enum": ["always", "python", "node"], "default": "always" }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
cckit/cli.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""cckit 命令行入口。
|
|
2
|
+
|
|
3
|
+
argparse 分发到各子命令:add / list / enable / disable / name-only /
|
|
4
|
+
remove / doctor / exec。破坏性操作在非 -y 时需确认。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from . import doctor, exec as exec_mod, installer, state
|
|
13
|
+
from .errors import CckitError
|
|
14
|
+
|
|
15
|
+
_SYMBOLS = {"installed": "·", "enabled": "●", "name-only": "◐", "off": "○"} # 状态图标预定义
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _scope(args) -> str:
|
|
19
|
+
"""
|
|
20
|
+
返回作用域字符串,project 或 global。默认 global,除非 --project 指定。
|
|
21
|
+
|
|
22
|
+
:param args: argparse.Namespace, 解析后的命令行参数
|
|
23
|
+
|
|
24
|
+
:return: 作用域字符串
|
|
25
|
+
"""
|
|
26
|
+
return "project" if getattr(args, "project", False) else "global"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fmt(n: int) -> str:
|
|
30
|
+
"""
|
|
31
|
+
格式化数字,大于 1000 的用 k 表示。
|
|
32
|
+
|
|
33
|
+
:param n: int, 要格式化的数字
|
|
34
|
+
|
|
35
|
+
:return: 格式化后的字符串
|
|
36
|
+
"""
|
|
37
|
+
if n >= 1000:
|
|
38
|
+
return f"{n / 1000:.1f}k"
|
|
39
|
+
return str(n)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _pct(used: int, limit: int) -> str:
|
|
43
|
+
"""
|
|
44
|
+
百分比计算,limit 为 0 时返回 "∞"。
|
|
45
|
+
|
|
46
|
+
:param used: int, 已使用的数量
|
|
47
|
+
:param limit: int, 限制的数量
|
|
48
|
+
|
|
49
|
+
:return: 百分比字符串
|
|
50
|
+
"""
|
|
51
|
+
if limit <= 0:
|
|
52
|
+
return "∞"
|
|
53
|
+
return str(used * 100 // limit)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _env_label(s: state.SkillState) -> str:
|
|
57
|
+
"""
|
|
58
|
+
返回 skill 的依赖环境状态标签。
|
|
59
|
+
|
|
60
|
+
:param s: state.SkillState, skill 的状态对象
|
|
61
|
+
|
|
62
|
+
:return: 依赖环境状态标签字符串
|
|
63
|
+
"""
|
|
64
|
+
if not s.managed:
|
|
65
|
+
return ""
|
|
66
|
+
if s.env_ok is None:
|
|
67
|
+
return "prompt-only"
|
|
68
|
+
if s.env_ok:
|
|
69
|
+
return "env ok"
|
|
70
|
+
return "env MISSING → cckit doctor"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _as_dict(s: state.SkillState) -> dict:
|
|
74
|
+
"""
|
|
75
|
+
将 SkillState 对象转换为字典,用于 JSON 输出。
|
|
76
|
+
|
|
77
|
+
:param s: state.SkillState, skill 的状态对象
|
|
78
|
+
|
|
79
|
+
:return: 字典表示的 skill 状态
|
|
80
|
+
"""
|
|
81
|
+
return {
|
|
82
|
+
"name": s.name,
|
|
83
|
+
"kit": s.kit,
|
|
84
|
+
"scope": s.scope,
|
|
85
|
+
"state": s.state,
|
|
86
|
+
"managed": s.managed,
|
|
87
|
+
"env_ok": s.env_ok,
|
|
88
|
+
"desc_chars": s.desc_chars,
|
|
89
|
+
"version": s.version,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _print_list(skills: list[state.SkillState]) -> None:
|
|
94
|
+
"""
|
|
95
|
+
打印 skill 列表。
|
|
96
|
+
|
|
97
|
+
:param skills: list[state.SkillState], skill 列表
|
|
98
|
+
"""
|
|
99
|
+
# 按 kit 分组打印,kit 内按 name 排序
|
|
100
|
+
by_kit: dict[str, list[state.SkillState]] = {}
|
|
101
|
+
|
|
102
|
+
for s in skills:
|
|
103
|
+
by_kit.setdefault(s.kit or "(非 cckit 管理)", []).append(s)
|
|
104
|
+
for kit, items in by_kit.items():
|
|
105
|
+
ver = next((i.version for i in items if i.version), None)
|
|
106
|
+
if ver:
|
|
107
|
+
print(f"{kit} {ver}")
|
|
108
|
+
else:
|
|
109
|
+
print(kit)
|
|
110
|
+
for s in items:
|
|
111
|
+
env = _env_label(s)
|
|
112
|
+
scope_tag = " [project]" if s.scope == "project" else ""
|
|
113
|
+
ro_tag = " [只读]" if not s.managed else ""
|
|
114
|
+
print(f" {_SYMBOLS[s.state]} {s.name:<20} {s.state:<10} {env}"
|
|
115
|
+
f"{scope_tag}{ro_tag}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---- 子命令 ----
|
|
119
|
+
|
|
120
|
+
# 各个子命令的实现函数,返回 int 作为退出码
|
|
121
|
+
|
|
122
|
+
def cmd_add(args) -> int:
|
|
123
|
+
installer.install(
|
|
124
|
+
args.source, ref=args.ref, project=args.project,
|
|
125
|
+
no_enable=args.no_enable, only=args.only, assume_yes=args.yes, is_local_path=args.local)
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cmd_list(args) -> int:
|
|
130
|
+
if args.all:
|
|
131
|
+
scope = None
|
|
132
|
+
elif args.project:
|
|
133
|
+
scope = "project"
|
|
134
|
+
else:
|
|
135
|
+
scope = "global"
|
|
136
|
+
skills = state.list_skills(scope)
|
|
137
|
+
if args.json:
|
|
138
|
+
print(json.dumps([_as_dict(s) for s in skills], ensure_ascii=False, indent=2))
|
|
139
|
+
return 0
|
|
140
|
+
if not skills:
|
|
141
|
+
print("(无 skill)")
|
|
142
|
+
else:
|
|
143
|
+
_print_list(skills)
|
|
144
|
+
used, limit = state.budget(scope)
|
|
145
|
+
print(f"\n清单预算: {_fmt(used)} / {_fmt(limit)} 字符 ({_pct(used, limit)}%)")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_enable(args) -> int:
|
|
150
|
+
scope = _scope(args)
|
|
151
|
+
state.set_state(args.skill, "enabled", scope)
|
|
152
|
+
print(f"{args.skill} → enabled({scope}),新会话生效")
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_disable(args) -> int:
|
|
157
|
+
scope = _scope(args)
|
|
158
|
+
if args.purge:
|
|
159
|
+
state.set_state(args.skill, "installed", scope)
|
|
160
|
+
print(f"{args.skill} → installed({scope}),新会话生效")
|
|
161
|
+
else:
|
|
162
|
+
state.set_state(args.skill, "off", scope)
|
|
163
|
+
print(f"{args.skill} → off({scope}),新会话生效")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_name_only(args) -> int:
|
|
168
|
+
scope = _scope(args)
|
|
169
|
+
state.set_state(args.skill, "name-only", scope)
|
|
170
|
+
print(f"{args.skill} → name-only({scope}),新会话生效")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_remove(args) -> int:
|
|
175
|
+
if not args.yes:
|
|
176
|
+
ans = input(f"确认移除 kit {args.kit!r}? [y/N] ").strip().lower()
|
|
177
|
+
if ans not in ("y", "yes"):
|
|
178
|
+
print("已取消")
|
|
179
|
+
return 0
|
|
180
|
+
installer.remove_kit(args.kit, keep_env=args.keep_env)
|
|
181
|
+
print(f"已移除 {args.kit}")
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def cmd_doctor(args) -> int:
|
|
186
|
+
findings = doctor.run(fix=args.fix)
|
|
187
|
+
for f in findings:
|
|
188
|
+
icon = {"ok": "[ok]", "warn": "[warn]", "error": "[error]"}[f.status]
|
|
189
|
+
print(f"{icon} {f.check}: {f.message}")
|
|
190
|
+
if f.fix and f.status != "ok":
|
|
191
|
+
print(f" fix: {f.fix}")
|
|
192
|
+
n_err = sum(1 for f in findings if f.status == "error")
|
|
193
|
+
if args.fix:
|
|
194
|
+
print("\n已执行安全修复(清理悬空 link、重建 env)")
|
|
195
|
+
print(f"\n{len(findings)} 项检查,{n_err} 项错误")
|
|
196
|
+
return 1 if n_err else 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_exec(args) -> int:
|
|
200
|
+
return exec_mod.run(args.skill, args.script, args.args, scope=_scope(args))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---- parser ----
|
|
204
|
+
|
|
205
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
206
|
+
p = argparse.ArgumentParser(prog="cckit", description="Claude Code 的 Skill 工具箱管理器")
|
|
207
|
+
sub = p.add_subparsers(dest="command")
|
|
208
|
+
|
|
209
|
+
a = sub.add_parser("add", help="安装一个 kit")
|
|
210
|
+
a.add_argument("source", help="git URL(默认)或本地路径(配合 --local)")
|
|
211
|
+
a.add_argument("--local", action="store_true", help="source 解释为本地路径进行安装")
|
|
212
|
+
a.add_argument("--ref", help="分支 / tag / commit")
|
|
213
|
+
a.add_argument("--no-enable", action="store_true", help="只装不启用(installed 态)")
|
|
214
|
+
a.add_argument("--project", action="store_true", help="装到当前项目而非全局")
|
|
215
|
+
a.add_argument("--only", help="只启用指定 skill(逗号分隔)")
|
|
216
|
+
a.add_argument("-y", "--yes", action="store_true",
|
|
217
|
+
help="跳过确认(CI 用)。⚠️ 安装等于运行仓库作者代码,请自行确认来源可信")
|
|
218
|
+
a.set_defaults(func=cmd_add)
|
|
219
|
+
|
|
220
|
+
l = sub.add_parser("list", help="列出 skill(附清单预算)")
|
|
221
|
+
g = l.add_mutually_exclusive_group()
|
|
222
|
+
g.add_argument("--project", action="store_true", help="只列项目作用域")
|
|
223
|
+
g.add_argument("--all", action="store_true", help="列出全部作用域")
|
|
224
|
+
l.add_argument("--json", action="store_true", help="机器可读输出")
|
|
225
|
+
l.set_defaults(func=cmd_list)
|
|
226
|
+
|
|
227
|
+
e = sub.add_parser("enable", help="启用 skill")
|
|
228
|
+
e.add_argument("skill")
|
|
229
|
+
e.add_argument("--project", action="store_true")
|
|
230
|
+
e.set_defaults(func=cmd_enable)
|
|
231
|
+
|
|
232
|
+
d = sub.add_parser("disable", help="禁用 skill(默认写 skillOverrides: off)")
|
|
233
|
+
d.add_argument("skill")
|
|
234
|
+
d.add_argument("--purge", action="store_true", help="删 link,彻底移出扫描范围")
|
|
235
|
+
d.add_argument("--project", action="store_true")
|
|
236
|
+
d.set_defaults(func=cmd_disable)
|
|
237
|
+
|
|
238
|
+
n = sub.add_parser("name-only", help="只显示名字,省清单预算")
|
|
239
|
+
n.add_argument("skill")
|
|
240
|
+
n.add_argument("--project", action="store_true")
|
|
241
|
+
n.set_defaults(func=cmd_name_only)
|
|
242
|
+
|
|
243
|
+
r = sub.add_parser("remove", help="卸载 kit(清理 link/env/store/registry/overrides)")
|
|
244
|
+
r.add_argument("kit")
|
|
245
|
+
r.add_argument("--keep-env", action="store_true", help="保留 env(便于重装调试)")
|
|
246
|
+
r.add_argument("-y", "--yes", action="store_true", help="跳过确认")
|
|
247
|
+
r.set_defaults(func=cmd_remove)
|
|
248
|
+
|
|
249
|
+
doc = sub.add_parser("doctor", help="诊断(只诊断不自动修)")
|
|
250
|
+
doc.add_argument("--fix", action="store_true", help="只修明确安全的项")
|
|
251
|
+
doc.set_defaults(func=cmd_doctor)
|
|
252
|
+
|
|
253
|
+
x = sub.add_parser("exec", help="运行 skill 脚本")
|
|
254
|
+
x.add_argument("skill")
|
|
255
|
+
x.add_argument("script")
|
|
256
|
+
x.add_argument("--project", action="store_true", help="作用于项目作用域")
|
|
257
|
+
x.add_argument("args", nargs=argparse.REMAINDER, help="传给脚本的参数")
|
|
258
|
+
x.set_defaults(func=cmd_exec)
|
|
259
|
+
|
|
260
|
+
return p
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _configure_stdio() -> None:
|
|
264
|
+
"""把 stdout/stderr 切到 UTF-8。
|
|
265
|
+
|
|
266
|
+
Windows 上 Python 的默认控制台/管道编码是 GBK,而 list 的 ●/◐/○、计划里的
|
|
267
|
+
⚠️ 等符号无法被 GBK 编码,会直接抛 UnicodeEncodeError。统一改成 UTF-8。
|
|
268
|
+
"""
|
|
269
|
+
for stream in (sys.stdout, sys.stderr):
|
|
270
|
+
try:
|
|
271
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
272
|
+
except (AttributeError, ValueError):
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main(argv: list[str] | None = None) -> int:
|
|
277
|
+
_configure_stdio()
|
|
278
|
+
parser = build_parser()
|
|
279
|
+
args = parser.parse_args(argv)
|
|
280
|
+
if not getattr(args, "func", None):
|
|
281
|
+
parser.print_help()
|
|
282
|
+
return 0
|
|
283
|
+
try:
|
|
284
|
+
return args.func(args) or 0
|
|
285
|
+
except CckitError as e:
|
|
286
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
287
|
+
return 1
|
|
288
|
+
except KeyboardInterrupt:
|
|
289
|
+
print("已中断", file=sys.stderr)
|
|
290
|
+
return 130
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == "__main__":
|
|
294
|
+
raise SystemExit(main())
|
cckit/config.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""cckit 的路径与平台解析。
|
|
2
|
+
|
|
3
|
+
集中处理三类"环境相关"的问题:
|
|
4
|
+
- cckit 自身的数据目录(CCKIT_HOME 可覆盖 ~/.cckit)
|
|
5
|
+
- Claude Code 配置目录(必须经 CLAUDE_CONFIG_DIR 解析,禁止硬编码 ~/.claude)
|
|
6
|
+
- 当前项目根(向上找 .claude 目录或 cckit.lock 文件)
|
|
7
|
+
|
|
8
|
+
平台名只做 sys.platform → manifest 平台名的映射,不做行为分支。
|
|
9
|
+
真正的平台行为分支只存在于适配层:link.py(链接)与 env.py(解释器路径)。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# 清单预算上限缺省值:约 256k token 上下文的 1%,按 ~4 字符/token 估算。
|
|
18
|
+
# skillListingBudgetFraction 会按比例缩放它,见 state.budget()。
|
|
19
|
+
DEFAULT_BUDGET_LIMIT_CHARS = 10240
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cckit_home() -> Path:
|
|
23
|
+
"""cckit 数据目录。CCKIT_HOME 覆盖,缺省 ~/.cckit。"""
|
|
24
|
+
env = os.environ.get("CCKIT_HOME")
|
|
25
|
+
if env:
|
|
26
|
+
return Path(env).expanduser()
|
|
27
|
+
return Path.home() / ".cckit"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def store_dir() -> Path:
|
|
31
|
+
"""kit 真实文件的存放处,全局唯一一份。"""
|
|
32
|
+
return cckit_home() / "store"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def envs_dir() -> Path:
|
|
36
|
+
"""每 skill 一个独立环境(venv / node_modules)。"""
|
|
37
|
+
return cckit_home() / "envs"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def logs_dir() -> Path:
|
|
41
|
+
"""日志与临时目录。"""
|
|
42
|
+
return cckit_home() / "logs"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def registry_path() -> Path:
|
|
46
|
+
"""registry.json 的位置。"""
|
|
47
|
+
return cckit_home() / "registry.json"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def usage_log_path() -> Path:
|
|
51
|
+
"""用量统计,追加式 JSONL,天然并发安全(见 Docs/09)。"""
|
|
52
|
+
return cckit_home() / "usage.jsonl"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def claude_config_dir() -> Path:
|
|
56
|
+
"""Claude Code 配置目录。
|
|
57
|
+
|
|
58
|
+
回落顺序(已核实):CLAUDE_CONFIG_DIR > XDG_CONFIG_HOME/claude > ~/.claude。
|
|
59
|
+
不可硬编码 ~/.claude —— CC 二进制允许重定向配置目录(见 Docs/06 2.5)。
|
|
60
|
+
"""
|
|
61
|
+
env = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
62
|
+
if env:
|
|
63
|
+
return Path(env).expanduser()
|
|
64
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
65
|
+
if xdg:
|
|
66
|
+
return Path(xdg).expanduser() / "claude"
|
|
67
|
+
return Path.home() / ".claude"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def project_root(start: Path | None = None) -> Path | None:
|
|
71
|
+
"""向上找项目根:含 cckit.lock 文件,或含 .claude/ 目录(排除全局配置目录)。
|
|
72
|
+
|
|
73
|
+
全局 CC 配置目录(CLAUDE_CONFIG_DIR 或 ~/.claude)不是"项目",必须排除 ——
|
|
74
|
+
否则从任意目录向上找都会停在用户家目录,把全局 skills 误当成项目 skill。
|
|
75
|
+
"""
|
|
76
|
+
cc_dirs = {claude_config_dir().resolve(), (Path.home() / ".claude").resolve()}
|
|
77
|
+
cur = Path(start or os.getcwd()).resolve()
|
|
78
|
+
for d in (cur, *cur.parents):
|
|
79
|
+
if (d / "cckit.lock").is_file():
|
|
80
|
+
return d
|
|
81
|
+
claude = d / ".claude"
|
|
82
|
+
if claude.is_dir() and claude.resolve() not in cc_dirs:
|
|
83
|
+
return d
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def platform_name() -> str:
|
|
88
|
+
"""sys.platform → manifest 平台名。只做命名映射,不做行为分支。"""
|
|
89
|
+
if sys.platform == "win32":
|
|
90
|
+
return "windows"
|
|
91
|
+
if sys.platform == "darwin":
|
|
92
|
+
return "macos"
|
|
93
|
+
return "linux"
|