openclawmp 0.1.4 → 0.1.5

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.
@@ -2,343 +2,509 @@
2
2
  // commands/publish.js — Publish a local asset directory to the marketplace
3
3
  // ============================================================================
4
4
 
5
- 'use strict';
5
+ "use strict";
6
6
 
7
- const fs = require('fs');
8
- const path = require('path');
9
- const api = require('../api.js');
10
- const { createTarGzFromDirectory } = require('../archive.js');
11
- const config = require('../config.js');
12
- const { fish, info, ok, warn, err, c, detail } = require('../ui.js');
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const api = require("../api.js");
10
+ const { createTarGzFromDirectory } = require("../archive.js");
11
+ const config = require("../config.js");
12
+ const { fish, info, ok, warn, err, c, detail } = require("../ui.js");
13
13
 
14
14
  /**
15
15
  * Parse SKILL.md frontmatter (YAML-like key: value pairs between --- markers)
16
16
  */
17
17
  function parseFrontmatter(content) {
18
- const fm = {};
19
- let body = content;
20
-
21
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)/);
22
- if (match) {
23
- const fmText = match[1];
24
- body = match[2].trim();
25
-
26
- for (const line of fmText.split('\n')) {
27
- const trimmed = line.trim();
28
- if (!trimmed || trimmed.startsWith('#')) continue;
29
- const kv = trimmed.match(/^([\w-]+)\s*:\s*(.*)/);
30
- if (kv) {
31
- let val = kv[2].trim();
32
- // Strip surrounding quotes
33
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
34
- val = val.slice(1, -1);
18
+ const fm = {};
19
+ let body = content;
20
+
21
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)/);
22
+ if (match) {
23
+ const fmText = match[1];
24
+ body = match[2].trim();
25
+
26
+ for (const line of fmText.split("\n")) {
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed.startsWith("#")) continue;
29
+ const kv = trimmed.match(/^([\w-]+)\s*:\s*(.*)/);
30
+ if (kv) {
31
+ let val = kv[2].trim();
32
+ // Strip surrounding quotes
33
+ if (
34
+ (val.startsWith('"') && val.endsWith('"')) ||
35
+ (val.startsWith("'") && val.endsWith("'"))
36
+ ) {
37
+ val = val.slice(1, -1);
38
+ }
39
+ fm[kv[1]] = val;
40
+ }
35
41
  }
36
- fm[kv[1]] = val;
37
- }
38
42
  }
39
- }
40
43
 
41
- return { frontmatter: fm, body };
44
+ return { frontmatter: fm, body };
42
45
  }
43
46
 
44
47
  /**
45
48
  * Extract metadata from a skill directory
46
49
  */
47
50
  function extractMetadata(skillDir) {
48
- const hasSkillMd = fs.existsSync(path.join(skillDir, 'SKILL.md'));
49
- const hasPluginJson = fs.existsSync(path.join(skillDir, 'openclaw.plugin.json'));
50
- const hasPackageJson = fs.existsSync(path.join(skillDir, 'package.json'));
51
- const hasReadme = fs.existsSync(path.join(skillDir, 'README.md'));
52
-
53
- let name = '', displayName = '', description = '', version = '1.0.0';
54
- let readme = '', tags = [], category = '', longDescription = '';
55
- let detectedType = '';
56
-
57
- // --- Priority 1: SKILL.md ---
58
- if (hasSkillMd) {
59
- const content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
60
- const { frontmatter: fm, body } = parseFrontmatter(content);
61
-
62
- name = fm.name || '';
63
- displayName = fm.displayName || fm['display-name'] || '';
64
- description = fm.description || '';
65
- version = fm.version || '1.0.0';
66
- readme = body;
67
- if (fm.tags) {
68
- tags = fm.tags.split(',').map(t => t.trim()).filter(Boolean);
51
+ const hasSkillMd = fs.existsSync(path.join(skillDir, "SKILL.md"));
52
+ const hasPluginJson = fs.existsSync(
53
+ path.join(skillDir, "openclaw.plugin.json"),
54
+ );
55
+ const hasPackageJson = fs.existsSync(path.join(skillDir, "package.json"));
56
+ const hasReadme = fs.existsSync(path.join(skillDir, "README.md"));
57
+
58
+ let name = "",
59
+ displayName = "",
60
+ description = "",
61
+ version = "1.0.0";
62
+ let readme = "",
63
+ tags = [],
64
+ category = "",
65
+ longDescription = "";
66
+ let detectedType = "";
67
+
68
+ // --- Priority 1: SKILL.md ---
69
+ if (hasSkillMd) {
70
+ const content = fs.readFileSync(
71
+ path.join(skillDir, "SKILL.md"),
72
+ "utf-8",
73
+ );
74
+ const { frontmatter: fm, body } = parseFrontmatter(content);
75
+
76
+ name = fm.name || "";
77
+ displayName = fm.displayName || fm["display-name"] || "";
78
+ description = fm.description || "";
79
+ version = fm.version || "1.0.0";
80
+ readme = body;
81
+ if (fm.tags) {
82
+ tags = fm.tags
83
+ .split(",")
84
+ .map((t) => t.trim())
85
+ .filter(Boolean);
86
+ }
87
+ category = fm.category || "";
88
+ longDescription = fm.longDescription || description;
89
+ detectedType = fm.type || "skill";
69
90
  }
70
- category = fm.category || '';
71
- longDescription = fm.longDescription || description;
72
- detectedType = fm.type || 'skill';
73
- }
74
91
 
75
- // --- Priority 2: openclaw.plugin.json ---
76
- if (hasPluginJson && !name) {
77
- try {
78
- const plugin = JSON.parse(fs.readFileSync(path.join(skillDir, 'openclaw.plugin.json'), 'utf-8'));
79
- name = name || plugin.id || '';
80
- displayName = displayName || plugin.name || '';
81
- description = description || plugin.description || '';
82
- version = version === '1.0.0' ? (plugin.version || '1.0.0') : version;
83
-
84
- // Detect channel type
85
- if (Array.isArray(plugin.channels) && plugin.channels.length > 0) {
86
- detectedType = detectedType || 'channel';
87
- } else {
88
- detectedType = detectedType || 'plugin';
89
- }
90
- } catch {}
91
- }
92
+ // --- Priority 2: openclaw.plugin.json ---
93
+ if (hasPluginJson && !name) {
94
+ try {
95
+ const plugin = JSON.parse(
96
+ fs.readFileSync(
97
+ path.join(skillDir, "openclaw.plugin.json"),
98
+ "utf-8",
99
+ ),
100
+ );
101
+ name = name || plugin.id || "";
102
+ displayName = displayName || plugin.name || "";
103
+ description = description || plugin.description || "";
104
+ version = version === "1.0.0" ? plugin.version || "1.0.0" : version;
105
+
106
+ // Detect channel type
107
+ if (Array.isArray(plugin.channels) && plugin.channels.length > 0) {
108
+ detectedType = detectedType || "channel";
109
+ } else {
110
+ detectedType = detectedType || "plugin";
111
+ }
112
+ } catch {}
113
+ }
92
114
 
93
- // --- Priority 3: package.json ---
94
- if (hasPackageJson && !name) {
95
- try {
96
- const pkg = JSON.parse(fs.readFileSync(path.join(skillDir, 'package.json'), 'utf-8'));
97
- let pkgName = pkg.name || '';
98
- // Strip @scope/ prefix
99
- if (pkgName.startsWith('@') && pkgName.includes('/')) {
100
- pkgName = pkgName.split('/').pop();
101
- }
102
- name = name || pkgName;
103
- displayName = displayName || pkgName;
104
- description = description || pkg.description || '';
105
- version = version === '1.0.0' ? (pkg.version || '1.0.0') : version;
106
- } catch {}
107
- }
115
+ // --- Priority 3: package.json ---
116
+ if (hasPackageJson && !name) {
117
+ try {
118
+ const pkg = JSON.parse(
119
+ fs.readFileSync(path.join(skillDir, "package.json"), "utf-8"),
120
+ );
121
+ let pkgName = pkg.name || "";
122
+ // Strip @scope/ prefix
123
+ if (pkgName.startsWith("@") && pkgName.includes("/")) {
124
+ pkgName = pkgName.split("/").pop();
125
+ }
126
+ name = name || pkgName;
127
+ displayName = displayName || pkgName;
128
+ description = description || pkg.description || "";
129
+ version = version === "1.0.0" ? pkg.version || "1.0.0" : version;
130
+ } catch {}
131
+ }
108
132
 
109
- // --- Priority 4: README.md ---
110
- if (hasReadme) {
111
- try {
112
- const readmeContent = fs.readFileSync(path.join(skillDir, 'README.md'), 'utf-8');
113
- if (!readme) readme = readmeContent;
114
- if (!displayName) {
115
- const titleMatch = readmeContent.match(/^#\s+(.+)$/m);
116
- if (titleMatch) displayName = titleMatch[1].trim();
117
- }
118
- if (!description) {
119
- for (const line of readmeContent.split('\n')) {
120
- const t = line.trim();
121
- if (!t || t.startsWith('#') || t.startsWith('---')) continue;
122
- description = t;
123
- break;
124
- }
125
- }
126
- } catch {}
127
- }
128
-
129
- // Fallbacks
130
- if (!name) name = path.basename(skillDir);
131
- if (!displayName) displayName = name;
132
- if (!detectedType) detectedType = '';
133
-
134
- return {
135
- name, displayName, type: detectedType,
136
- description, version, readme,
137
- tags, category,
138
- longDescription: longDescription || description,
139
- };
133
+ // --- Priority 4: README.md ---
134
+ if (hasReadme) {
135
+ try {
136
+ const readmeContent = fs.readFileSync(
137
+ path.join(skillDir, "README.md"),
138
+ "utf-8",
139
+ );
140
+ if (!readme) readme = readmeContent;
141
+ if (!displayName) {
142
+ const titleMatch = readmeContent.match(/^#\s+(.+)$/m);
143
+ if (titleMatch) displayName = titleMatch[1].trim();
144
+ }
145
+ if (!description) {
146
+ for (const line of readmeContent.split("\n")) {
147
+ const t = line.trim();
148
+ if (!t || t.startsWith("#") || t.startsWith("---"))
149
+ continue;
150
+ description = t;
151
+ break;
152
+ }
153
+ }
154
+ } catch {}
155
+ }
156
+
157
+ // Fallbacks
158
+ if (!name) name = path.basename(skillDir);
159
+ if (!displayName) displayName = name;
160
+ if (!detectedType) detectedType = "";
161
+
162
+ return {
163
+ name,
164
+ displayName,
165
+ type: detectedType,
166
+ description,
167
+ version,
168
+ readme,
169
+ tags,
170
+ category,
171
+ longDescription: longDescription || description,
172
+ };
140
173
  }
141
174
 
142
175
  async function run(args, flags) {
143
- let skillDir = args[0] || '.';
144
- skillDir = path.resolve(skillDir);
145
-
146
- fish(`Publishing from ${c('bold', skillDir)}`);
147
- console.log('');
148
-
149
- // Check device ID
150
- const deviceId = config.getDeviceId();
151
- if (!deviceId) {
152
- err('No OpenClaw device identity found.');
153
- console.log('');
154
- console.log(` Expected: ${config.DEVICE_JSON}`);
155
- console.log(' Make sure OpenClaw is installed and has been started at least once.');
156
- console.log('');
157
- console.log(` Your device must be authorized first:`);
158
- console.log(` 1. Login on ${c('bold', config.getApiBase())} (GitHub/Google)`);
159
- console.log(' 2. Activate an invite code');
160
- console.log(' 3. Authorize this device (your deviceId will be auto-detected)');
161
- process.exit(1);
162
- }
163
-
164
- info(`Device ID: ${deviceId.slice(0, 12)}...`);
165
-
166
- // Extract metadata
167
- const meta = extractMetadata(skillDir);
168
-
169
- // If type not detected, prompt user
170
- if (!meta.type) {
171
- warn('Could not auto-detect asset type (no SKILL.md or openclaw.plugin.json found)');
172
- console.log(' Available types: skill, plugin, channel, trigger, experience');
173
-
174
- const readline = require('readline');
175
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
176
- meta.type = await new Promise(resolve => {
177
- rl.question(' Enter asset type: ', answer => {
178
- rl.close();
179
- resolve(answer.trim());
180
- });
181
- });
176
+ let skillDir = args[0] || ".";
177
+ skillDir = path.resolve(skillDir);
178
+
179
+ fish(`Publishing from ${c("bold", skillDir)}`);
180
+ console.log("");
181
+
182
+ // Check device ID
183
+ const deviceId = config.getDeviceId();
184
+ if (!deviceId) {
185
+ err("No OpenClaw device identity found.");
186
+ console.log("");
187
+ console.log(` Expected: ${config.DEVICE_JSON}`);
188
+ console.log(
189
+ " Make sure OpenClaw is installed and has been started at least once.",
190
+ );
191
+ console.log("");
192
+ console.log(` Your device must be authorized first:`);
193
+ console.log(
194
+ ` 1. Login on ${c("bold", config.getApiBase())} (GitHub/Google)`,
195
+ );
196
+ console.log(" 2. Activate an invite code");
197
+ console.log(
198
+ " 3. Authorize this device (your deviceId will be auto-detected)",
199
+ );
200
+ process.exit(1);
201
+ }
202
+
203
+ info(`Device ID: ${deviceId.slice(0, 12)}...`);
182
204
 
205
+ // Extract metadata
206
+ const meta = extractMetadata(skillDir);
207
+
208
+ // If type not detected, prompt user
183
209
  if (!meta.type) {
184
- err('Asset type is required');
185
- process.exit(1);
210
+ warn(
211
+ "Could not auto-detect asset type (no SKILL.md or openclaw.plugin.json found)",
212
+ );
213
+ console.log(
214
+ " Available types: skill, plugin, channel, trigger, experience",
215
+ );
216
+
217
+ const readline = require("readline");
218
+ const rl = readline.createInterface({
219
+ input: process.stdin,
220
+ output: process.stdout,
221
+ });
222
+ meta.type = await new Promise((resolve) => {
223
+ rl.question(" Enter asset type: ", (answer) => {
224
+ rl.close();
225
+ resolve(answer.trim());
226
+ });
227
+ });
228
+
229
+ if (!meta.type) {
230
+ err("Asset type is required");
231
+ process.exit(1);
232
+ }
186
233
  }
187
- }
188
-
189
- // ─── Validate package contents (hard block) ─────────────────────────
190
- const valErrors = [];
191
- switch (meta.type) {
192
- case 'skill': {
193
- const sp = path.join(skillDir, 'SKILL.md');
194
- if (!fs.existsSync(sp)) { valErrors.push('缺少 SKILL.md — skill 类型必须包含此文件'); break; }
195
- const { frontmatter: sfm, body: sbody } = parseFrontmatter(fs.readFileSync(sp, 'utf-8'));
196
- if (!sfm.name && !sfm.displayName && !sfm['display-name']) valErrors.push('SKILL.md frontmatter 缺少 name');
197
- if (!sfm.description) valErrors.push('SKILL.md frontmatter 缺少 description');
198
- if (!sbody.trim()) valErrors.push('SKILL.md 正文为空(frontmatter 之后需要技能说明)');
199
- break;
234
+
235
+ // ─── Validate package contents (hard block) ─────────────────────────
236
+ const valErrors = [];
237
+ switch (meta.type) {
238
+ case "skill": {
239
+ const sp = path.join(skillDir, "SKILL.md");
240
+ if (!fs.existsSync(sp)) {
241
+ valErrors.push("缺少 SKILL.md — skill 类型必须包含此文件");
242
+ break;
243
+ }
244
+ const { frontmatter: sfm, body: sbody } = parseFrontmatter(
245
+ fs.readFileSync(sp, "utf-8"),
246
+ );
247
+ if (!sfm.name && !sfm.displayName && !sfm["display-name"])
248
+ valErrors.push("SKILL.md frontmatter 缺少 name");
249
+ if (!sfm.description)
250
+ valErrors.push("SKILL.md frontmatter 缺少 description");
251
+ if (!sbody.trim())
252
+ valErrors.push(
253
+ "SKILL.md 正文为空(frontmatter 之后需要技能说明)",
254
+ );
255
+ break;
256
+ }
257
+ case "plugin":
258
+ case "channel": {
259
+ const pjp = path.join(skillDir, "openclaw.plugin.json");
260
+ if (!fs.existsSync(pjp)) {
261
+ valErrors.push(
262
+ `缺少 openclaw.plugin.json — ${meta.type} 类型必须包含此文件`,
263
+ );
264
+ break;
265
+ }
266
+ try {
267
+ const pd = JSON.parse(fs.readFileSync(pjp, "utf-8"));
268
+ if (!pd.id) valErrors.push("openclaw.plugin.json 缺少 id");
269
+ if (
270
+ meta.type === "channel" &&
271
+ (!Array.isArray(pd.channels) || !pd.channels.length)
272
+ ) {
273
+ valErrors.push(
274
+ "openclaw.plugin.json 缺少 channels 数组(channel 类型必须声明)",
275
+ );
276
+ }
277
+ } catch {
278
+ valErrors.push("openclaw.plugin.json JSON 格式错误");
279
+ break;
280
+ }
281
+ if (!fs.existsSync(path.join(skillDir, "README.md")))
282
+ valErrors.push(
283
+ `缺少 README.md — ${meta.type} 类型必须包含 README.md`,
284
+ );
285
+ if (!meta.displayName || !meta.description)
286
+ valErrors.push(
287
+ "无法提取 displayName/description — 请在 openclaw.plugin.json 添加 name/description 或确保 README.md 有标题和描述",
288
+ );
289
+ break;
290
+ }
291
+ case "trigger":
292
+ case "experience": {
293
+ const rp = path.join(skillDir, "README.md");
294
+ if (!fs.existsSync(rp)) {
295
+ valErrors.push(
296
+ `缺少 README.md — ${meta.type} 类型必须包含此文件`,
297
+ );
298
+ break;
299
+ }
300
+ const rc = fs.readFileSync(rp, "utf-8");
301
+ let ht = false,
302
+ hd = false;
303
+ for (const l of rc.split("\n")) {
304
+ const t = l.trim();
305
+ if (!ht && /^#\s+.+/.test(t)) {
306
+ ht = true;
307
+ continue;
308
+ }
309
+ if (
310
+ ht &&
311
+ !hd &&
312
+ t &&
313
+ !t.startsWith("#") &&
314
+ !t.startsWith("---") &&
315
+ !t.startsWith(">")
316
+ ) {
317
+ hd = true;
318
+ break;
319
+ }
320
+ }
321
+ if (!ht) valErrors.push("README.md 缺少标题行(# 名称)");
322
+ if (!hd)
323
+ valErrors.push(
324
+ "README.md 缺少描述段落(标题后需要有文字说明)",
325
+ );
326
+ break;
327
+ }
328
+ }
329
+
330
+ if (valErrors.length) {
331
+ console.log("");
332
+ err("发布校验失败:");
333
+ for (const e of valErrors) console.log(` ${c("red", "✗")} ${e}`);
334
+ console.log("");
335
+ info("请补全以上内容后重新发布。");
336
+ process.exit(1);
337
+ }
338
+
339
+ // Show preview
340
+ console.log("");
341
+ console.log(` Name: ${meta.name}`);
342
+ console.log(` Display: ${meta.displayName}`);
343
+ console.log(` Type: ${meta.type}`);
344
+ console.log(` Version: ${meta.version}`);
345
+ console.log(` Description: ${(meta.description || "").slice(0, 80)}`);
346
+ if (meta.tags.length) {
347
+ console.log(` Tags: ${meta.tags.join(", ")}`);
200
348
  }
201
- case 'plugin':
202
- case 'channel': {
203
- const pjp = path.join(skillDir, 'openclaw.plugin.json');
204
- if (!fs.existsSync(pjp)) { valErrors.push(`缺少 openclaw.plugin.json — ${meta.type} 类型必须包含此文件`); break; }
205
- try {
206
- const pd = JSON.parse(fs.readFileSync(pjp, 'utf-8'));
207
- if (!pd.id) valErrors.push('openclaw.plugin.json 缺少 id');
208
- if (meta.type === 'channel' && (!Array.isArray(pd.channels) || !pd.channels.length)) {
209
- valErrors.push('openclaw.plugin.json 缺少 channels 数组(channel 类型必须声明)');
349
+ console.log("");
350
+
351
+ // Confirm
352
+ if (!flags.yes && !flags.y) {
353
+ const readline = require("readline");
354
+ const rl = readline.createInterface({
355
+ input: process.stdin,
356
+ output: process.stdout,
357
+ });
358
+ const answer = await new Promise((resolve) => {
359
+ rl.question(` Publish to ${config.getApiBase()}? [Y/n] `, resolve);
360
+ });
361
+ rl.close();
362
+ if (/^[nN]/.test(answer)) {
363
+ info("Cancelled.");
364
+ return;
210
365
  }
211
- } catch { valErrors.push('openclaw.plugin.json JSON 格式错误'); break; }
212
- if (!fs.existsSync(path.join(skillDir, 'README.md'))) valErrors.push(`缺少 README.md — ${meta.type} 类型必须包含 README.md`);
213
- if (!meta.displayName || !meta.description) valErrors.push('无法提取 displayName/description — 请在 openclaw.plugin.json 添加 name/description 或确保 README.md 有标题和描述');
214
- break;
215
366
  }
216
- case 'trigger':
217
- case 'experience': {
218
- const rp = path.join(skillDir, 'README.md');
219
- if (!fs.existsSync(rp)) { valErrors.push(`缺少 README.md — ${meta.type} 类型必须包含此文件`); break; }
220
- const rc = fs.readFileSync(rp, 'utf-8');
221
- let ht = false, hd = false;
222
- for (const l of rc.split('\n')) {
223
- const t = l.trim();
224
- if (!ht && /^#\s+.+/.test(t)) { ht = true; continue; }
225
- if (ht && !hd && t && !t.startsWith('#') && !t.startsWith('---') && !t.startsWith('>')) { hd = true; break; }
226
- }
227
- if (!ht) valErrors.push('README.md 缺少标题行(# 名称)');
228
- if (!hd) valErrors.push('README.md 缺少描述段落(标题后需要有文字说明)');
229
- break;
367
+
368
+ // Create tarball
369
+ const tarball = path.join(
370
+ require("os").tmpdir(),
371
+ `openclawmp-publish-${Date.now()}.tar.gz`,
372
+ );
373
+ try {
374
+ createTarGzFromDirectory(skillDir, tarball);
375
+ } catch (e) {
376
+ err("Failed to create package tarball");
377
+ process.exit(1);
230
378
  }
231
- }
232
-
233
- if (valErrors.length) {
234
- console.log('');
235
- err('发布校验失败:');
236
- for (const e of valErrors) console.log(` ${c('red', '✗')} ${e}`);
237
- console.log('');
238
- info('请补全以上内容后重新发布。');
239
- process.exit(1);
240
- }
241
-
242
- // Show preview
243
- console.log('');
244
- console.log(` Name: ${meta.name}`);
245
- console.log(` Display: ${meta.displayName}`);
246
- console.log(` Type: ${meta.type}`);
247
- console.log(` Version: ${meta.version}`);
248
- console.log(` Description: ${(meta.description || '').slice(0, 80)}`);
249
- if (meta.tags.length) {
250
- console.log(` Tags: ${meta.tags.join(', ')}`);
251
- }
252
- console.log('');
253
-
254
- // Confirm
255
- if (!flags.yes && !flags.y) {
256
- const readline = require('readline');
257
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
258
- const answer = await new Promise(resolve => {
259
- rl.question(` Publish to ${config.getApiBase()}? [Y/n] `, resolve);
260
- });
261
- rl.close();
262
- if (/^[nN]/.test(answer)) {
263
- info('Cancelled.');
264
- return;
379
+
380
+ const tarStats = fs.statSync(tarball);
381
+ const sizeKb = (tarStats.size / 1024).toFixed(1);
382
+ info(`Package: ${sizeKb}KB compressed`);
383
+
384
+ // Build payload
385
+ const payload = {
386
+ name: meta.name,
387
+ displayName: meta.displayName,
388
+ type: meta.type,
389
+ description: meta.description,
390
+ version: meta.version,
391
+ readme: meta.readme,
392
+ tags: meta.tags,
393
+ category: meta.category,
394
+ longDescription: meta.longDescription,
395
+ authorId: process.env.SEAFOOD_AUTHOR_ID || "",
396
+ authorName: process.env.SEAFOOD_AUTHOR_NAME || "",
397
+ authorAvatar: process.env.SEAFOOD_AUTHOR_AVATAR || "",
398
+ };
399
+
400
+ // POST multipart: metadata + package file
401
+ let formData;
402
+
403
+ // Node 18+ has global FormData via undici
404
+ if (typeof globalThis.FormData !== "undefined") {
405
+ formData = new globalThis.FormData();
406
+ formData.append(
407
+ "metadata",
408
+ new Blob([JSON.stringify(payload)], { type: "application/json" }),
409
+ "metadata.json",
410
+ );
411
+ const tarBuffer = fs.readFileSync(tarball);
412
+ formData.append(
413
+ "package",
414
+ new Blob([tarBuffer], { type: "application/gzip" }),
415
+ "package.tar.gz",
416
+ );
417
+ } else {
418
+ // Fallback for older Node — use raw fetch with multipart boundary
419
+ err("FormData not available. Requires Node.js 18+ with fetch support.");
420
+ process.exit(1);
265
421
  }
266
- }
267
-
268
- // Create tarball
269
- const tarball = path.join(require('os').tmpdir(), `openclawmp-publish-${Date.now()}.tar.gz`);
270
- try {
271
- createTarGzFromDirectory(skillDir, tarball);
272
- } catch (e) {
273
- err('Failed to create package tarball');
274
- process.exit(1);
275
- }
276
-
277
- const tarStats = fs.statSync(tarball);
278
- const sizeKb = (tarStats.size / 1024).toFixed(1);
279
- info(`Package: ${sizeKb}KB compressed`);
280
-
281
- // Build payload
282
- const payload = {
283
- name: meta.name,
284
- displayName: meta.displayName,
285
- type: meta.type,
286
- description: meta.description,
287
- version: meta.version,
288
- readme: meta.readme,
289
- tags: meta.tags,
290
- category: meta.category,
291
- longDescription: meta.longDescription,
292
- authorId: process.env.SEAFOOD_AUTHOR_ID || '',
293
- authorName: process.env.SEAFOOD_AUTHOR_NAME || '',
294
- authorAvatar: process.env.SEAFOOD_AUTHOR_AVATAR || '',
295
- };
296
-
297
- // POST multipart: metadata + package file
298
- const { FormData, File } = require('node:buffer');
299
- let formData;
300
-
301
- // Node 18+ has global FormData via undici
302
- if (typeof globalThis.FormData !== 'undefined') {
303
- formData = new globalThis.FormData();
304
- formData.append('metadata', new Blob([JSON.stringify(payload)], { type: 'application/json' }), 'metadata.json');
305
- const tarBuffer = fs.readFileSync(tarball);
306
- formData.append('package', new Blob([tarBuffer], { type: 'application/gzip' }), 'package.tar.gz');
307
- } else {
308
- // Fallback for older Node — use raw fetch with multipart boundary
309
- err('FormData not available. Requires Node.js 18+ with fetch support.');
310
- process.exit(1);
311
- }
312
-
313
- const { status, data: respData } = await api.postMultipart('/api/v1/assets/publish', formData);
314
-
315
- // Clean up tarball
316
- try { fs.unlinkSync(tarball); } catch {}
317
-
318
- if (status === 200 || status === 201) {
319
- const assetId = respData?.data?.id || 'unknown';
320
- const fileCount = respData?.data?.files?.length || '?';
321
-
322
- console.log('');
323
- ok('Published successfully! 🎉');
324
- console.log('');
325
- detail('ID', assetId);
326
- detail('Files', fileCount);
327
- detail('Page', `${config.getApiBase()}/asset/${assetId}`);
328
- console.log('');
329
-
330
- // Check for metadataIncomplete flag
331
- if (respData?.data?.metadataIncomplete) {
332
- const missingFields = (respData.data.missingFields || []).join(', ');
333
- warn(`部分元数据缺失: ${missingFields}`);
334
- console.log(' 建议让 Agent 自动补全,或手动编辑后重新发布');
335
- console.log('');
422
+
423
+ const { status, data: respData } = await api.postMultipart(
424
+ "/api/v1/assets/publish",
425
+ formData,
426
+ );
427
+
428
+ // 调用安审接口 /api/v1/assets/scan
429
+ let scanStatus = 0;
430
+ let scanRespData = {};
431
+ detail("上传成功, 正在审核中 🎉", "padding");
432
+ if (typeof globalThis.FormData !== "undefined") {
433
+ const scanFormData = new globalThis.FormData();
434
+ if (respData?.data?.id) {
435
+ scanFormData.append("assetId", String(respData.data.id));
436
+ }
437
+ if (respData?.data?.url) {
438
+ scanFormData.append("url", String(respData.data.url));
439
+ }
440
+ if (respData?.data?.hash) {
441
+ scanFormData.append("hash", String(respData.data.hash));
442
+ }
443
+ // 可选:附带包供安审
444
+ scanFormData.append(
445
+ "metadata",
446
+ new Blob([JSON.stringify(payload)], { type: "application/json" }),
447
+ "metadata.json",
448
+ );
449
+ const tarBufForScan = fs.readFileSync(tarball);
450
+ scanFormData.append(
451
+ "package",
452
+ new Blob([tarBufForScan], { type: "application/gzip" }),
453
+ "package.tar.gz",
454
+ );
455
+ const { status: sStatus, data: sData } = await api.postMultipart(
456
+ "/api/v1/assets/scan",
457
+ scanFormData,
458
+ );
459
+ scanStatus = sStatus;
460
+ scanRespData = sData;
461
+ } else {
462
+ err(
463
+ "FormData not available for scan. Requires Node.js 18+ with fetch support.",
464
+ );
465
+ process.exit(1);
466
+ }
467
+ // Clean up tarball
468
+ try {
469
+ fs.unlinkSync(tarball);
470
+ } catch {}
471
+
472
+ if (status === 200 || status === 201) {
473
+ const assetId = respData?.data?.id || "unknown";
474
+ const fileCount = respData?.data?.files?.length || "?";
475
+
476
+ console.log("");
477
+ scanRespData?.data?.scan_message
478
+ ? ok("Published successfully! 🎉")
479
+ : warn("Published Unsuccessfully! ");
480
+ console.log("");
481
+ detail("ID", assetId);
482
+ detail("Files", fileCount);
483
+ detail("Page", `${config.getApiBase()}/asset/${assetId}`);
484
+ console.log("");
485
+
486
+ // Check for metadataIncomplete flag
487
+ if (respData?.data?.metadataIncomplete) {
488
+ const missingFields = (respData.data.missingFields || []).join(
489
+ ", ",
490
+ );
491
+ warn(`部分元数据缺失: ${missingFields}`);
492
+ console.log(" 建议让 Agent 自动补全,或手动编辑后重新发布");
493
+ console.log("");
494
+ }
495
+ if (scanRespData?.data?.scan_message || scanRespData?.scan_message) {
496
+ detail(
497
+ "scan_message",
498
+ scanRespData?.data?.scan_message || scanRespData?.scan_message,
499
+ );
500
+ }
501
+ } else {
502
+ const errorMsg =
503
+ respData?.error || JSON.stringify(respData) || "Unknown error";
504
+ err(`Publish failed (HTTP ${status}): ${errorMsg}`);
505
+ detail("scan_err_message", scanRespData?.scan_message);
506
+ process.exit(1);
336
507
  }
337
- } else {
338
- const errorMsg = respData?.error || JSON.stringify(respData) || 'Unknown error';
339
- err(`Publish failed (HTTP ${status}): ${errorMsg}`);
340
- process.exit(1);
341
- }
342
508
  }
343
509
 
344
510
  module.exports = { run };