openclawmp 0.1.3 → 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/archive.js +322 -0
- package/lib/commands/comment.js +101 -83
- package/lib/commands/install.js +1 -43
- 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/archive.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const zlib = require('zlib');
|
|
6
|
+
|
|
7
|
+
const TAR_BLOCK_SIZE = 512;
|
|
8
|
+
const TAR_END_BLOCKS = Buffer.alloc(TAR_BLOCK_SIZE * 2, 0);
|
|
9
|
+
const ZIP_LOCAL_FILE_HEADER = 0x04034b50;
|
|
10
|
+
const ZIP_CENTRAL_DIRECTORY_HEADER = 0x02014b50;
|
|
11
|
+
const ZIP_END_OF_CENTRAL_DIRECTORY = 0x06054b50;
|
|
12
|
+
|
|
13
|
+
function normalizeArchivePath(filePath) {
|
|
14
|
+
return filePath.split(path.sep).join('/');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function withDotPrefix(filePath) {
|
|
18
|
+
const normalized = normalizeArchivePath(filePath);
|
|
19
|
+
if (!normalized || normalized === '.') return './';
|
|
20
|
+
return normalized.startsWith('./') ? normalized : `./${normalized}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function posixModeForStat(stat) {
|
|
24
|
+
return stat.mode & 0o7777;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeString(buffer, value, offset, length) {
|
|
28
|
+
const source = Buffer.from(value, 'utf8');
|
|
29
|
+
source.copy(buffer, offset, 0, Math.min(source.length, length));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeOctal(buffer, value, offset, length) {
|
|
33
|
+
const octal = Math.max(0, value).toString(8);
|
|
34
|
+
const padded = octal.padStart(length - 2, '0');
|
|
35
|
+
writeString(buffer, `${padded}\0 `, offset, length);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeChecksumPlaceholder(buffer) {
|
|
39
|
+
buffer.fill(0x20, 148, 156);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function finalizeChecksum(buffer) {
|
|
43
|
+
let checksum = 0;
|
|
44
|
+
for (const byte of buffer) checksum += byte;
|
|
45
|
+
const octal = checksum.toString(8).padStart(6, '0');
|
|
46
|
+
writeString(buffer, `${octal}\0 `, 148, 8);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildTarHeader(name, stat, typeflag) {
|
|
50
|
+
const header = Buffer.alloc(TAR_BLOCK_SIZE, 0);
|
|
51
|
+
const normalizedName = normalizeArchivePath(name);
|
|
52
|
+
const prefixCut = normalizedName.length > 100 ? normalizedName.lastIndexOf('/', 100) : -1;
|
|
53
|
+
let entryName = normalizedName;
|
|
54
|
+
let prefix = '';
|
|
55
|
+
|
|
56
|
+
if (normalizedName.length > 100) {
|
|
57
|
+
if (prefixCut <= 0 || normalizedName.length - prefixCut - 1 > 100 || prefixCut > 155) {
|
|
58
|
+
throw new Error(`Path too long for tar header: ${normalizedName}`);
|
|
59
|
+
}
|
|
60
|
+
prefix = normalizedName.slice(0, prefixCut);
|
|
61
|
+
entryName = normalizedName.slice(prefixCut + 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
writeString(header, entryName, 0, 100);
|
|
65
|
+
writeOctal(header, posixModeForStat(stat), 100, 8);
|
|
66
|
+
writeOctal(header, 0, 108, 8);
|
|
67
|
+
writeOctal(header, 0, 116, 8);
|
|
68
|
+
writeOctal(header, typeflag === '5' ? 0 : stat.size, 124, 12);
|
|
69
|
+
writeOctal(header, Math.floor(stat.mtimeMs / 1000), 136, 12);
|
|
70
|
+
writeChecksumPlaceholder(header);
|
|
71
|
+
writeString(header, typeflag, 156, 1);
|
|
72
|
+
writeString(header, 'ustar', 257, 6);
|
|
73
|
+
writeString(header, '00', 263, 2);
|
|
74
|
+
if (prefix) writeString(header, prefix, 345, 155);
|
|
75
|
+
finalizeChecksum(header);
|
|
76
|
+
return header;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function collectEntries(rootDir, currentDir = rootDir, entries = []) {
|
|
80
|
+
const children = fs.readdirSync(currentDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
81
|
+
for (const child of children) {
|
|
82
|
+
const absPath = path.join(currentDir, child.name);
|
|
83
|
+
const relPath = path.relative(rootDir, absPath);
|
|
84
|
+
const stat = fs.lstatSync(absPath);
|
|
85
|
+
|
|
86
|
+
if (stat.isDirectory()) {
|
|
87
|
+
entries.push({ path: relPath, stat, typeflag: '5' });
|
|
88
|
+
collectEntries(rootDir, absPath, entries);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (stat.isFile()) {
|
|
93
|
+
entries.push({ path: relPath, stat, typeflag: '0' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return entries;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createTarBuffer(sourceDir) {
|
|
100
|
+
const chunks = [];
|
|
101
|
+
const entries = collectEntries(sourceDir);
|
|
102
|
+
const rootStat = fs.lstatSync(sourceDir);
|
|
103
|
+
|
|
104
|
+
chunks.push(buildTarHeader('./', rootStat, '5'));
|
|
105
|
+
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const archivePath = withDotPrefix(entry.path);
|
|
108
|
+
const headerPath = entry.typeflag === '5' ? `${archivePath}/` : archivePath;
|
|
109
|
+
chunks.push(buildTarHeader(headerPath, entry.stat, entry.typeflag));
|
|
110
|
+
|
|
111
|
+
if (entry.typeflag === '0') {
|
|
112
|
+
const content = fs.readFileSync(path.join(sourceDir, entry.path));
|
|
113
|
+
chunks.push(content);
|
|
114
|
+
const remainder = content.length % TAR_BLOCK_SIZE;
|
|
115
|
+
if (remainder !== 0) {
|
|
116
|
+
chunks.push(Buffer.alloc(TAR_BLOCK_SIZE - remainder, 0));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
chunks.push(TAR_END_BLOCKS);
|
|
122
|
+
return Buffer.concat(chunks);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createTarGzFromDirectory(sourceDir, targetFile) {
|
|
126
|
+
const tarBuffer = createTarBuffer(sourceDir);
|
|
127
|
+
const gzipBuffer = zlib.gzipSync(tarBuffer);
|
|
128
|
+
fs.writeFileSync(targetFile, gzipBuffer);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isTarBuffer(buffer) {
|
|
132
|
+
if (buffer.length < 265) return false;
|
|
133
|
+
const magic = buffer.toString('utf8', 257, 262);
|
|
134
|
+
return magic === 'ustar';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function pathSegments(filePath) {
|
|
138
|
+
return normalizeArchivePath(filePath).split('/').filter(segment => segment && segment !== '.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function stripCommonRoot(entries) {
|
|
142
|
+
const fileEntries = entries.filter(entry => entry.name && entry.segments.length > 0);
|
|
143
|
+
if (fileEntries.length === 0) return entries;
|
|
144
|
+
|
|
145
|
+
const firstRoot = fileEntries[0].segments[0];
|
|
146
|
+
if (!firstRoot) return entries;
|
|
147
|
+
if (!fileEntries.every(entry => entry.segments[0] === firstRoot)) return entries;
|
|
148
|
+
|
|
149
|
+
const hasRootFile = fileEntries.some(entry => entry.segments.length === 1 && entry.type !== 'directory');
|
|
150
|
+
if (hasRootFile) return entries;
|
|
151
|
+
|
|
152
|
+
return entries.map(entry => ({
|
|
153
|
+
...entry,
|
|
154
|
+
segments: entry.segments.slice(1),
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensureWithinTarget(targetDir, segments) {
|
|
159
|
+
const destination = path.resolve(targetDir, ...segments);
|
|
160
|
+
const root = path.resolve(targetDir);
|
|
161
|
+
if (destination !== root && !destination.startsWith(`${root}${path.sep}`)) {
|
|
162
|
+
throw new Error(`Archive entry escapes target directory: ${segments.join('/')}`);
|
|
163
|
+
}
|
|
164
|
+
return destination;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function applyMode(destination, mode, isDirectory) {
|
|
168
|
+
if (process.platform === 'win32') return;
|
|
169
|
+
const fallback = isDirectory ? 0o755 : 0o644;
|
|
170
|
+
try {
|
|
171
|
+
fs.chmodSync(destination, mode || fallback);
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function extractEntries(entries, targetDir) {
|
|
176
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
177
|
+
const normalizedEntries = stripCommonRoot(entries).filter(entry => entry.segments.length > 0);
|
|
178
|
+
|
|
179
|
+
for (const entry of normalizedEntries) {
|
|
180
|
+
const destination = ensureWithinTarget(targetDir, entry.segments);
|
|
181
|
+
if (entry.type === 'directory') {
|
|
182
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
183
|
+
applyMode(destination, entry.mode, true);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
188
|
+
fs.writeFileSync(destination, entry.content);
|
|
189
|
+
applyMode(destination, entry.mode, false);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return normalizedEntries.length > 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseTarOctal(buffer, offset, length) {
|
|
196
|
+
const raw = buffer.toString('utf8', offset, offset + length).replace(/\0.*$/, '').trim();
|
|
197
|
+
return raw ? parseInt(raw, 8) : 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function extractTar(buffer, targetDir) {
|
|
201
|
+
const entries = [];
|
|
202
|
+
let offset = 0;
|
|
203
|
+
|
|
204
|
+
while (offset + TAR_BLOCK_SIZE <= buffer.length) {
|
|
205
|
+
const header = buffer.subarray(offset, offset + TAR_BLOCK_SIZE);
|
|
206
|
+
if (header.every(byte => byte === 0)) break;
|
|
207
|
+
|
|
208
|
+
const name = header.toString('utf8', 0, 100).replace(/\0.*$/, '');
|
|
209
|
+
const prefix = header.toString('utf8', 345, 500).replace(/\0.*$/, '');
|
|
210
|
+
const fullName = prefix ? `${prefix}/${name}` : name;
|
|
211
|
+
const typeflag = header.toString('utf8', 156, 157) || '0';
|
|
212
|
+
const size = parseTarOctal(header, 124, 12);
|
|
213
|
+
const mode = parseTarOctal(header, 100, 8);
|
|
214
|
+
offset += TAR_BLOCK_SIZE;
|
|
215
|
+
|
|
216
|
+
const content = buffer.subarray(offset, offset + size);
|
|
217
|
+
const segments = pathSegments(fullName);
|
|
218
|
+
if (typeflag === '5') {
|
|
219
|
+
entries.push({ name: fullName, segments, type: 'directory', mode });
|
|
220
|
+
} else if (typeflag === '0' || typeflag === '\0') {
|
|
221
|
+
entries.push({ name: fullName, segments, type: 'file', mode, content: Buffer.from(content) });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
offset += Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return extractEntries(entries, targetDir);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function locateZipEnd(buffer) {
|
|
231
|
+
const minimumOffset = Math.max(0, buffer.length - 0xffff - 22);
|
|
232
|
+
for (let offset = buffer.length - 22; offset >= minimumOffset; offset--) {
|
|
233
|
+
if (buffer.readUInt32LE(offset) === ZIP_END_OF_CENTRAL_DIRECTORY) {
|
|
234
|
+
return offset;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
throw new Error('Invalid zip: end of central directory not found');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractZip(buffer, targetDir) {
|
|
241
|
+
const eocdOffset = locateZipEnd(buffer);
|
|
242
|
+
const centralDirectorySize = buffer.readUInt32LE(eocdOffset + 12);
|
|
243
|
+
const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16);
|
|
244
|
+
const entries = [];
|
|
245
|
+
|
|
246
|
+
let offset = centralDirectoryOffset;
|
|
247
|
+
const end = centralDirectoryOffset + centralDirectorySize;
|
|
248
|
+
|
|
249
|
+
while (offset < end) {
|
|
250
|
+
if (buffer.readUInt32LE(offset) !== ZIP_CENTRAL_DIRECTORY_HEADER) {
|
|
251
|
+
throw new Error('Invalid zip: malformed central directory');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const compressionMethod = buffer.readUInt16LE(offset + 10);
|
|
255
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
256
|
+
const uncompressedSize = buffer.readUInt32LE(offset + 24);
|
|
257
|
+
const fileNameLength = buffer.readUInt16LE(offset + 28);
|
|
258
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
259
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
260
|
+
const externalAttributes = buffer.readUInt32LE(offset + 38);
|
|
261
|
+
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
262
|
+
const fileName = buffer.toString('utf8', offset + 46, offset + 46 + fileNameLength);
|
|
263
|
+
const isDirectory = fileName.endsWith('/');
|
|
264
|
+
const mode = (externalAttributes >>> 16) & 0xffff;
|
|
265
|
+
|
|
266
|
+
if (buffer.readUInt32LE(localHeaderOffset) !== ZIP_LOCAL_FILE_HEADER) {
|
|
267
|
+
throw new Error('Invalid zip: missing local file header');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
|
|
271
|
+
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
|
|
272
|
+
const dataOffset = localHeaderOffset + 30 + localNameLength + localExtraLength;
|
|
273
|
+
const compressedData = buffer.subarray(dataOffset, dataOffset + compressedSize);
|
|
274
|
+
|
|
275
|
+
let content = Buffer.alloc(0);
|
|
276
|
+
if (!isDirectory) {
|
|
277
|
+
if (compressionMethod === 0) {
|
|
278
|
+
content = Buffer.from(compressedData);
|
|
279
|
+
} else if (compressionMethod === 8) {
|
|
280
|
+
content = zlib.inflateRawSync(compressedData);
|
|
281
|
+
} else {
|
|
282
|
+
throw new Error(`Unsupported zip compression method: ${compressionMethod}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (content.length !== uncompressedSize) {
|
|
286
|
+
throw new Error('Invalid zip: uncompressed size mismatch');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
entries.push({
|
|
291
|
+
name: fileName,
|
|
292
|
+
segments: pathSegments(fileName),
|
|
293
|
+
type: isDirectory ? 'directory' : 'file',
|
|
294
|
+
mode,
|
|
295
|
+
content,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
offset += 46 + fileNameLength + extraLength + commentLength;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return extractEntries(entries, targetDir);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function extractPackage(buffer, targetDir) {
|
|
305
|
+
const isZip = buffer.length >= 4 && buffer.readUInt32LE(0) === ZIP_LOCAL_FILE_HEADER;
|
|
306
|
+
if (isZip) {
|
|
307
|
+
return extractZip(buffer, targetDir);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let tarBuffer = buffer;
|
|
311
|
+
try {
|
|
312
|
+
tarBuffer = zlib.gunzipSync(buffer);
|
|
313
|
+
} catch {}
|
|
314
|
+
|
|
315
|
+
if (!isTarBuffer(tarBuffer)) return false;
|
|
316
|
+
return extractTar(tarBuffer, targetDir);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
createTarGzFromDirectory,
|
|
321
|
+
extractPackage,
|
|
322
|
+
};
|
package/lib/commands/comment.js
CHANGED
|
@@ -2,118 +2,136 @@
|
|
|
2
2
|
// commands/comment.js — Post / list comments on an asset
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
"use strict";
|
|
6
6
|
|
|
7
|
-
const api = require(
|
|
8
|
-
const auth = require(
|
|
9
|
-
const { ok, info, err, c, detail } = require(
|
|
7
|
+
const api = require("../api.js");
|
|
8
|
+
const auth = require("../auth.js");
|
|
9
|
+
const { ok, info, err, c, detail } = require("../ui.js");
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Format a timestamp to a readable date string
|
|
13
13
|
*/
|
|
14
14
|
function fmtDate(ts) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
if (!ts) return "?";
|
|
16
|
+
const d = new Date(ts);
|
|
17
|
+
if (isNaN(d.getTime())) return String(ts);
|
|
18
|
+
return d.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Render star rating: ★★★★☆
|
|
23
23
|
*/
|
|
24
24
|
function renderRating(rating) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
if (!rating) return "";
|
|
26
|
+
const n = Math.max(0, Math.min(5, Math.round(rating)));
|
|
27
|
+
return c("yellow", "★".repeat(n)) + c("dim", "☆".repeat(5 - n));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* openclawmp comment <assetRef> <content> [--rating N] [--as-agent]
|
|
32
32
|
*/
|
|
33
33
|
async function runComment(args, flags) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const asset = await api.resolveAssetRef(args[0]);
|
|
46
|
-
const content = args.slice(1).join(' ');
|
|
47
|
-
const displayName = asset.displayName || asset.name || args[0];
|
|
48
|
-
|
|
49
|
-
const body = {
|
|
50
|
-
content,
|
|
51
|
-
commenterType: flags['as-agent'] ? 'agent' : 'user',
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
if (flags.rating !== undefined) {
|
|
55
|
-
const rating = parseInt(flags.rating, 10);
|
|
56
|
-
if (isNaN(rating) || rating < 1 || rating > 5) {
|
|
57
|
-
err('Rating must be between 1 and 5');
|
|
58
|
-
process.exit(1);
|
|
34
|
+
if (args.length < 2) {
|
|
35
|
+
err(
|
|
36
|
+
"Usage: openclawmp comment <assetRef> <content> [--rating 5] [--as-agent]",
|
|
37
|
+
);
|
|
38
|
+
console.log(
|
|
39
|
+
' Example: openclawmp comment trigger/@xiaoyue/pdf-watcher "非常好用!"',
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
59
42
|
}
|
|
60
|
-
body.rating = rating;
|
|
61
|
-
}
|
|
62
43
|
|
|
63
|
-
|
|
44
|
+
if (!auth.isAuthenticated()) {
|
|
45
|
+
err("Authentication required. Run: openclawmp login");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
50
|
+
const content = args.slice(1).join(" ");
|
|
51
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
52
|
+
|
|
53
|
+
const body = {
|
|
54
|
+
content,
|
|
55
|
+
commenterType: flags["as-agent"] ? "agent" : "user",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (flags.rating !== undefined) {
|
|
59
|
+
const rating = parseInt(flags.rating, 10);
|
|
60
|
+
if (isNaN(rating) || rating < 1 || rating > 5) {
|
|
61
|
+
err("Rating must be between 1 and 5");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
body.rating = rating;
|
|
65
|
+
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const { status, data } = await api.post(
|
|
68
|
+
`/api/assets/${asset.id}/comments`,
|
|
69
|
+
body,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (status >= 200 && status < 300) {
|
|
73
|
+
const comment = data.comment || data;
|
|
74
|
+
console.log("");
|
|
75
|
+
ok(`评论审核成功后自动发布 ${c("bold", displayName)}`);
|
|
76
|
+
if (body.rating) {
|
|
77
|
+
detail("评分", renderRating(body.rating));
|
|
78
|
+
}
|
|
79
|
+
detail("内容", content);
|
|
80
|
+
console.log("");
|
|
81
|
+
} else {
|
|
82
|
+
err(
|
|
83
|
+
`评论失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`,
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
71
86
|
}
|
|
72
|
-
detail('内容', content);
|
|
73
|
-
console.log('');
|
|
74
|
-
} else {
|
|
75
|
-
err(`评论失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
/**
|
|
81
90
|
* openclawmp comments <assetRef>
|
|
82
91
|
*/
|
|
83
92
|
async function runComments(args) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
93
|
+
if (args.length === 0) {
|
|
94
|
+
err("Usage: openclawmp comments <assetRef>");
|
|
95
|
+
console.log(
|
|
96
|
+
" Example: openclawmp comments trigger/@xiaoyue/pdf-watcher",
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
102
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
103
|
+
|
|
104
|
+
const result = await api.get(`/api/assets/${asset.id}/comments`);
|
|
105
|
+
const comments = result?.data?.comments || result?.comments || [];
|
|
106
|
+
|
|
107
|
+
console.log("");
|
|
108
|
+
info(`${c("bold", displayName)} 的评论(${comments.length} 条)`);
|
|
109
|
+
console.log(` ${"─".repeat(50)}`);
|
|
110
|
+
|
|
111
|
+
if (comments.length === 0) {
|
|
112
|
+
console.log(` ${c("dim", "暂无评论。成为第一个评论者吧!")}`);
|
|
113
|
+
console.log("");
|
|
114
|
+
console.log(` openclawmp comment ${args[0]} "你的评论"`);
|
|
115
|
+
} else {
|
|
116
|
+
for (const cm of comments) {
|
|
117
|
+
const author =
|
|
118
|
+
cm.author?.name ||
|
|
119
|
+
cm.authorName ||
|
|
120
|
+
cm.commenterType ||
|
|
121
|
+
"anonymous";
|
|
122
|
+
const rating = cm.rating ? ` ${renderRating(cm.rating)}` : "";
|
|
123
|
+
const badge =
|
|
124
|
+
cm.commenterType === "agent" ? c("magenta", " 🤖") : "";
|
|
125
|
+
const time = fmtDate(cm.createdAt || cm.created_at);
|
|
126
|
+
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log(
|
|
129
|
+
` ${c("cyan", author)}${badge}${rating} ${c("dim", time)}`,
|
|
130
|
+
);
|
|
131
|
+
console.log(` ${cm.content}`);
|
|
132
|
+
}
|
|
114
133
|
}
|
|
115
|
-
|
|
116
|
-
console.log('');
|
|
134
|
+
console.log("");
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
module.exports = { runComment, runComments };
|
package/lib/commands/install.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const { execSync } = require('child_process');
|
|
10
9
|
const api = require('../api.js');
|
|
10
|
+
const { extractPackage } = require('../archive.js');
|
|
11
11
|
const config = require('../config.js');
|
|
12
12
|
const { fish, info, ok, warn, err, c, detail } = require('../ui.js');
|
|
13
13
|
|
|
@@ -42,48 +42,6 @@ function parseSpec(spec) {
|
|
|
42
42
|
return { type, slug, authorFilter };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Extract a tar.gz or zip buffer to a directory
|
|
47
|
-
*/
|
|
48
|
-
function extractPackage(buffer, targetDir) {
|
|
49
|
-
const tmpFile = path.join(require('os').tmpdir(), `openclawmp-pkg-${process.pid}-${Date.now()}`);
|
|
50
|
-
fs.writeFileSync(tmpFile, buffer);
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// Try tar first
|
|
54
|
-
try {
|
|
55
|
-
execSync(`tar xzf "${tmpFile}" -C "${targetDir}" --strip-components=1 2>/dev/null`, { stdio: 'pipe' });
|
|
56
|
-
return true;
|
|
57
|
-
} catch {
|
|
58
|
-
// Try without --strip-components
|
|
59
|
-
try {
|
|
60
|
-
execSync(`tar xzf "${tmpFile}" -C "${targetDir}" 2>/dev/null`, { stdio: 'pipe' });
|
|
61
|
-
return true;
|
|
62
|
-
} catch {
|
|
63
|
-
// Try unzip
|
|
64
|
-
try {
|
|
65
|
-
execSync(`unzip -o -q "${tmpFile}" -d "${targetDir}" 2>/dev/null`, { stdio: 'pipe' });
|
|
66
|
-
// If single subdirectory, move contents up
|
|
67
|
-
const entries = fs.readdirSync(targetDir);
|
|
68
|
-
const dirs = entries.filter(e => fs.statSync(path.join(targetDir, e)).isDirectory());
|
|
69
|
-
if (dirs.length === 1 && entries.length === 1) {
|
|
70
|
-
const subdir = path.join(targetDir, dirs[0]);
|
|
71
|
-
for (const f of fs.readdirSync(subdir)) {
|
|
72
|
-
fs.renameSync(path.join(subdir, f), path.join(targetDir, f));
|
|
73
|
-
}
|
|
74
|
-
fs.rmdirSync(subdir);
|
|
75
|
-
}
|
|
76
|
-
return true;
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} finally {
|
|
83
|
-
try { fs.unlinkSync(tmpFile); } catch {}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
45
|
/**
|
|
88
46
|
* Count files recursively in a directory
|
|
89
47
|
*/
|