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.
- package/lib/commands/comment.js +101 -83
- package/lib/commands/issue.js +121 -89
- package/lib/commands/publish.js +472 -306
- package/lib/config.js +104 -97
- package/package.json +3 -3
package/lib/commands/publish.js
CHANGED
|
@@ -2,343 +2,509 @@
|
|
|
2
2
|
// commands/publish.js — Publish a local asset directory to the marketplace
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
"use strict";
|
|
6
6
|
|
|
7
|
-
const fs = require(
|
|
8
|
-
const path = require(
|
|
9
|
-
const api = require(
|
|
10
|
-
const { createTarGzFromDirectory } = require(
|
|
11
|
-
const config = require(
|
|
12
|
-
const { fish, info, ok, warn, err, c, detail } = require(
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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 };
|