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 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
+ }