clawkittool 0.1.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/README.md +52 -0
- package/bin/clawkittool.js +79 -0
- package/lib/get.js +234 -0
- package/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# clawkittool
|
|
2
|
+
|
|
3
|
+
按需下载安装 OpenClaw workflow kit 的轻量安装器。
|
|
4
|
+
|
|
5
|
+
## 使用方式
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
clawkittool get product-kit
|
|
9
|
+
clawkittool get hotnews-kit --dir ./my-clawkit
|
|
10
|
+
clawkittool get product-kit --manifest https://example.com/clawkit/manifest.json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## manifest 格式
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"version": 1,
|
|
18
|
+
"core": {
|
|
19
|
+
"url": "https://example.com/core.zip"
|
|
20
|
+
},
|
|
21
|
+
"kits": {
|
|
22
|
+
"product-kit": {
|
|
23
|
+
"url": "https://example.com/product-kit.zip"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## GitHub Release 用法
|
|
30
|
+
|
|
31
|
+
如果你把 `core.zip`、`product-kit.zip`、`hotnews-kit.zip` 上传到 GitHub Release,可以让构建脚本直接生成对应的 manifest:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
CLAWKIT_GITHUB_REPO=hanson/openclawstudy \
|
|
35
|
+
CLAWKIT_GITHUB_TAG=v0.1.0 \
|
|
36
|
+
npm run build:distribution
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
这时 `manifest.json` 里的地址会变成:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"core": {
|
|
44
|
+
"url": "https://github.com/hanson/openclawstudy/releases/download/v0.1.0/core.zip"
|
|
45
|
+
},
|
|
46
|
+
"kits": {
|
|
47
|
+
"product-kit": {
|
|
48
|
+
"url": "https://github.com/hanson/openclawstudy/releases/download/v0.1.0/product-kit.zip"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { installKitFromManifest } = require('../lib/get');
|
|
5
|
+
|
|
6
|
+
function printUsage() {
|
|
7
|
+
console.log(`Usage:
|
|
8
|
+
clawkittool get <kit-name> [--dir <path>] [--manifest <url-or-path>] [--force] [--skip-install]
|
|
9
|
+
`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function main(argv) {
|
|
13
|
+
const [command, maybeKitName, ...rest] = argv;
|
|
14
|
+
|
|
15
|
+
if (!command || command === '--help' || command === '-h') {
|
|
16
|
+
printUsage();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (command !== 'get') {
|
|
21
|
+
throw new Error(`Unknown command: ${command}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!maybeKitName) {
|
|
25
|
+
throw new Error('Missing kit name for get command');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let targetDir = path.resolve(process.cwd(), maybeKitName);
|
|
29
|
+
let manifestSource = process.env.CLAWKIT_MANIFEST_URL || null;
|
|
30
|
+
let force = false;
|
|
31
|
+
let skipInstall = false;
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
34
|
+
const arg = rest[index];
|
|
35
|
+
|
|
36
|
+
if (arg === '--dir') {
|
|
37
|
+
targetDir = path.resolve(process.cwd(), rest[index + 1] || '');
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (arg === '--manifest') {
|
|
43
|
+
manifestSource = rest[index + 1] || null;
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (arg === '--force') {
|
|
49
|
+
force = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (arg === '--skip-install') {
|
|
54
|
+
skipInstall = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!manifestSource) {
|
|
62
|
+
throw new Error('Missing manifest source. Use --manifest or set CLAWKIT_MANIFEST_URL');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await installKitFromManifest({
|
|
66
|
+
kitName: maybeKitName,
|
|
67
|
+
targetDir,
|
|
68
|
+
manifestSource,
|
|
69
|
+
force,
|
|
70
|
+
skipInstall,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log(`Installed ${result.kitName} into ${result.targetDir}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
77
|
+
console.error(`clawkittool failed: ${error.message}`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
});
|
package/lib/get.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
function ensureDir(dirPath) {
|
|
7
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function makeTempDir() {
|
|
11
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'clawkittool-'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isUrl(value) {
|
|
15
|
+
return /^https?:\/\//.test(value) || /^file:\/\//.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readText(source) {
|
|
19
|
+
if (/^file:\/\//.test(source)) {
|
|
20
|
+
return fs.readFileSync(new URL(source), 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (/^https?:\/\//.test(source)) {
|
|
24
|
+
const response = await fetch(source);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch ${source}: ${response.status} ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
return await response.text();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return fs.readFileSync(path.resolve(source), 'utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadManifest(source) {
|
|
35
|
+
const raw = await readText(source);
|
|
36
|
+
const manifest = JSON.parse(raw);
|
|
37
|
+
|
|
38
|
+
if (!manifest.core || !manifest.core.url) {
|
|
39
|
+
throw new Error('Manifest is missing core.url');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!manifest.kits || typeof manifest.kits !== 'object') {
|
|
43
|
+
throw new Error('Manifest is missing kits');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return manifest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function downloadFile(source, targetPath) {
|
|
50
|
+
ensureDir(path.dirname(targetPath));
|
|
51
|
+
|
|
52
|
+
if (/^file:\/\//.test(source)) {
|
|
53
|
+
fs.copyFileSync(new URL(source), targetPath);
|
|
54
|
+
return targetPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (/^https?:\/\//.test(source)) {
|
|
58
|
+
const response = await fetch(source);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Failed to download ${source}: ${response.status} ${response.statusText}`);
|
|
61
|
+
}
|
|
62
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
63
|
+
fs.writeFileSync(targetPath, Buffer.from(arrayBuffer));
|
|
64
|
+
return targetPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.copyFileSync(path.resolve(source), targetPath);
|
|
68
|
+
return targetPath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractZip(zipPath, targetDir) {
|
|
72
|
+
ensureDir(targetDir);
|
|
73
|
+
|
|
74
|
+
if (process.platform === 'win32') {
|
|
75
|
+
const result = spawnSync(
|
|
76
|
+
'powershell.exe',
|
|
77
|
+
[
|
|
78
|
+
'-NoProfile',
|
|
79
|
+
'-Command',
|
|
80
|
+
`Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${targetDir.replace(/'/g, "''")}' -Force`,
|
|
81
|
+
],
|
|
82
|
+
{ encoding: 'utf8' },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (result.status !== 0) {
|
|
86
|
+
throw new Error(`Failed to extract zip archive: ${result.stderr || result.stdout}`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = spawnSync('unzip', ['-qo', zipPath, '-d', targetDir], {
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (result.status !== 0) {
|
|
96
|
+
throw new Error(`Failed to extract zip archive: ${result.stderr || result.stdout}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function listMeaningfulEntries(dirPath) {
|
|
101
|
+
return fs
|
|
102
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
103
|
+
.filter((entry) => entry.name !== '__MACOSX' && entry.name !== '.DS_Store');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveArchiveRoot(extractDir) {
|
|
107
|
+
const entries = listMeaningfulEntries(extractDir);
|
|
108
|
+
if (entries.length === 1 && entries[0].isDirectory()) {
|
|
109
|
+
return path.join(extractDir, entries[0].name);
|
|
110
|
+
}
|
|
111
|
+
return extractDir;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function copyDirectoryContents(sourceDir, targetDir, force) {
|
|
115
|
+
ensureDir(targetDir);
|
|
116
|
+
|
|
117
|
+
for (const entry of listMeaningfulEntries(sourceDir)) {
|
|
118
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
119
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
120
|
+
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
123
|
+
throw new Error(`Target directory already exists: ${targetPath}`);
|
|
124
|
+
}
|
|
125
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
126
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
131
|
+
throw new Error(`Target file already exists: ${targetPath}`);
|
|
132
|
+
}
|
|
133
|
+
ensureDir(path.dirname(targetPath));
|
|
134
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function installCoreArchive(extractDir, targetDir, force) {
|
|
139
|
+
const archiveRoot = resolveArchiveRoot(extractDir);
|
|
140
|
+
copyDirectoryContents(archiveRoot, targetDir, force);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveKitSourceDir(extractDir, kitName) {
|
|
144
|
+
const archiveRoot = resolveArchiveRoot(extractDir);
|
|
145
|
+
const directKitDir = path.join(archiveRoot, kitName);
|
|
146
|
+
if (fs.existsSync(path.join(directKitDir, 'kit.json'))) {
|
|
147
|
+
return directKitDir;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const nestedKitDir = path.join(archiveRoot, 'kits', kitName);
|
|
151
|
+
if (fs.existsSync(path.join(nestedKitDir, 'kit.json'))) {
|
|
152
|
+
return nestedKitDir;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(path.join(archiveRoot, 'kit.json'))) {
|
|
156
|
+
return archiveRoot;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new Error(`Unable to locate kit "${kitName}" inside archive`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function installKitArchive(extractDir, targetDir, kitName, force) {
|
|
163
|
+
const kitSourceDir = resolveKitSourceDir(extractDir, kitName);
|
|
164
|
+
const targetKitDir = path.join(targetDir, 'kits', kitName);
|
|
165
|
+
|
|
166
|
+
if (fs.existsSync(targetKitDir) && !force) {
|
|
167
|
+
throw new Error(`Target kit directory already exists: ${targetKitDir}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fs.rmSync(targetKitDir, { recursive: true, force: true });
|
|
171
|
+
ensureDir(path.dirname(targetKitDir));
|
|
172
|
+
fs.cpSync(kitSourceDir, targetKitDir, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runInstall(targetDir) {
|
|
176
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
177
|
+
const result = spawnSync(npmCommand, ['install'], {
|
|
178
|
+
cwd: targetDir,
|
|
179
|
+
stdio: 'inherit',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (result.status !== 0) {
|
|
183
|
+
throw new Error(`npm install failed in ${targetDir}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function installKitFromManifest({
|
|
188
|
+
kitName,
|
|
189
|
+
targetDir,
|
|
190
|
+
manifestSource,
|
|
191
|
+
force = false,
|
|
192
|
+
skipInstall = false,
|
|
193
|
+
}) {
|
|
194
|
+
const manifest = await loadManifest(manifestSource);
|
|
195
|
+
const kitEntry = manifest.kits[kitName];
|
|
196
|
+
|
|
197
|
+
if (!kitEntry || !kitEntry.url) {
|
|
198
|
+
throw new Error(`Kit "${kitName}" not found in manifest`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ensureDir(targetDir);
|
|
202
|
+
|
|
203
|
+
const tempDir = makeTempDir();
|
|
204
|
+
const coreZipPath = path.join(tempDir, 'core.zip');
|
|
205
|
+
const coreExtractDir = path.join(tempDir, 'core');
|
|
206
|
+
const kitZipPath = path.join(tempDir, `${kitName}.zip`);
|
|
207
|
+
const kitExtractDir = path.join(tempDir, kitName);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await downloadFile(manifest.core.url, coreZipPath);
|
|
211
|
+
extractZip(coreZipPath, coreExtractDir);
|
|
212
|
+
installCoreArchive(coreExtractDir, targetDir, force);
|
|
213
|
+
|
|
214
|
+
await downloadFile(kitEntry.url, kitZipPath);
|
|
215
|
+
extractZip(kitZipPath, kitExtractDir);
|
|
216
|
+
installKitArchive(kitExtractDir, targetDir, kitName, force);
|
|
217
|
+
|
|
218
|
+
if (!skipInstall) {
|
|
219
|
+
runInstall(targetDir);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
kitName,
|
|
224
|
+
targetDir,
|
|
225
|
+
};
|
|
226
|
+
} finally {
|
|
227
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
installKitFromManifest,
|
|
233
|
+
loadManifest,
|
|
234
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawkittool",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "On-demand installer for OpenClaw ClawKit workflow kits",
|
|
5
|
+
"bin": {
|
|
6
|
+
"clawkittool": "./bin/clawkittool.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"lib",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
}
|
|
17
|
+
}
|