skillreg 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.
- skillreg/__init__.py +8 -0
- skillreg/builtin/skillreg-skill/SKILL.md +94 -0
- skillreg/builtin/skillreg-skill/references/install.md +90 -0
- skillreg/builtin/skillreg-skill/references/troubleshooting.md +143 -0
- skillreg/cli.py +137 -0
- skillreg/config.py +81 -0
- skillreg/server/__init__.py +148 -0
- skillreg/server/compat.py +77 -0
- skillreg/server/files.py +34 -0
- skillreg/server/git.py +78 -0
- skillreg/server/health.py +26 -0
- skillreg/server/hooks.py +118 -0
- skillreg/server/import_.py +180 -0
- skillreg/server/registry.py +71 -0
- skillreg/server/skills.py +155 -0
- skillreg/server/submodules.py +304 -0
- skillreg/server/sync.py +246 -0
- skillreg/server/workspace.py +58 -0
- skillreg/services/__init__.py +3 -0
- skillreg/services/file_browser.py +197 -0
- skillreg/services/importer.py +672 -0
- skillreg/services/self_skill.py +68 -0
- skillreg/services/skill_registry.py +555 -0
- skillreg/services/sync_manager.py +561 -0
- skillreg/services/url_utils.py +28 -0
- skillreg-0.1.0.dist-info/METADATA +162 -0
- skillreg-0.1.0.dist-info/RECORD +30 -0
- skillreg-0.1.0.dist-info/WHEEL +4 -0
- skillreg-0.1.0.dist-info/entry_points.txt +2 -0
- skillreg-0.1.0.dist-info/licenses/LICENSE +21 -0
skillreg/__init__.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skillreg-skill
|
|
3
|
+
description: 当用户要注册、导入、转换或同步本地 skill 时使用。用户可能在任意项目里对 agent 说“注册这个 skill”;优先把当前项目或用户给定路径中 agent/用户写好的 SKILL.md 注册到当前 skillreg workspace,而不是要求用户理解产品仓、workspace 仓等实现细节。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# skillreg-skill
|
|
7
|
+
|
|
8
|
+
当用户想用 `skillreg` 完成 skill 的注册、导入、转换、同步、查看状态或排障时,优先使用这个 skill。
|
|
9
|
+
|
|
10
|
+
核心心智模型:
|
|
11
|
+
|
|
12
|
+
- 用户关心的是“把本地已经写好的 skill 放进我的 skill workspace,并同步给 agent 用”。
|
|
13
|
+
- 用户不需要关心当前是在业务项目、skill 源码项目、`skillreg` 产品仓,还是 workspace 仓。
|
|
14
|
+
- agent 要负责识别本地 skill 来源、确认当前 `skillreg` workspace,然后完成注册或给出可执行的下一步。
|
|
15
|
+
|
|
16
|
+
## 触发场景
|
|
17
|
+
|
|
18
|
+
- 用户说"用 skillreg 管一下这个 skill"
|
|
19
|
+
- 用户说"注册这个 skill"、"把这个 skill 注册一下"、"把这个目录注册成 skill"
|
|
20
|
+
- 用户要把一个本地目录、zip、git 仓库注册进 workspace
|
|
21
|
+
- 用户要把 `skills/<name>` 转成 `repos/<name>-cli/skill/<name>`
|
|
22
|
+
- 用户要把 workspace 中的 skills 同步到 Claude、Codex、`.agents/skills` 等目标目录
|
|
23
|
+
- 用户反馈 `skillreg` 命令找不到、dashboard 起不来、workspace 不生效、sync 结果不对
|
|
24
|
+
|
|
25
|
+
## 先做什么
|
|
26
|
+
|
|
27
|
+
1. 识别要注册的 skill 来源:
|
|
28
|
+
- 用户给了路径:优先用该路径。
|
|
29
|
+
- 用户没给路径:从当前工作目录开始找 `SKILL.md`;如果当前目录本身就是 skill 目录,直接使用当前目录。
|
|
30
|
+
- 如果找不到 `SKILL.md`,先告诉用户需要一个包含 `SKILL.md` 的目录,并说明当前检查过哪里。
|
|
31
|
+
2. 跑 `skillreg config`,确认当前 `workspace_path`、targets 和配置文件位置。
|
|
32
|
+
3. 如果 `workspace_path` 为空,先创建或引导选择 workspace;不要要求用户判断“产品仓/工作区仓”。
|
|
33
|
+
4. 如果命令不存在或环境没装好,读 `references/install.md`。
|
|
34
|
+
5. 如果命令能跑但行为异常(workspace 不对、dashboard/sync 出错),读 `references/troubleshooting.md`。
|
|
35
|
+
|
|
36
|
+
## 推荐工作流
|
|
37
|
+
|
|
38
|
+
### 1. 创建或确认 workspace
|
|
39
|
+
|
|
40
|
+
| 场景 | 操作 |
|
|
41
|
+
|------|------|
|
|
42
|
+
| 新建 workspace | `skillreg workspace create ~/my-skills` |
|
|
43
|
+
| 已有 workspace | 确认它至少包含 `skills/` 目录 |
|
|
44
|
+
| 需要图形操作 | `skillreg dashboard open` |
|
|
45
|
+
|
|
46
|
+
说明:
|
|
47
|
+
- workspace 切换主要在 dashboard 中完成。
|
|
48
|
+
- CLI 更适合做"确认配置、创建 workspace、启动 dashboard"这几个入口动作。
|
|
49
|
+
|
|
50
|
+
### 2. 把本地 skill 纳入 workspace
|
|
51
|
+
|
|
52
|
+
根据用户目标选择入口:
|
|
53
|
+
|
|
54
|
+
| 目标 | 入口 |
|
|
55
|
+
|------|------|
|
|
56
|
+
| 把当前目录或给定路径里的 skill 注册进 workspace | dashboard 的 import 流程或 `/api/registry/register` |
|
|
57
|
+
| 保留原目录内容并注册进 workspace | dashboard 的 import 流程 |
|
|
58
|
+
| 已有 registry 风格调用面 | `/api/registry/register` |
|
|
59
|
+
| 把 file skill 变成 repo/CLI 骨架 | `/api/registry/convert` |
|
|
60
|
+
|
|
61
|
+
要点:
|
|
62
|
+
- `register` → 把 skill 放进 `skills/<name>/`
|
|
63
|
+
- `convert` → 把 `skills/<name>` 迁到 `repos/<name>-cli/skill/<name>/`,并生成基础 CLI repo 骨架
|
|
64
|
+
- “注册”默认不是注册当前代码仓库本身,而是注册其中包含 `SKILL.md` 的 skill 目录。
|
|
65
|
+
- 如果用户在任意业务项目里说“注册 skill”,agent 应主动定位该项目中的 `SKILL.md` 或询问具体 skill 目录。
|
|
66
|
+
|
|
67
|
+
### 3. 同步到 agent 目标目录
|
|
68
|
+
|
|
69
|
+
1. 在 dashboard 的 Sync 页面配置 targets。
|
|
70
|
+
2. 执行同步,检查状态。
|
|
71
|
+
|
|
72
|
+
状态含义(不要混淆):
|
|
73
|
+
|
|
74
|
+
| 状态 | 含义 |
|
|
75
|
+
|------|------|
|
|
76
|
+
| `synced` | workspace 和 target 一致 |
|
|
77
|
+
| `modified` | target 里有本地改动或差异 |
|
|
78
|
+
| `missing` | 配置里有这个 skill,但目标缺失 |
|
|
79
|
+
| `not-installed` | 该 target 当前还没有安装这个 skill |
|
|
80
|
+
|
|
81
|
+
## agent 行为约束
|
|
82
|
+
|
|
83
|
+
- **先验证再行动**:先用 `skillreg config`、dashboard、API 或测试确认能力真的可用。
|
|
84
|
+
- **用户心智优先**:不要把"产品仓 / workspace 仓 / submodule 仓"作为用户必须回答的问题;这些只是 agent 内部判断路径和命令时的实现细节。
|
|
85
|
+
- **任意项目可触发**:用户可能在任何项目里说"注册 skill"。先找本地 `SKILL.md`,再把它注册进 `skillreg config` 指向的 workspace。
|
|
86
|
+
- **优先工具**:如果用户只是要完成一项实际操作,优先直接用 `skillreg` 完成,不要手工复制目录。
|
|
87
|
+
- **排障先诊断**:如果用户是在排障,先给出现状和根因,再决定是否需要改代码或改配置。
|
|
88
|
+
|
|
89
|
+
## 何时读参考文档
|
|
90
|
+
|
|
91
|
+
| 问题类型 | 参考文档 |
|
|
92
|
+
|----------|----------|
|
|
93
|
+
| 安装、开发态启动、PATH/venv 问题 | `references/install.md` |
|
|
94
|
+
| dashboard 起不来、workspace 不对、sync 异常、skill 没注入、命令行为不符合预期 | `references/troubleshooting.md` |
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# skillreg 安装与启动
|
|
2
|
+
|
|
3
|
+
这个文档给 agent 一个稳定起点:当用户要“安装 skillreg”“把 skillreg CLI 跑起来”“确认本地环境能不能用”时,先按这里走。
|
|
4
|
+
|
|
5
|
+
## 1. 先确认本机入口
|
|
6
|
+
|
|
7
|
+
无论用户当前在哪个项目里,都先用下面两个命令识别 `skillreg` 是否可用、当前 workspace 指向哪里:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pwd
|
|
11
|
+
skillreg config
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
如果 `skillreg` 命令不存在,再继续看下面的安装方式。
|
|
15
|
+
|
|
16
|
+
不要要求用户先判断自己是在“产品仓”还是“workspace 仓”。用户通常只是在某个本地项目里想把一个包含 `SKILL.md` 的目录注册进 workspace。
|
|
17
|
+
|
|
18
|
+
## 2. 面向用户的 CLI 安装
|
|
19
|
+
|
|
20
|
+
如果目标是给本机装一个可直接调用的 `skillreg`,优先建议:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv tool install skillreg
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
安装后先确认:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
skillreg config
|
|
30
|
+
which skillreg
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
如果项目还没真正发布到公开包源,`uv tool install skillreg` 可能失败。这种情况下要明确告诉用户:
|
|
34
|
+
|
|
35
|
+
- 当前更可靠的是源码开发态安装
|
|
36
|
+
- 或者从本地仓库执行 `uv pip install -e .`
|
|
37
|
+
|
|
38
|
+
## 3. 开发态安装
|
|
39
|
+
|
|
40
|
+
当 agent 正在 `skillreg` 仓库里改代码时,优先用开发态安装:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv venv
|
|
44
|
+
uv pip install -e ".[dev]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
安装后可验证:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
skillreg config
|
|
51
|
+
skillreg dashboard open --no-browser
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
如果用户是在当前仓库里直接联调,也可以直接起服务:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m uvicorn skillreg.server:app --host 127.0.0.1 --port 8787
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 4. 初始化最小可用闭环
|
|
61
|
+
|
|
62
|
+
安装完成后,最小验证顺序:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
skillreg workspace create ~/my-skills
|
|
66
|
+
skillreg dashboard open --no-browser
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
然后确认:
|
|
70
|
+
|
|
71
|
+
- `~/.skillreg/config.json` 已生成
|
|
72
|
+
- `workspace_path` 指向新建 workspace
|
|
73
|
+
- workspace 下至少有 `skills/` 和 `repos/`
|
|
74
|
+
|
|
75
|
+
## 5. 常用命令
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
skillreg config
|
|
79
|
+
skillreg workspace create <path>
|
|
80
|
+
skillreg dashboard open
|
|
81
|
+
skillreg dashboard open --no-browser
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
当前 agent 应该把 CLI 看成:
|
|
85
|
+
|
|
86
|
+
- `config`:确认配置和当前 workspace
|
|
87
|
+
- `workspace create`:创建新的 workspace
|
|
88
|
+
- `dashboard open`:进入主要操作入口
|
|
89
|
+
|
|
90
|
+
workspace 切换、导入、sync target 管理,目前主要还是通过 dashboard 完成。
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# skillreg 排障指南
|
|
2
|
+
|
|
3
|
+
当用户反馈“装好了但不能用”“workspace 不对”“sync 看起来不可信”“dashboard 起不来”时,按这个顺序排查。
|
|
4
|
+
|
|
5
|
+
## 1. 先看当前配置
|
|
6
|
+
|
|
7
|
+
第一步永远先跑:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
skillreg config
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
重点看:
|
|
14
|
+
|
|
15
|
+
- 配置文件是不是 `~/.skillreg/config.json`
|
|
16
|
+
- `workspace_path` 有没有值
|
|
17
|
+
- 指向的路径是不是用户真正想操作的那个 workspace
|
|
18
|
+
|
|
19
|
+
如果 `workspace_path` 为空或错误,很多问题都会是表象。
|
|
20
|
+
|
|
21
|
+
## 2. workspace 问题
|
|
22
|
+
|
|
23
|
+
典型现象:
|
|
24
|
+
|
|
25
|
+
- skills 列表为空
|
|
26
|
+
- dashboard 里看不到预期 skill
|
|
27
|
+
- sync 用的是错误仓库
|
|
28
|
+
|
|
29
|
+
检查:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ls <workspace>
|
|
33
|
+
ls <workspace>/skills
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
确认:
|
|
37
|
+
|
|
38
|
+
- workspace 路径存在
|
|
39
|
+
- 它是目录,不是文件
|
|
40
|
+
- 至少包含 `skills/`
|
|
41
|
+
|
|
42
|
+
如果用户已经有 workspace,但 CLI 当前没指向它:
|
|
43
|
+
|
|
44
|
+
- 打开 dashboard
|
|
45
|
+
- 在 header 中切换到正确 workspace
|
|
46
|
+
|
|
47
|
+
## 3. dashboard 起不来
|
|
48
|
+
|
|
49
|
+
先区分是“命令起不来”,还是“服务起来了但页面打不开”。
|
|
50
|
+
|
|
51
|
+
### 命令起不来
|
|
52
|
+
|
|
53
|
+
优先检查:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
which skillreg
|
|
57
|
+
skillreg config
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
如果 `skillreg` 不存在,回到 `install.md` 重新安装。
|
|
61
|
+
|
|
62
|
+
### 服务起来但网页打不开
|
|
63
|
+
|
|
64
|
+
可直接用开发命令确认后端是否可起:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m uvicorn skillreg.server:app --host 127.0.0.1 --port 8787
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
再检查:
|
|
71
|
+
|
|
72
|
+
- 端口是否被占用
|
|
73
|
+
- `dashboard/dist` 是否存在
|
|
74
|
+
- 浏览器访问的是不是正确端口
|
|
75
|
+
|
|
76
|
+
## 4. register / convert 异常
|
|
77
|
+
|
|
78
|
+
### register 失败
|
|
79
|
+
|
|
80
|
+
优先检查源目录:
|
|
81
|
+
|
|
82
|
+
- 是否存在 `SKILL.md`
|
|
83
|
+
- frontmatter 里是否有 `name`
|
|
84
|
+
- `name` 是否满足 `[A-Za-z0-9][A-Za-z0-9_-]*`
|
|
85
|
+
|
|
86
|
+
常见冲突:
|
|
87
|
+
|
|
88
|
+
- `skills/<name>` 已存在
|
|
89
|
+
- 没有传 `force=true`
|
|
90
|
+
|
|
91
|
+
### convert 失败
|
|
92
|
+
|
|
93
|
+
优先检查:
|
|
94
|
+
|
|
95
|
+
- `skills/<name>/SKILL.md` 是否存在
|
|
96
|
+
- `repos/<name>-cli` 是否已经存在
|
|
97
|
+
|
|
98
|
+
语义上要记住:
|
|
99
|
+
|
|
100
|
+
- `convert` 是迁移,不是复制
|
|
101
|
+
- 成功后原来的 `skills/<name>` 会移入 `repos/<name>-cli/skill/<name>/`
|
|
102
|
+
|
|
103
|
+
## 5. sync 状态不对
|
|
104
|
+
|
|
105
|
+
先别急着改代码,先确认状态解释有没有搞错:
|
|
106
|
+
|
|
107
|
+
- `synced`:一致
|
|
108
|
+
- `modified`:目标侧有差异
|
|
109
|
+
- `missing`:配置/期望存在,但目标缺失
|
|
110
|
+
- `not-installed`:当前 target 还没安装这个 skill
|
|
111
|
+
|
|
112
|
+
如果用户说“怎么全是 missing / not-installed”,先检查:
|
|
113
|
+
|
|
114
|
+
- 当前 target 路径是不是对的
|
|
115
|
+
- target 目录是否真的存在
|
|
116
|
+
- workspace 当前选中的 skill 是否在目标里出现
|
|
117
|
+
|
|
118
|
+
## 6. builtin skill 没注入
|
|
119
|
+
|
|
120
|
+
`skillreg` 会把 builtin `skillreg-skill` 注入到:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
<workspace>/.skillreg/builtin/skillreg-skill/
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
如果用户说看不到:
|
|
127
|
+
|
|
128
|
+
1. 先确认当前 workspace 是对的
|
|
129
|
+
2. 再检查这个目录是否存在
|
|
130
|
+
3. 如果是开发态,确认源码里 `src/skillreg/builtin/skillreg-skill/` 存在
|
|
131
|
+
4. 如果是安装态,确认 wheel 中包含 builtin 目录
|
|
132
|
+
|
|
133
|
+
## 7. 什么时候该改代码
|
|
134
|
+
|
|
135
|
+
只有在下面这些都确认之后,再进入代码修复:
|
|
136
|
+
|
|
137
|
+
- 配置路径正确
|
|
138
|
+
- workspace 正确
|
|
139
|
+
- 输入目录和 skill 元数据合法
|
|
140
|
+
- target 路径和目标状态核对过
|
|
141
|
+
- CLI 安装和 PATH 没问题
|
|
142
|
+
|
|
143
|
+
也就是说,先排除“环境 / 配置 / 路径 / 术语理解”问题,再动实现。
|
skillreg/cli.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""skillreg CLI entry point.
|
|
2
|
+
|
|
3
|
+
Minimal command set for M1 (issue #02):
|
|
4
|
+
|
|
5
|
+
- ``skillreg config`` — print config status (auto-creates config).
|
|
6
|
+
- ``skillreg dashboard open`` — start the FastAPI backend + open a browser.
|
|
7
|
+
|
|
8
|
+
Built with `click`. The entry point ``skillreg = "skillreg.cli:main"`` is
|
|
9
|
+
declared in ``pyproject.toml``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import webbrowser
|
|
17
|
+
from typing import Optional, Sequence
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
import uvicorn
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from .config import CONFIG_FILE, load_config
|
|
24
|
+
|
|
25
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
26
|
+
DEFAULT_PORT = 8787
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_csv(values: Sequence[str]) -> str:
|
|
30
|
+
return ", ".join(values) if values else "(none)"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _echo_workspace_summary(*, heading: str = "skillreg context") -> None:
|
|
34
|
+
"""Print the current workspace context after user-facing commands."""
|
|
35
|
+
cfg = load_config()
|
|
36
|
+
click.echo(heading)
|
|
37
|
+
click.echo(f" config file : {CONFIG_FILE}")
|
|
38
|
+
click.echo(f" current workspace : {cfg.workspace_path or '(not configured)'}")
|
|
39
|
+
click.echo(f" sync targets : {_format_csv(cfg.targets)}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.group()
|
|
43
|
+
@click.version_option(version=__version__, package_name="skillreg")
|
|
44
|
+
def cli() -> None:
|
|
45
|
+
"""skillreg — skill registry control plane."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cli.command()
|
|
49
|
+
def config() -> None:
|
|
50
|
+
"""Print skillreg configuration status.
|
|
51
|
+
|
|
52
|
+
Auto-creates ``~/.skillreg/config.json`` with the default empty structure
|
|
53
|
+
on first run.
|
|
54
|
+
"""
|
|
55
|
+
cfg = load_config()
|
|
56
|
+
click.echo("skillreg config")
|
|
57
|
+
click.echo(f" config file : {CONFIG_FILE}")
|
|
58
|
+
click.echo(f" workspace : {cfg.workspace_path or '(not configured)'}")
|
|
59
|
+
click.echo(
|
|
60
|
+
f" targets : "
|
|
61
|
+
f"{', '.join(cfg.targets) if cfg.targets else '(none)'}"
|
|
62
|
+
)
|
|
63
|
+
click.echo(
|
|
64
|
+
f" agents : "
|
|
65
|
+
f"{', '.join(cfg.agents.keys()) if cfg.agents else '(none)'}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@cli.group()
|
|
70
|
+
def dashboard() -> None:
|
|
71
|
+
"""Manage the skillreg dashboard."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@cli.group()
|
|
75
|
+
def workspace() -> None:
|
|
76
|
+
"""Manage skillreg workspaces."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@workspace.command("create")
|
|
80
|
+
@click.argument("location", type=click.Path())
|
|
81
|
+
def create_workspace(location: str) -> None:
|
|
82
|
+
"""Create a new workspace at LOCATION.
|
|
83
|
+
|
|
84
|
+
Initializes a git repo with skills/ and repos/ directories,
|
|
85
|
+
writes .gitignore and README, and updates the config pointer.
|
|
86
|
+
"""
|
|
87
|
+
from .services.importer import create_workspace as _create
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = _create(location)
|
|
91
|
+
click.echo(f"✓ Workspace created at {result['workspace_path']}")
|
|
92
|
+
click.echo(f" git init : {'✓' if result['has_git'] else '✗'}")
|
|
93
|
+
click.echo(f" skills/ : {'✓' if result['has_skills_dir'] else '✗'}")
|
|
94
|
+
click.echo(f" repos/ : {'✓' if result['has_repos_dir'] else '✗'}")
|
|
95
|
+
_echo_workspace_summary(heading="skillreg context after workspace create")
|
|
96
|
+
except ValueError as e:
|
|
97
|
+
click.echo(f"✗ {e}", err=True)
|
|
98
|
+
raise SystemExit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dashboard.command("open")
|
|
102
|
+
@click.option("--host", default=DEFAULT_HOST, show_default=True, help="Bind host.")
|
|
103
|
+
@click.option("--port", default=DEFAULT_PORT, show_default=True, type=int, help="Bind port.")
|
|
104
|
+
@click.option(
|
|
105
|
+
"--no-browser",
|
|
106
|
+
is_flag=True,
|
|
107
|
+
default=False,
|
|
108
|
+
help="Do not open a browser; just start the server.",
|
|
109
|
+
)
|
|
110
|
+
def open_dashboard(host: str, port: int, no_browser: bool) -> None:
|
|
111
|
+
"""Start the FastAPI backend and open the dashboard in a browser."""
|
|
112
|
+
url = f"http://{host}:{port}"
|
|
113
|
+
if not no_browser:
|
|
114
|
+
# Delay browser launch so uvicorn has a moment to bind the socket.
|
|
115
|
+
threading.Thread(
|
|
116
|
+
target=lambda: (time.sleep(1.0), webbrowser.open(url)),
|
|
117
|
+
daemon=True,
|
|
118
|
+
).start()
|
|
119
|
+
click.echo(f"skillreg backend starting at {url}")
|
|
120
|
+
_echo_workspace_summary(heading="skillreg context for dashboard")
|
|
121
|
+
click.echo(" press Ctrl+C to stop.")
|
|
122
|
+
uvicorn.run(
|
|
123
|
+
"skillreg.server:app",
|
|
124
|
+
host=host,
|
|
125
|
+
port=port,
|
|
126
|
+
log_config=None,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
131
|
+
"""Entry point for the ``skillreg`` console script."""
|
|
132
|
+
cli(args=list(argv) if argv is not None else None, standalone_mode=True)
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|
skillreg/config.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Configuration management for skillreg.
|
|
2
|
+
|
|
3
|
+
Per PRD §2.3, all skillreg-local state lives in ``~/.skillreg/config.json``:
|
|
4
|
+
|
|
5
|
+
- ``workspace_path`` — pointer to the workspace repo (skills storage). The
|
|
6
|
+
workspace itself only holds ``skills/`` + ``repos/``; it no longer carries
|
|
7
|
+
``sync-skills.json`` or ``infra/``.
|
|
8
|
+
- ``targets`` — install targets (e.g. ``~/.claude/skills``).
|
|
9
|
+
- ``agents`` — agent conventions map (claude / codebuddy / codex …).
|
|
10
|
+
|
|
11
|
+
Exclude rules and manifest settings stay code-internal (not in this file).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
# --- locations -------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
CONFIG_DIR: Path = Path.home() / ".skillreg"
|
|
25
|
+
CONFIG_FILE: Path = CONFIG_DIR / "config.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --- model -----------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SkillregConfig(BaseModel):
|
|
32
|
+
"""Schema for ``~/.skillreg/config.json``."""
|
|
33
|
+
|
|
34
|
+
workspace_path: Optional[str] = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Absolute path to the workspace repo (skills storage). "
|
|
37
|
+
"null until the user configures it.",
|
|
38
|
+
)
|
|
39
|
+
targets: List[str] = Field(
|
|
40
|
+
default_factory=list,
|
|
41
|
+
description="Install targets (agent skill dirs), e.g. ~/.claude/skills.",
|
|
42
|
+
)
|
|
43
|
+
agents: Dict[str, Any] = Field(
|
|
44
|
+
default_factory=dict,
|
|
45
|
+
description="Agent conventions map. Keys are agent names "
|
|
46
|
+
"(claude / codebuddy / codex …); values are agent-specific convention "
|
|
47
|
+
"records (structure expanded in later issues).",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def default_config() -> SkillregConfig:
|
|
52
|
+
"""Return a fresh empty config with all expected fields."""
|
|
53
|
+
return SkillregConfig()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# --- I/O -------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_config(path: Optional[Path] = None) -> SkillregConfig:
|
|
60
|
+
"""Load config from ``path`` (default: ``~/.skillreg/config.json``).
|
|
61
|
+
|
|
62
|
+
If the file does not exist, it is created with the default empty structure
|
|
63
|
+
and returned. The config directory is created on demand.
|
|
64
|
+
"""
|
|
65
|
+
if path is None:
|
|
66
|
+
path = CONFIG_FILE
|
|
67
|
+
if not path.exists():
|
|
68
|
+
cfg = default_config()
|
|
69
|
+
save_config(cfg, path)
|
|
70
|
+
return cfg
|
|
71
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
72
|
+
# Coerce into the schema; unknown keys are dropped, missing keys defaulted.
|
|
73
|
+
return SkillregConfig.model_validate(data)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def save_config(cfg: SkillregConfig, path: Optional[Path] = None) -> None:
|
|
77
|
+
"""Persist ``cfg`` to ``path`` (default: ``~/.skillreg/config.json``)."""
|
|
78
|
+
if path is None:
|
|
79
|
+
path = CONFIG_FILE
|
|
80
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
path.write_text(cfg.model_dump_json(indent=2), encoding="utf-8")
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""FastAPI backend for skillreg.
|
|
2
|
+
|
|
3
|
+
Exposes API routes under ``/api`` and (when present) serves the built dashboard
|
|
4
|
+
static files via ``StaticFiles``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI, HTTPException
|
|
14
|
+
from fastapi.responses import FileResponse
|
|
15
|
+
|
|
16
|
+
from .. import __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def dashboard_dir() -> Optional[Path]:
|
|
20
|
+
"""Locate the dashboard static directory."""
|
|
21
|
+
env = os.environ.get("SKILLREG_DASHBOARD_DIR")
|
|
22
|
+
if env:
|
|
23
|
+
p = Path(env)
|
|
24
|
+
if p.is_dir():
|
|
25
|
+
return p
|
|
26
|
+
dashboard_root = Path(__file__).resolve().parents[3] / "dashboard"
|
|
27
|
+
dist = dashboard_root / "dist"
|
|
28
|
+
if dist.is_dir():
|
|
29
|
+
return dist
|
|
30
|
+
if dashboard_root.is_dir():
|
|
31
|
+
return dashboard_root
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_app() -> FastAPI:
|
|
36
|
+
"""Build the FastAPI application with all routes mounted."""
|
|
37
|
+
app = FastAPI(title="skillreg", version=__version__)
|
|
38
|
+
|
|
39
|
+
# Inject self-skill on startup if workspace is configured
|
|
40
|
+
from ..config import load_config
|
|
41
|
+
from ..services.self_skill import init_self_skill
|
|
42
|
+
|
|
43
|
+
cfg = load_config()
|
|
44
|
+
if cfg.workspace_path:
|
|
45
|
+
try:
|
|
46
|
+
init_self_skill(cfg.workspace_path)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass # Non-fatal: dashboard still works without self-skill
|
|
49
|
+
|
|
50
|
+
# Register route modules
|
|
51
|
+
from .health import router as health_router
|
|
52
|
+
|
|
53
|
+
app.include_router(health_router)
|
|
54
|
+
|
|
55
|
+
# Skills / registry / sync routes (built progressively)
|
|
56
|
+
try:
|
|
57
|
+
from .skills import router as skills_router
|
|
58
|
+
app.include_router(skills_router)
|
|
59
|
+
except ImportError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from .sync import router as sync_router
|
|
64
|
+
app.include_router(sync_router)
|
|
65
|
+
except ImportError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from .registry import router as registry_router
|
|
70
|
+
app.include_router(registry_router)
|
|
71
|
+
except ImportError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from .files import router as files_router
|
|
76
|
+
app.include_router(files_router)
|
|
77
|
+
except ImportError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
from .git import router as git_router
|
|
82
|
+
app.include_router(git_router)
|
|
83
|
+
except ImportError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
from .submodules import router as submodules_router
|
|
88
|
+
app.include_router(submodules_router)
|
|
89
|
+
except ImportError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
from .hooks import router as hooks_router
|
|
94
|
+
app.include_router(hooks_router)
|
|
95
|
+
except ImportError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
from .import_ import router as import_router
|
|
100
|
+
app.include_router(import_router)
|
|
101
|
+
except ImportError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
from .compat import router as compat_router
|
|
106
|
+
app.include_router(compat_router)
|
|
107
|
+
except ImportError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
from .workspace import router as workspace_router
|
|
112
|
+
app.include_router(workspace_router)
|
|
113
|
+
except ImportError:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
# Dashboard static serving (SPA fallback, after /api routes)
|
|
117
|
+
_d = dashboard_dir()
|
|
118
|
+
if _d is not None:
|
|
119
|
+
index_file = _d / "index.html"
|
|
120
|
+
if index_file.is_file():
|
|
121
|
+
@app.get("/assets/{asset_path:path}", include_in_schema=False)
|
|
122
|
+
def dashboard_assets(asset_path: str):
|
|
123
|
+
file_path = (_d / "assets" / asset_path).resolve()
|
|
124
|
+
assets_root = (_d / "assets").resolve()
|
|
125
|
+
try:
|
|
126
|
+
file_path.relative_to(assets_root)
|
|
127
|
+
except ValueError as exc:
|
|
128
|
+
raise HTTPException(404) from exc
|
|
129
|
+
if not file_path.is_file():
|
|
130
|
+
raise HTTPException(404)
|
|
131
|
+
return FileResponse(file_path)
|
|
132
|
+
|
|
133
|
+
@app.get("/{full_path:path}", include_in_schema=False)
|
|
134
|
+
def dashboard_spa_fallback(full_path: str):
|
|
135
|
+
request_path = (_d / full_path).resolve()
|
|
136
|
+
try:
|
|
137
|
+
request_path.relative_to(_d.resolve())
|
|
138
|
+
except ValueError:
|
|
139
|
+
return FileResponse(index_file)
|
|
140
|
+
if full_path and request_path.is_file():
|
|
141
|
+
return FileResponse(request_path)
|
|
142
|
+
return FileResponse(index_file)
|
|
143
|
+
|
|
144
|
+
return app
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Module-level app for uvicorn (skillreg.server:app)
|
|
148
|
+
app = create_app()
|