tolingcode 1.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/QUICKSTART.md +196 -0
- package/README.md +106 -0
- package/bin/tolingcode.js +207 -0
- package/docs/SERVER.md +253 -0
- package/package.json +32 -0
- package/scripts/publish.bat +30 -0
- package/server/registry.js +220 -0
package/QUICKSTART.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# TolingCode 快速开始指南
|
|
2
|
+
|
|
3
|
+
## 🎯 目标
|
|
4
|
+
|
|
5
|
+
创建一个类似 `clawhub` 的 CLI 工具,通过 `toling.me` 托管和分发 skills 和 apps。
|
|
6
|
+
|
|
7
|
+
## 📦 使用流程
|
|
8
|
+
|
|
9
|
+
### 1. 用户安装 CLI
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g tolingcode@latest
|
|
13
|
+
# 或指定版本
|
|
14
|
+
npm install -g tolingcode@2026.03.06
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 2. 用户安装 Skill
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
tolingcode install skills weather
|
|
21
|
+
tolingcode install skills weather -v 2026.03.06
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 3. 开发者发布 Skill
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 方法 1: 使用 publish 命令(需要 API 支持)
|
|
28
|
+
tolingcode publish ./my-skill --type skills --name my-skill --version 2026.03.06
|
|
29
|
+
|
|
30
|
+
# 方法 2: 手动上传
|
|
31
|
+
tar -czf my-skill-2026.03.06.tar.gz -C ./my-skill .
|
|
32
|
+
scp my-skill-2026.03.06.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
|
|
33
|
+
# 然后更新 registry.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 🚀 部署步骤
|
|
37
|
+
|
|
38
|
+
### 第一步:准备 toling.me 服务器
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# SSH 登录服务器
|
|
42
|
+
ssh user@toling.me
|
|
43
|
+
|
|
44
|
+
# 创建目录
|
|
45
|
+
sudo mkdir -p /var/www/toling.me/{packages/skills,packages/apps,api}
|
|
46
|
+
sudo chown -R $USER:$USER /var/www/toling.me
|
|
47
|
+
|
|
48
|
+
# 安装 Node.js (如果没有)
|
|
49
|
+
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
|
50
|
+
sudo apt-get install -y nodejs
|
|
51
|
+
|
|
52
|
+
# 安装依赖
|
|
53
|
+
cd /var/www/toling.me/api
|
|
54
|
+
npm init -y
|
|
55
|
+
npm install express multer
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 第二步:部署服务端代码
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 上传 registry.js 到服务器
|
|
62
|
+
scp registry.js user@toling.me:/var/www/toling.me/api/
|
|
63
|
+
|
|
64
|
+
# 创建 systemd 服务
|
|
65
|
+
sudo tee /etc/systemd/system/tolingcode-registry.service > /dev/null <<EOF
|
|
66
|
+
[Unit]
|
|
67
|
+
Description=TolingCode Registry Server
|
|
68
|
+
After=network.target
|
|
69
|
+
|
|
70
|
+
[Service]
|
|
71
|
+
Type=simple
|
|
72
|
+
User=www-data
|
|
73
|
+
WorkingDirectory=/var/www/toling.me/api
|
|
74
|
+
ExecStart=/usr/bin/node registry.js
|
|
75
|
+
Restart=on-failure
|
|
76
|
+
|
|
77
|
+
[Install]
|
|
78
|
+
WantedBy=multi-user.target
|
|
79
|
+
EOF
|
|
80
|
+
|
|
81
|
+
# 启动服务
|
|
82
|
+
sudo systemctl daemon-reload
|
|
83
|
+
sudo systemctl enable tolingcode-registry
|
|
84
|
+
sudo systemctl start tolingcode-registry
|
|
85
|
+
sudo systemctl status tolingcode-registry
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 第三步:配置 Nginx
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 编辑 Nginx 配置
|
|
92
|
+
sudo nano /etc/nginx/sites-available/toling.me
|
|
93
|
+
|
|
94
|
+
# 添加以下内容:
|
|
95
|
+
server {
|
|
96
|
+
listen 443 ssl;
|
|
97
|
+
server_name toling.me;
|
|
98
|
+
|
|
99
|
+
ssl_certificate /etc/letsencrypt/live/toling.me/fullchain.pem;
|
|
100
|
+
ssl_certificate_key /etc/letsencrypt/live/toling.me/privkey.pem;
|
|
101
|
+
|
|
102
|
+
root /var/www/toling.me;
|
|
103
|
+
|
|
104
|
+
# API
|
|
105
|
+
location /api/ {
|
|
106
|
+
proxy_pass http://localhost:3000;
|
|
107
|
+
proxy_set_header Host $host;
|
|
108
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
109
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Packages
|
|
113
|
+
location /packages/ {
|
|
114
|
+
alias /var/www/toling.me/packages/;
|
|
115
|
+
autoindex on;
|
|
116
|
+
add_header Content-Type application/octet-stream;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# 测试并重载
|
|
121
|
+
sudo nginx -t
|
|
122
|
+
sudo systemctl reload nginx
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 第四步:发布 CLI 到 npm
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# 在本地开发机器上
|
|
129
|
+
cd tolingcode
|
|
130
|
+
|
|
131
|
+
# 登录 npm
|
|
132
|
+
npm login
|
|
133
|
+
|
|
134
|
+
# 发布
|
|
135
|
+
npm version 1.0.0
|
|
136
|
+
npm publish
|
|
137
|
+
|
|
138
|
+
# 验证
|
|
139
|
+
npm view tolingcode
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 第五步:发布第一个 Skill
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# 创建测试 skill
|
|
146
|
+
mkdir -p test-skill
|
|
147
|
+
echo '{"name": "test", "description": "Test skill"}' > test-skill/SKILL.md
|
|
148
|
+
|
|
149
|
+
# 发布
|
|
150
|
+
cd tolingcode
|
|
151
|
+
node bin/tolingcode.js publish ../test-skill --type skills --name test-skill --version 2026.03.06
|
|
152
|
+
|
|
153
|
+
# 或者手动上传
|
|
154
|
+
tar -czf test-skill-2026.03.06.tar.gz -C ../test-skill .
|
|
155
|
+
scp test-skill-2026.03.06.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
|
|
156
|
+
|
|
157
|
+
# 更新 registry.json (SSH 到服务器编辑)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## ✅ 测试
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# 安装 CLI
|
|
164
|
+
npm install -g tolingcode@latest
|
|
165
|
+
|
|
166
|
+
# 列出可用技能
|
|
167
|
+
tolingcode list skills
|
|
168
|
+
|
|
169
|
+
# 安装技能
|
|
170
|
+
tolingcode install skills test-skill
|
|
171
|
+
|
|
172
|
+
# 验证安装
|
|
173
|
+
ls ~/.openclaw/workspace/skills/
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 📝 注意事项
|
|
177
|
+
|
|
178
|
+
1. **版本命名**: 使用 `YYYY.MM.DD` 格式,便于追踪发布日期
|
|
179
|
+
2. **认证**: 生产环境需要添加 API 认证(API Key 或 JWT)
|
|
180
|
+
3. **HTTPS**: 确保 toling.me 配置了 SSL 证书
|
|
181
|
+
4. **备份**: 定期备份 registry.json 和 packages 目录
|
|
182
|
+
5. **监控**: 添加日志和监控,跟踪下载量和错误
|
|
183
|
+
|
|
184
|
+
## 🔧 后续扩展
|
|
185
|
+
|
|
186
|
+
- [ ] 添加用户认证系统
|
|
187
|
+
- [ ] 支持私有包(需要 token)
|
|
188
|
+
- [ ] 添加下载统计
|
|
189
|
+
- [ ] Web 管理界面
|
|
190
|
+
- [ ] 自动版本递增
|
|
191
|
+
- [ ] 依赖管理
|
|
192
|
+
- [ ] 包签名验证
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
有问题?查看 `docs/SERVER.md` 获取详细服务端文档。
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# TolingCode CLI
|
|
2
|
+
|
|
3
|
+
通过 npm 安装和管理 TolingCode 的 skills 和 apps。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 安装最新版本
|
|
9
|
+
npm install -g tolingcode@latest
|
|
10
|
+
|
|
11
|
+
# 安装指定版本
|
|
12
|
+
npm install -g tolingcode@2026.02.26
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 使用
|
|
16
|
+
|
|
17
|
+
### 安装 Skill
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 安装最新版本
|
|
21
|
+
tolingcode install skills weather
|
|
22
|
+
|
|
23
|
+
# 安装指定版本
|
|
24
|
+
tolingcode install skills weather -v 2026.03.06
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 安装 App
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 安装到当前目录
|
|
31
|
+
tolingcode install apps myapp
|
|
32
|
+
|
|
33
|
+
# 全局安装
|
|
34
|
+
tolingcode install apps myapp -g
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 列出可用包
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 列出所有
|
|
41
|
+
tolingcode list
|
|
42
|
+
|
|
43
|
+
# 只列出 skills
|
|
44
|
+
tolingcode list skills
|
|
45
|
+
|
|
46
|
+
# 只列出 apps
|
|
47
|
+
tolingcode list apps
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 搜索
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
tolingcode search weather
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 发布 (开发者)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
tolingcode publish ./my-skill --type skills --name my-skill --version 2026.03.06
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 版本命名
|
|
63
|
+
|
|
64
|
+
推荐使用日期格式:`YYYY.MM.DD`
|
|
65
|
+
|
|
66
|
+
- `2026.02.26` - 2026 年 2 月 26 日发布的版本
|
|
67
|
+
- `latest` - 最新版本
|
|
68
|
+
|
|
69
|
+
## 环境变量
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 自定义 registry 地址(默认:https://toling.me/api/registry)
|
|
73
|
+
export TOLINGCODE_REGISTRY=https://registry.toling.me/api/registry
|
|
74
|
+
|
|
75
|
+
# 自定义安装路径
|
|
76
|
+
export OPENCLAW_WORKSPACE=/path/to/workspace
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 开发
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 本地测试
|
|
83
|
+
cd tolingcode
|
|
84
|
+
npm link
|
|
85
|
+
tolingcode --help
|
|
86
|
+
|
|
87
|
+
# 发布到 npm
|
|
88
|
+
npm version patch # 或 minor/major
|
|
89
|
+
npm publish
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 目录结构
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
tolingcode/
|
|
96
|
+
├── bin/
|
|
97
|
+
│ └── tolingcode.js # CLI 入口
|
|
98
|
+
├── docs/
|
|
99
|
+
│ └── SERVER.md # 服务端部署文档
|
|
100
|
+
├── package.json
|
|
101
|
+
└── README.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const tar = require('tar');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
const REGISTRY_URL = 'https://toling.me/api/registry';
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('tolingcode')
|
|
17
|
+
.description('TolingCode CLI - Install skills and apps from toling.me')
|
|
18
|
+
.version('1.0.0');
|
|
19
|
+
|
|
20
|
+
// install command
|
|
21
|
+
program
|
|
22
|
+
.command('install <type> <name>')
|
|
23
|
+
.description('Install a skill or app')
|
|
24
|
+
.option('-v, --version <version>', 'Specify version (e.g., 2026.02.26 or latest)')
|
|
25
|
+
.option('-g, --global', 'Install globally (for apps)')
|
|
26
|
+
.action(async (type, name, options) => {
|
|
27
|
+
const version = options.version || 'latest';
|
|
28
|
+
|
|
29
|
+
console.log(chalk.blue(`\n🔍 Searching for ${type}: ${name}@${version}...`));
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Fetch package info from registry
|
|
33
|
+
const spinner = ora('Fetching package info...').start();
|
|
34
|
+
const response = await axios.get(`${REGISTRY_URL}/${type}/${name}`, {
|
|
35
|
+
params: { version }
|
|
36
|
+
});
|
|
37
|
+
spinner.stop();
|
|
38
|
+
|
|
39
|
+
const pkg = response.data;
|
|
40
|
+
console.log(chalk.green(`✓ Found: ${pkg.name}@${pkg.version}`));
|
|
41
|
+
console.log(chalk.gray(` Description: ${pkg.description}`));
|
|
42
|
+
console.log(chalk.gray(` Download URL: ${pkg.downloadUrl}`));
|
|
43
|
+
|
|
44
|
+
// Determine install path
|
|
45
|
+
let installPath;
|
|
46
|
+
if (type === 'skills') {
|
|
47
|
+
const workspace = process.env.OPENCLAW_WORKSPACE || path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw', 'workspace');
|
|
48
|
+
installPath = path.join(workspace, 'skills');
|
|
49
|
+
} else if (type === 'apps') {
|
|
50
|
+
if (options.global) {
|
|
51
|
+
installPath = path.join(process.env.APPDATA || path.join(process.env.HOME || '', '.local'), 'tolingcode', 'apps');
|
|
52
|
+
} else {
|
|
53
|
+
installPath = path.join(process.cwd(), 'tolingcode-apps');
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
console.log(chalk.red(`✗ Unknown type: ${type}`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create directory if not exists
|
|
61
|
+
if (!fs.existsSync(installPath)) {
|
|
62
|
+
fs.mkdirSync(installPath, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Download and extract
|
|
66
|
+
const downloadSpinner = ora('Downloading...').start();
|
|
67
|
+
const tarballResponse = await axios.get(pkg.downloadUrl, {
|
|
68
|
+
responseType: 'stream'
|
|
69
|
+
});
|
|
70
|
+
downloadSpinner.stop();
|
|
71
|
+
console.log(chalk.green('✓ Downloaded'));
|
|
72
|
+
|
|
73
|
+
const extractSpinner = ora('Installing...').start();
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
tarballResponse.data
|
|
76
|
+
.pipe(tar.x({ C: installPath, strip: 1 }))
|
|
77
|
+
.on('end', () => {
|
|
78
|
+
extractSpinner.stop();
|
|
79
|
+
console.log(chalk.green(`✓ Installed to: ${installPath}`));
|
|
80
|
+
resolve();
|
|
81
|
+
})
|
|
82
|
+
.on('error', (err) => {
|
|
83
|
+
extractSpinner.stop();
|
|
84
|
+
reject(err);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
console.log(chalk.green(`\n🎉 ${name} installed successfully!`));
|
|
89
|
+
|
|
90
|
+
// Show next steps
|
|
91
|
+
if (type === 'skills') {
|
|
92
|
+
console.log(chalk.yellow('\n💡 Next steps:'));
|
|
93
|
+
console.log(' Restart OpenClaw or reload skills to use the new skill.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.log(chalk.red(`\n✗ Error: ${error.message}`));
|
|
98
|
+
if (error.response?.status === 404) {
|
|
99
|
+
console.log(chalk.yellow(' Package not found. Check the name and version.'));
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// list command - list available skills/apps
|
|
106
|
+
program
|
|
107
|
+
.command('list [type]')
|
|
108
|
+
.description('List available skills or apps')
|
|
109
|
+
.action(async (type) => {
|
|
110
|
+
console.log(chalk.blue('\n📦 Available packages:\n'));
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const targetType = type || 'all';
|
|
114
|
+
const response = await axios.get(`${REGISTRY_URL}/list`, {
|
|
115
|
+
params: { type: targetType }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const packages = response.data;
|
|
119
|
+
if (packages.length === 0) {
|
|
120
|
+
console.log(chalk.yellow(' No packages found.'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
packages.forEach(pkg => {
|
|
125
|
+
console.log(chalk.green(` ${pkg.type}/${pkg.name}`));
|
|
126
|
+
console.log(chalk.gray(` v${pkg.version} - ${pkg.description}`));
|
|
127
|
+
console.log(chalk.gray(` Latest: ${pkg.latestVersion}\n`));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log(chalk.red(`✗ Error: ${error.message}`));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// search command
|
|
137
|
+
program
|
|
138
|
+
.command('search <query>')
|
|
139
|
+
.description('Search for skills or apps')
|
|
140
|
+
.action(async (query) => {
|
|
141
|
+
console.log(chalk.blue(`\n🔍 Searching for: ${query}\n`));
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const response = await axios.get(`${REGISTRY_URL}/search`, {
|
|
145
|
+
params: { q: query }
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const results = response.data;
|
|
149
|
+
if (results.length === 0) {
|
|
150
|
+
console.log(chalk.yellow(' No results found.'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
results.forEach(pkg => {
|
|
155
|
+
console.log(chalk.green(` ${pkg.type}/${pkg.name}`));
|
|
156
|
+
console.log(chalk.gray(` v${pkg.version} - ${pkg.description}\n`));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.log(chalk.red(`✗ Error: ${error.message}`));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// publish command - for publishing skills/apps to registry
|
|
166
|
+
program
|
|
167
|
+
.command('publish <path>')
|
|
168
|
+
.description('Publish a skill or app to the registry')
|
|
169
|
+
.option('--type <type>', 'Package type (skills or apps)', 'skills')
|
|
170
|
+
.option('--name <name>', 'Package name')
|
|
171
|
+
.option('--version <version>', 'Package version')
|
|
172
|
+
.action(async (pkgPath, options) => {
|
|
173
|
+
const absPath = path.resolve(pkgPath);
|
|
174
|
+
|
|
175
|
+
if (!fs.existsSync(absPath)) {
|
|
176
|
+
console.log(chalk.red(`✗ Path not found: ${absPath}`));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(chalk.blue(`\n📤 Publishing ${options.type}: ${options.name || '(auto)'}...`));
|
|
181
|
+
|
|
182
|
+
// Read package.json if exists
|
|
183
|
+
let pkgInfo = {};
|
|
184
|
+
const pkgJsonPath = path.join(absPath, 'package.json');
|
|
185
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
186
|
+
pkgInfo = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const name = options.name || pkgInfo.name || path.basename(absPath);
|
|
190
|
+
const version = options.version || pkgInfo.version || new Date().toISOString().split('T')[0].replace(/-/g, '.');
|
|
191
|
+
|
|
192
|
+
console.log(chalk.gray(` Name: ${name}`));
|
|
193
|
+
console.log(chalk.gray(` Version: ${version}`));
|
|
194
|
+
console.log(chalk.gray(` Type: ${options.type}`));
|
|
195
|
+
|
|
196
|
+
// TODO: Implement actual publish logic
|
|
197
|
+
// This would:
|
|
198
|
+
// 1. Create tarball
|
|
199
|
+
// 2. Upload to toling.me server
|
|
200
|
+
// 3. Update registry index
|
|
201
|
+
|
|
202
|
+
console.log(chalk.yellow('\n⚠️ Publish endpoint not implemented yet.'));
|
|
203
|
+
console.log(chalk.gray(' You need to implement the server-side API on toling.me'));
|
|
204
|
+
console.log(chalk.gray(' See: docs/PUBLISH.md for details'));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
program.parse();
|
package/docs/SERVER.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# TolingCode Registry Server
|
|
2
|
+
|
|
3
|
+
这是在 `toling.me` 上需要部署的服务端代码,用于托管 skill 和 app 包。
|
|
4
|
+
|
|
5
|
+
## 目录结构
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/var/www/toling.me/
|
|
9
|
+
├── api/
|
|
10
|
+
│ └── registry/ # Registry API
|
|
11
|
+
│ ├── index.php # 或 node.js 入口
|
|
12
|
+
│ └── packages/ # 包存储目录
|
|
13
|
+
│ ├── skills/
|
|
14
|
+
│ └── apps/
|
|
15
|
+
├── packages/ # 下载的 tarball 存储
|
|
16
|
+
│ ├── skills/
|
|
17
|
+
│ └── apps/
|
|
18
|
+
└── registry.json # 包索引(或用数据库)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API 端点
|
|
22
|
+
|
|
23
|
+
### 1. 获取包信息
|
|
24
|
+
```
|
|
25
|
+
GET /api/registry/{type}/{name}?version=latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
响应:
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"name": "weather",
|
|
32
|
+
"type": "skills",
|
|
33
|
+
"version": "2026.03.06",
|
|
34
|
+
"description": "Get weather via wttr.in",
|
|
35
|
+
"downloadUrl": "https://toling.me/packages/skills/weather-2026.03.06.tar.gz",
|
|
36
|
+
"latestVersion": "2026.03.06",
|
|
37
|
+
"versions": ["2026.03.06", "2026.03.01", "2026.02.26"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. 列出所有包
|
|
42
|
+
```
|
|
43
|
+
GET /api/registry/list?type=skills
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
响应:
|
|
47
|
+
```json
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
"name": "weather",
|
|
51
|
+
"type": "skills",
|
|
52
|
+
"version": "2026.03.06",
|
|
53
|
+
"description": "Get weather via wttr.in",
|
|
54
|
+
"latestVersion": "2026.03.06"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. 搜索
|
|
60
|
+
```
|
|
61
|
+
GET /api/registry/search?q=weather
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. 下载 tarball
|
|
65
|
+
```
|
|
66
|
+
GET /packages/skills/weather-2026.03.06.tar.gz
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 简单实现 (Node.js + Express)
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// api/registry/server.js
|
|
73
|
+
const express = require('express');
|
|
74
|
+
const fs = require('fs');
|
|
75
|
+
const path = require('path');
|
|
76
|
+
const app = express();
|
|
77
|
+
|
|
78
|
+
const REGISTRY_FILE = '/var/www/toling.me/registry.json';
|
|
79
|
+
const PACKAGES_DIR = '/var/www/toling.me/packages';
|
|
80
|
+
|
|
81
|
+
// 读取注册表
|
|
82
|
+
function getRegistry() {
|
|
83
|
+
if (fs.existsSync(REGISTRY_FILE)) {
|
|
84
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
|
|
85
|
+
}
|
|
86
|
+
return { skills: {}, apps: {} };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 获取包信息
|
|
90
|
+
app.get('/api/registry/:type/:name', (req, res) => {
|
|
91
|
+
const { type, name } = req.params;
|
|
92
|
+
const { version = 'latest' } = req.query;
|
|
93
|
+
|
|
94
|
+
const registry = getRegistry();
|
|
95
|
+
const pkg = registry[type]?.[name];
|
|
96
|
+
|
|
97
|
+
if (!pkg) {
|
|
98
|
+
return res.status(404).json({ error: 'Package not found' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ver = version === 'latest' ? pkg.latestVersion : version;
|
|
102
|
+
const verInfo = pkg.versions?.[ver];
|
|
103
|
+
|
|
104
|
+
if (!verInfo) {
|
|
105
|
+
return res.status(404).json({ error: 'Version not found' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.json({
|
|
109
|
+
name,
|
|
110
|
+
type,
|
|
111
|
+
version: ver,
|
|
112
|
+
description: pkg.description,
|
|
113
|
+
downloadUrl: `https://toling.me/packages/${type}/${name}-${ver}.tar.gz`,
|
|
114
|
+
latestVersion: pkg.latestVersion,
|
|
115
|
+
versions: Object.keys(pkg.versions || {})
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 列出包
|
|
120
|
+
app.get('/api/registry/list', (req, res) => {
|
|
121
|
+
const { type = 'all' } = req.query;
|
|
122
|
+
const registry = getRegistry();
|
|
123
|
+
const result = [];
|
|
124
|
+
|
|
125
|
+
const types = type === 'all' ? ['skills', 'apps'] : [type];
|
|
126
|
+
|
|
127
|
+
types.forEach(t => {
|
|
128
|
+
if (registry[t]) {
|
|
129
|
+
Object.entries(registry[t]).forEach(([name, pkg]) => {
|
|
130
|
+
result.push({
|
|
131
|
+
name,
|
|
132
|
+
type: t,
|
|
133
|
+
version: pkg.latestVersion,
|
|
134
|
+
description: pkg.description,
|
|
135
|
+
latestVersion: pkg.latestVersion
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
res.json(result);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 搜索
|
|
145
|
+
app.get('/api/registry/search', (req, res) => {
|
|
146
|
+
const { q } = req.query;
|
|
147
|
+
const registry = getRegistry();
|
|
148
|
+
const result = [];
|
|
149
|
+
|
|
150
|
+
['skills', 'apps'].forEach(type => {
|
|
151
|
+
if (registry[type]) {
|
|
152
|
+
Object.entries(registry[type]).forEach(([name, pkg]) => {
|
|
153
|
+
if (name.toLowerCase().includes(q.toLowerCase()) ||
|
|
154
|
+
pkg.description?.toLowerCase().includes(q.toLowerCase())) {
|
|
155
|
+
result.push({
|
|
156
|
+
name,
|
|
157
|
+
type,
|
|
158
|
+
version: pkg.latestVersion,
|
|
159
|
+
description: pkg.description
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
res.json(result);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// 提供静态文件(tarball 下载)
|
|
170
|
+
app.use('/packages', express.static(PACKAGES_DIR));
|
|
171
|
+
|
|
172
|
+
app.listen(3000, () => {
|
|
173
|
+
console.log('TolingCode Registry running on port 3000');
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 注册表格式 (registry.json)
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"skills": {
|
|
182
|
+
"weather": {
|
|
183
|
+
"description": "Get weather via wttr.in",
|
|
184
|
+
"latestVersion": "2026.03.06",
|
|
185
|
+
"versions": {
|
|
186
|
+
"2026.03.06": {
|
|
187
|
+
"publishedAt": "2026-03-06T10:00:00Z",
|
|
188
|
+
"hash": "sha256:abc123..."
|
|
189
|
+
},
|
|
190
|
+
"2026.03.01": {
|
|
191
|
+
"publishedAt": "2026-03-01T10:00:00Z",
|
|
192
|
+
"hash": "sha256:def456..."
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"apps": {}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 发布脚本示例
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
#!/bin/bash
|
|
205
|
+
# publish.sh
|
|
206
|
+
|
|
207
|
+
SKILL_PATH=$1
|
|
208
|
+
NAME=$2
|
|
209
|
+
VERSION=${3:-$(date +%Y.%m.%d)}
|
|
210
|
+
|
|
211
|
+
# 创建 tarball
|
|
212
|
+
tar -czf ${NAME}-${VERSION}.tar.gz -C $(dirname $SKILL_PATH) $(basename $SKILL_PATH)
|
|
213
|
+
|
|
214
|
+
# 上传到服务器
|
|
215
|
+
scp ${NAME}-${VERSION}.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
|
|
216
|
+
|
|
217
|
+
# 更新 registry.json (需要 API 或 SSH)
|
|
218
|
+
# 或者通过 API 发布
|
|
219
|
+
curl -X POST https://toling.me/api/registry/publish \
|
|
220
|
+
-F "type=skills" \
|
|
221
|
+
-F "name=${NAME}" \
|
|
222
|
+
-F "version=${VERSION}" \
|
|
223
|
+
-F "package=@${NAME}-${VERSION}.tar.gz"
|
|
224
|
+
|
|
225
|
+
echo "Published ${NAME}@${VERSION}"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Nginx 配置
|
|
229
|
+
|
|
230
|
+
```nginx
|
|
231
|
+
server {
|
|
232
|
+
listen 443 ssl;
|
|
233
|
+
server_name toling.me;
|
|
234
|
+
|
|
235
|
+
ssl_certificate /etc/letsencrypt/live/toling.me/fullchain.pem;
|
|
236
|
+
ssl_certificate_key /etc/letsencrypt/live/toling.me/privkey.pem;
|
|
237
|
+
|
|
238
|
+
root /var/www/toling.me;
|
|
239
|
+
|
|
240
|
+
# API
|
|
241
|
+
location /api/ {
|
|
242
|
+
proxy_pass http://localhost:3000;
|
|
243
|
+
proxy_set_header Host $host;
|
|
244
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Packages
|
|
248
|
+
location /packages/ {
|
|
249
|
+
alias /var/www/toling.me/packages/;
|
|
250
|
+
autoindex on;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tolingcode",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TolingCode CLI - Install skills and apps from toling.me",
|
|
5
|
+
"main": "bin/tolingcode.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tolingcode": "bin/tolingcode.js",
|
|
8
|
+
"tlc": "bin/tolingcode.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"publish": "npm publish"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"tolingcode",
|
|
16
|
+
"openclaw",
|
|
17
|
+
"skills",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"author": "TolingCode",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^11.0.0",
|
|
24
|
+
"chalk": "^4.1.2",
|
|
25
|
+
"ora": "^5.4.1",
|
|
26
|
+
"axios": "^1.6.0",
|
|
27
|
+
"tar": "^6.2.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=16.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM TolingCode Publish Script (Windows)
|
|
3
|
+
REM Usage: publish.bat <skill-path> <name> [version]
|
|
4
|
+
|
|
5
|
+
SET SKILL_PATH=%1
|
|
6
|
+
SET NAME=%2
|
|
7
|
+
SET VERSION=%3
|
|
8
|
+
|
|
9
|
+
IF "%VERSION%"=="" (
|
|
10
|
+
REM Use today's date as version
|
|
11
|
+
FOR /F "tokens=2-4 delims=/ " %%A IN ('date /T') DO (SET VERSION=%%C.%%B.%%A)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
ECHO Publishing %NAME%@%VERSION%...
|
|
15
|
+
|
|
16
|
+
REM Create tarball (requires tar.exe on Windows 10+)
|
|
17
|
+
tar -czf %NAME%-%VERSION%.tar.gz -C %SKILL_PATH% .
|
|
18
|
+
|
|
19
|
+
ECHO Created: %NAME%-%VERSION%.tar.gz
|
|
20
|
+
ECHO.
|
|
21
|
+
ECHO Next steps:
|
|
22
|
+
ECHO 1. Upload to toling.me: scp %NAME%-%VERSION%.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
|
|
23
|
+
ECHO 2. Update registry.json on server
|
|
24
|
+
ECHO.
|
|
25
|
+
ECHO Or use the API (when implemented):
|
|
26
|
+
ECHO curl -X POST https://toling.me/api/registry/publish ^
|
|
27
|
+
ECHO -F "type=skills" ^
|
|
28
|
+
ECHO -F "name=%NAME%" ^
|
|
29
|
+
ECHO -F "version=%VERSION%" ^
|
|
30
|
+
ECHO -F "package=@%NAME%-%VERSION%.tar.gz"
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// TolingCode Registry Server - Simple Node.js Implementation
|
|
2
|
+
// Deploy this on toling.me
|
|
3
|
+
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const multer = require('multer');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
const app = express();
|
|
11
|
+
const PORT = process.env.PORT || 3000;
|
|
12
|
+
|
|
13
|
+
// Paths
|
|
14
|
+
const BASE_DIR = process.env.REGISTRY_BASE_DIR || '/var/www/toling.me';
|
|
15
|
+
const REGISTRY_FILE = path.join(BASE_DIR, 'registry.json');
|
|
16
|
+
const PACKAGES_DIR = path.join(BASE_DIR, 'packages');
|
|
17
|
+
|
|
18
|
+
// Ensure directories exist
|
|
19
|
+
if (!fs.existsSync(PACKAGES_DIR)) {
|
|
20
|
+
fs.mkdirSync(path.join(PACKAGES_DIR, 'skills'), { recursive: true });
|
|
21
|
+
fs.mkdirSync(path.join(PACKAGES_DIR, 'apps'), { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Initialize registry if not exists
|
|
25
|
+
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
26
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify({ skills: {}, apps: {} }, null, 2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Middleware
|
|
30
|
+
app.use(express.json());
|
|
31
|
+
const upload = multer({ dest: path.join(BASE_DIR, 'uploads') });
|
|
32
|
+
|
|
33
|
+
// Helper: Read registry
|
|
34
|
+
function getRegistry() {
|
|
35
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper: Write registry
|
|
39
|
+
function saveRegistry(data) {
|
|
40
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper: Calculate file hash
|
|
44
|
+
function hashFile(filePath) {
|
|
45
|
+
const content = fs.readFileSync(filePath);
|
|
46
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// GET /api/registry/:type/:name - Get package info
|
|
50
|
+
app.get('/api/registry/:type/:name', (req, res) => {
|
|
51
|
+
const { type, name } = req.params;
|
|
52
|
+
const { version = 'latest' } = req.query;
|
|
53
|
+
|
|
54
|
+
if (!['skills', 'apps'].includes(type)) {
|
|
55
|
+
return res.status(400).json({ error: 'Invalid type. Use "skills" or "apps"' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const registry = getRegistry();
|
|
59
|
+
const pkg = registry[type]?.[name];
|
|
60
|
+
|
|
61
|
+
if (!pkg) {
|
|
62
|
+
return res.status(404).json({ error: `Package ${name} not found` });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ver = version === 'latest' ? pkg.latestVersion : version;
|
|
66
|
+
const verInfo = pkg.versions?.[ver];
|
|
67
|
+
|
|
68
|
+
if (!verInfo) {
|
|
69
|
+
return res.status(404).json({ error: `Version ${ver} not found` });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
res.json({
|
|
73
|
+
name,
|
|
74
|
+
type,
|
|
75
|
+
version: ver,
|
|
76
|
+
description: pkg.description || '',
|
|
77
|
+
downloadUrl: `https://toling.me/packages/${type}/${name}-${ver}.tar.gz`,
|
|
78
|
+
latestVersion: pkg.latestVersion,
|
|
79
|
+
versions: Object.keys(pkg.versions || {}).sort().reverse()
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// GET /api/registry/list - List all packages
|
|
84
|
+
app.get('/api/registry/list', (req, res) => {
|
|
85
|
+
const { type = 'all' } = req.query;
|
|
86
|
+
const registry = getRegistry();
|
|
87
|
+
const result = [];
|
|
88
|
+
|
|
89
|
+
const types = type === 'all' ? ['skills', 'apps'] : [type];
|
|
90
|
+
|
|
91
|
+
types.forEach(t => {
|
|
92
|
+
if (registry[t]) {
|
|
93
|
+
Object.entries(registry[t]).forEach(([name, pkg]) => {
|
|
94
|
+
result.push({
|
|
95
|
+
name,
|
|
96
|
+
type: t,
|
|
97
|
+
version: pkg.latestVersion,
|
|
98
|
+
description: pkg.description || '',
|
|
99
|
+
latestVersion: pkg.latestVersion
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
res.json(result);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// GET /api/registry/search - Search packages
|
|
109
|
+
app.get('/api/registry/search', (req, res) => {
|
|
110
|
+
const { q } = req.query;
|
|
111
|
+
if (!q) {
|
|
112
|
+
return res.status(400).json({ error: 'Query parameter "q" is required' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const registry = getRegistry();
|
|
116
|
+
const result = [];
|
|
117
|
+
const query = q.toLowerCase();
|
|
118
|
+
|
|
119
|
+
['skills', 'apps'].forEach(type => {
|
|
120
|
+
if (registry[type]) {
|
|
121
|
+
Object.entries(registry[type]).forEach(([name, pkg]) => {
|
|
122
|
+
if (name.toLowerCase().includes(query) ||
|
|
123
|
+
(pkg.description && pkg.description.toLowerCase().includes(query))) {
|
|
124
|
+
result.push({
|
|
125
|
+
name,
|
|
126
|
+
type,
|
|
127
|
+
version: pkg.latestVersion,
|
|
128
|
+
description: pkg.description || ''
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
res.json(result);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// POST /api/registry/publish - Publish a package (requires auth)
|
|
139
|
+
app.post('/api/registry/publish', upload.single('package'), (req, res) => {
|
|
140
|
+
// TODO: Add authentication (API key, JWT, etc.)
|
|
141
|
+
const { type, name, version } = req.body;
|
|
142
|
+
|
|
143
|
+
if (!type || !name || !version) {
|
|
144
|
+
return res.status(400).json({ error: 'Missing required fields: type, name, version' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!['skills', 'apps'].includes(type)) {
|
|
148
|
+
return res.status(400).json({ error: 'Invalid type. Use "skills" or "apps"' });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!req.file) {
|
|
152
|
+
return res.status(400).json({ error: 'No package file uploaded' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const registry = getRegistry();
|
|
156
|
+
|
|
157
|
+
// Initialize type if not exists
|
|
158
|
+
if (!registry[type]) {
|
|
159
|
+
registry[type] = {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Initialize package if not exists
|
|
163
|
+
if (!registry[type][name]) {
|
|
164
|
+
registry[type][name] = {
|
|
165
|
+
description: req.body.description || '',
|
|
166
|
+
latestVersion: version,
|
|
167
|
+
versions: {}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add version
|
|
172
|
+
const fileHash = hashFile(req.file.path);
|
|
173
|
+
registry[type][name].versions[version] = {
|
|
174
|
+
publishedAt: new Date().toISOString(),
|
|
175
|
+
hash: `sha256:${fileHash}`,
|
|
176
|
+
size: req.file.size
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Update latest version
|
|
180
|
+
registry[type][name].latestVersion = version;
|
|
181
|
+
|
|
182
|
+
// Move file to packages directory
|
|
183
|
+
const destDir = path.join(PACKAGES_DIR, type);
|
|
184
|
+
const destFile = path.join(destDir, `${name}-${version}.tar.gz`);
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(destDir)) {
|
|
187
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fs.renameSync(req.file.path, destFile);
|
|
191
|
+
|
|
192
|
+
// Save registry
|
|
193
|
+
saveRegistry(registry);
|
|
194
|
+
|
|
195
|
+
// Cleanup uploads dir
|
|
196
|
+
try {
|
|
197
|
+
fs.unlinkSync(req.file.path);
|
|
198
|
+
} catch (e) {}
|
|
199
|
+
|
|
200
|
+
res.json({
|
|
201
|
+
success: true,
|
|
202
|
+
name,
|
|
203
|
+
type,
|
|
204
|
+
version,
|
|
205
|
+
downloadUrl: `https://toling.me/packages/${type}/${name}-${version}.tar.gz`
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Serve static packages
|
|
210
|
+
app.use('/packages', express.static(path.join(BASE_DIR, 'packages')));
|
|
211
|
+
|
|
212
|
+
// Health check
|
|
213
|
+
app.get('/health', (req, res) => {
|
|
214
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.listen(PORT, () => {
|
|
218
|
+
console.log(`TolingCode Registry Server running on port ${PORT}`);
|
|
219
|
+
console.log(`Base directory: ${BASE_DIR}`);
|
|
220
|
+
});
|